NextJs passing dynamic width on resize not re-rendering
Asked Answered
L

3

7

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;
Lazes answered 25/3, 2020 at 16:57 Comment(0)
I
16

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.

Inbound answered 25/3, 2020 at 18:16 Comment(0)
L
1

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
Lazes answered 25/3, 2020 at 19:5 Comment(1)
Facundo Malgieri using width as a dependency for useEffect is not optimal, because useEffect will be called each time width changes, however, useEffect is not dependant on width in your case. You should use an empty array instead. The reason I added handleResize is because it is a dependency and linters would give a warning if it is not used, but in your case an empty array would suffice.Inbound
T
1

Hook

const useWidth = () => {
  const [width, setWidth] = useState(0)
  const handleResize = () => setWidth(window.innerWidth)
  useEffect(() => {
      //make sure it set properly on the first load (before resizing)
      handleResize()
      window.addEventListener('resize', handleResize)
      return () => window.removeEventListener('resize', handleResize)
      // the next line for linters, so they won't give a warning
      // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []) // empty
  return width
}
Thumb answered 6/3, 2023 at 8:15 Comment(1)
Calling handleResize() within the useEffect block is what made this solution work for me. Other solutions would return undefined for the value of width on first load (because I was listening for a resize event), so I would always have bugs related to trying to compare width to some breakpoint.Bouldon

© 2022 - 2024 — McMap. All rights reserved.