0

I need to conditionally render components based on screen size.

I use nextjs and getInitialProps for data fetching, the page is server-side rendered. I want to detect device screen size on the client-side, so I implement a customized hook to do it.

useWindowSize.js

import { useEffect, useState } from 'react';

export default function useWindowSize() {
    const [windowSize, setWindowSize] = useState({
        width: typeof window === 'undefined' ? 1200 : window.innerWidth, // default width 1200
    });

    useEffect(() => {
        // Handler to call on window resize
        function handleResize() {
            // Set window width/height to state
            setWindowSize({
                width: window.innerWidth,
                //height: window.innerHeight,
            });
        }

        // Add event listener
        window.addEventListener('resize', handleResize);

        // Call handler right away so state gets updated with initial window size
        handleResize();

        // Remove event listener on cleanup
        return () => window.removeEventListener('resize', handleResize);
    }, []); // Empty array ensures that effect is only run on mount

    return windowSize.width <= 600;
}

then I use this hook to detect window size and conditional render components

export default function IndexPage() {
  const isMobile = useWindowSize();

  if (typeof window !== "undefined") {
    // if you are running it on codesanbox, I don't know why log is not printed
    console.log("client side re-render");
  }

  return (
    <div>
      {isMobile ? (
        <div
          style={{
            color: "red",
            fontSize: 40
          }}
        >
          mobile
        </div>
      ) : (
        <div
          style={{
            color: "blue",
            fontSize: 20
          }}
        >
          desktop
        </div>
      )}
    </div>
  );
}

IndexPage.getInitialProps = () => {
  return {
    a: 1
  };
};

when I load the page on mobile browser, you will see

image

text mobile is applied wrong CSS style. video demo: https://share.getcloudapp.com/nOuk08L0

how to reproduce: https://codesandbox.io/s/thirsty-khayyam-npqpt

Can someone please help me out. Thank you in advance!

2 Answers 2

1

This is an issue that is related to how React patch up DOM from SSR. When there is a mismatch between client-side and server-side rendering, React will only patch/sync the text context for the node. The DOM attribute will not be automatically updated. In your case, the SSR result has the desktop style because there is no window object, and client side has the mobile result. After the mismatch, React update the text node from 'desktop' to mobile but not the style attributes.

In my opinion, you can use two different approaches. You can use Media Query to style your component based on the screen width instead of the hook. If you are doing SSR, not SSG, you can use user agent req.headers["user-agent"] to detect the device your device is being viewed on.

For the first approach, you might need to render more DOM node you might need to. For the second approach, you won't be able to know the actual viewport size, which can cause visual issue. You might be able to combine both approach to produce a good viewing experience for your user.

Reference

https://github.com/facebook/react/issues/11128#issuecomment-334882514

Sign up to request clarification or add additional context in comments.

Comments

0

Thanks for @Andrew Zheng's detailed explanation! Today I learned.

I know that I can style the layout by using pure CSS media query, but my use case needs a variable like isMobile to

if (isMobile) {
    doSomethingOnlyOnMobileWeb();
} else {
    doSomethingOnlyForDesktopWeb();
}

So I combined two approaches you provided, and modify my hook this way:

export default function useWindowSize(userAgent) {
    let isMobile = Boolean(
        userAgent &&
            userAgent.match(
                /Android|BlackBerry|iPhone|iPod|Opera Mini|IEMobile|WPDesktop/i
            )
    );
    
    const [windowSize, setWindowSize] = useState({
        width: isServer
            ? isMobile
                ? BREAKPOINT_SMALL
                : BREAKPOINT_LARGE
            : window.innerWidth,
        
    });

    useEffect(() => {
        // Handler to call on window resize
        function handleResize() {
            // Set window width/height to state
            setWindowSize({
                width: window.innerWidth,
                //height: window.innerHeight,
            });
        }

        // Add event listener
        window.addEventListener('resize', handleResize);

        // Call handler right away so state gets updated with initial window size
        handleResize();

        // Remove event listener on cleanup
        return () => window.removeEventListener('resize', handleResize);
    }, []); // Empty array ensures that effect is only run on mount

    return windowSize.width <= BREAKPOINT_SMALL;
}

diff: passing user-agent string to useWindowSize for server-side detection and use window.innerWidth for client-side detection. There won't be a mismatch between server and client.

1 Comment

I’m glad you figured out the answer!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.