nextjs custom responsive detection hook called twice

Viewed 1505

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
}
1 Answers

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.

Related