2

I have created a custom hook which will detect device on reload and resize.

import { useState, useEffect, useRef, useCallback } from "react";

export const DEVICE_SIZES_VALUE = {
  MOBILE: 576,
  TABLET: 768,
  DESKTOP: 992,
  LARGE_DESKTOP: 1200
};

export const DEVICE_TYPE = {
  MOBILE_ONLY: "mobileOnly",
  TABLET_ONLY: "tabletOnly",
  UP_TO_TABLET: "upToTablet",
  DESKTOP_ONLY: "desktopOnly"
};

const mobileOnly = () => window.innerWidth < DEVICE_SIZES_VALUE.MOBILE;
const tabletOnly = () =>
  window.innerWidth > DEVICE_SIZES_VALUE.MOBILE &&
  window.innerWidth < DEVICE_SIZES_VALUE.DESKTOP;

const upToTablet = () => window.innerWidth < DEVICE_SIZES_VALUE.DESKTOP;

const desktopOnly = () => window.innerWidth > DEVICE_SIZES_VALUE.DESKTOP;

const findViewType = deviceType => {
  switch (deviceType) {
    case DEVICE_TYPE.MOBILE_ONLY:
      return mobileOnly();

    case DEVICE_TYPE.TABLET_ONLY:
      return tabletOnly();

    case DEVICE_TYPE.UP_TO_TABLET:
      return upToTablet();

    case DEVICE_TYPE.DESKTOP_ONLY:
      return desktopOnly();

    default:
      return false;
  }
};

export const useView = type => {
  const isInit = useRef(true);
  const [isTypeFound, setIsTypeFound] = useState(false);

  const handleResize = useCallback(() => {
    setIsTypeFound(findViewType(type));
  }, [type]);

  useEffect(() => {
    // To avoid window undefined error in SSR
    if (process.browser) {
      window.addEventListener("resize", handleResize);
      return () => {
        window.removeEventListener("resize", handleResize);
      };
    }
  }, [handleResize]);

  useEffect(() => {
    // will Skip this after first render
    if (isInit && isInit.current) {
      setIsTypeFound(findViewType(type));
      isInit.current = false;
    }
  }, [isInit, type]);

  return isTypeFound;
};

in the pages folder home route file is

import Home from "../Home";

export default function IndexPage() {
  return (
    <div>
      <Home />
    </div>
  );
}

And in the home folder

import { useView } from "../customHook";

const Home = () => {
  const isTabletOnly = useView("upToTablet");
  console.log("==rendering===", isTabletOnly);

  return (
    <>
      {isTabletOnly && <p>this is responsive</p>}
      <p>this is regular </p>
    </>
  );
};

export default Home;

Issue:

  • If you see the console on tablet or mobile view it will log two times on refresh or if i land on that page from other route. Which means two times rendering
  • I know that this is happening due to state change

Questions:

  1. Is there any work around to solve that multi rendering issue?
  2. The way i have written can you suggest me to some best practice that could be done here.

NOTE: all the constants will be in separate file. SandBox Link

UPDATE:

Question No 1. can be solved by this useState(() => findViewType(type)) as @Drew Reese said.

Unfortunately more issue arise

Issue:

  1. during SSR findViewType(type) will show error as the window is not defined

  2. if i bypass that by using !process.browser then this type of issue will appear on refresh

<div className={`class_1 ${isTabletOnly ? 'class_2' : 'class_3' }`}>

</div>

this will shoot an warning SSR and CSR are different. And my responsive related class(class_2) will not append though isTabletOnly is true because it will come from Server side.

  1. if i use the window error bypass like this there will be eslint warning
export const useView = (type) => {
  if (!process.browser) return false;
  // my rest of the code
}
Monzoor Tamal
  • 790
  • 5
  • 15
  • Does this answer your question? [Why does useState cause the component to render twice on each update?](https://stackoverflow.com/questions/61578158/why-does-usestate-cause-the-component-to-render-twice-on-each-update) – Drew Reese Jul 23 '20 at 21:46
  • @DrewReese thanks for answering. I did try to make strict mode to false in next.config. But no luck. – Monzoor Tamal Jul 23 '20 at 21:54
  • You also shouldn't console log (or have any other side-effects) right in the functional component body. Try placing that log in a `useEffect` hook to see if it's *actually* rendering twice (from update) or not (just the double render invocation). – Drew Reese Jul 23 '20 at 23:55
  • @DrewReese sorry for the late reply. I know why this is happening. a. First render in server and gives it false. b. then first render in client with false as initial state is false. c. useView pass the type and hook setState with new value and rerender. I was looking for is there any way to eliminate the second step. – Monzoor Tamal Jul 25 '20 at 18:08
  • Listening for resizes is bad in this case, [`window.matchMedia`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) with change listeners is way more effective. You might want to check out [`useMediaQuery`](https://github.com/beautifulinteractions/beautiful-react-hooks/blob/master/docs/useMediaQuery.md) – Mordechai Jul 26 '20 at 21:11

1 Answers1

2

I believe you can condense the first two renders into the initial render with a state initializer function.

Lazy initial state

The initialState argument is the state used during the initial render. In subsequent renders, it is disregarded. If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render

const [isTypeFound, setIsTypeFound] = useState(
  () => findViewType(type) // <-- pass the type prop
);

This will run for the initial render and provide the actual component mounted state you desire.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Awesome. Thanks a lot. I totally forgot about that. This will perfectly work with reactjs project. Unfortunately not with NextJs. I have updated my question. – Monzoor Tamal Jul 25 '20 at 21:50
  • @MonzoorTamal I was thinking that maybe you could use `useLayoutEffect` instead, but the docs have a nice tip using useEffect and SSR to conditionally hide the UI until it's been hydrated: https://reactjs.org/docs/hooks-reference.html#uselayouteffect Maybe this'll help you a bit. – Drew Reese Jul 29 '20 at 06:46