NextJs passing dynamic width on resize not re-rendering

Viewed 8427

I'm trying to dynamically pass the width to a component's styles. On first load, it's okay, but if I resize it never re renders the component, even though the hook is working.

I read about that since NextJs is server side rendering this can cause this client side's issues. So here's the code:

Hook

const useWidth = () => {
  if (process.browser) {
    const [width, setWidth] = useState(window.innerWidth);
    const handleResize = () => setWidth(window.innerWidth);
    useEffect(() => {
      window.addEventListener('resize', handleResize);
      return () => window.removeEventListener('resize', handleResize);
    }, [width]);
    return width;
  }
  return 0;
};

Component (reduced just to show the example)

const Login = () => {
  const windowWidth = useWidth();
  const width = windowWidth > CELLPHONE_WIDTH ? '36.6rem' : '90%';
  const loginStyles = styles(width);
  return (
    <div className='container'>
      <TextInput
        type='text'
        width={width}
        placeholder='Email'
      />
    </div>
  );
};

Styles

function textInputStyles(width) {
  return css`
    width: ${width};
  `;
}

export default textInputStyles;
2 Answers

Problem here is the code first runs on server side with Next.js. Because process.browser returns false on the server side, your hook logic is never registered. Only a 0 is returned. Since no hook has been registered and no event has been set, changing window size will not trigger a re-render.

You need to use a componentDidMount() or a useEffect.

Here is an example for your case that would work.

const useWidth = () => {
    const [width, setWidth] = useState(0); // default width, detect on server.
    const handleResize = () => setWidth(window.innerWidth);
    useEffect(() => {
      window.addEventListener('resize', handleResize);
      return () => window.removeEventListener('resize', handleResize);
    }, [handleResize]);
    return width;
};

On the other hand, if you want to ensure that your initial state is that of the browser window, you can load your component dynamically on the client side only.

import dynamic from 'next/dynamic'
const Login = dynamic(
  () => import('./pathToLogin/Login'),
  { ssr: false },
)

and in your component where Login is used.

const TopLevelComponent = () => {
 <Login {...props} />
}

and then you can use the window object freely in your Login component.

const useWidth = () => {
  // Use window object freely
  const [width, setWidth] = useState(window.innerWidth); // default width, detect on server.

Refer to this if there is still confusion.

Thanks a lot Hassaan Tauqir for your help!!! :D

When I saw your first answer I tried it but couldn't call the custom hook inside useEffect because it was breaking the rule Call Hooks from React function components

But I managed to achieve the solution with this code, that is almost the same as the one you posted after you edited the answer. The only difference is that in the dependencies array of the useEffect inside the custom hook im using width instead of the handler. Dunno if that makes any difference in this case but its working perfectly.

const useWidth = () => {
  const [width, setWidth] = useState(0);
  const handleResize = () => setWidth(window.innerWidth);
  useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, [width]);
  return width;
};

And from the component Im using it like:

const Login = () => {
  const [width, setWidth] = useState('0');
  const windowWidth = useWidth();
  useEffect(() => {
    if (windowWidth < CELLPHONE_WIDTH) {
      setWidth('90%');
    } else {
      setWidth('36.6rem');
    }
  }, []);
// rest of the code
Related