Skip to content

Instantly share code, notes, and snippets.

@gaearon
Last active October 22, 2024 04:12
Show Gist options
  • Save gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 to your computer and use it in GitHub Desktop.
Save gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 to your computer and use it in GitHub Desktop.
useLayoutEffect and server rendering

If you use server rendering, keep in mind that neither useLayoutEffect nor useEffect can run until the JavaScript is downloaded.

You might see a warning if you try to useLayoutEffect on the server. Here's two common ways to fix it.

Option 1: Convert to useEffect

If this effect isn't important for first render (i.e. if the UI still looks valid before it runs), then useEffect instead.

function MyComponent() {
  useEffect(() => {
    // ...
  });
}

Like useLayoutEffect, it won't run on the server, but it also won't warn.

Option 2: Lazily show component with useLayoutEffect

If UI looks broken with useEffect but gets fixed by useLayoutEffect, it means that this component doesn't look right until the effect runs. However, that means the server-rendered HTML version of it won't look right until JavaScript loads anyway. So server-rendering it brings no benefit and shows a confusing UI.

To fix this, you can delay showing that component until after the client side JS loads and hydrates the component. To exclude a Child that needs layout effects from the server-rendered HTML, you can render it conditionally:

function Parent() {
  const [showChild, setShowChild] = useState(false);
  
  // Wait until after client-side hydration to show
  useEffect(() => {
    setShowChild(true);
  }, []);
  
  if (!showChild) {
    // You can show some kind of placeholder UI here
    return null;
  }

  return <Child {...props} />;
}

function Child(props) {
  useLayoutEffect(() => {
    // This is where your layout effect logic can be
  });
}

For example, this is handy for jQuery plugins which can be initialized later.


If you have some use case that isn't covered, please report a complete minimal code example here and we'll try to help.

@bacher
Copy link

bacher commented Nov 19, 2021

It's just amazing that this kludge is still the closest thing to an "official solution" to the noise-pollution problem.

I think it's shame... I like React but this decision is beyond common sense.

@mrkan-alelgn
Copy link

mrkan-alelgn commented Dec 22, 2021

@ValeryApps @asofiasbo I'm also having the same issue. did you manage to fix it?

@asofiasbo
Copy link

I haven't. I gave up on using Ant Design alongside NextJS @mrkan-alelgn

@Jackeriss
Copy link

Jackeriss commented Feb 3, 2022

@mrkan-alelgn @ValeryApps
I found the source of this issue in Ant Design issues. I guess we have to wait a bit :)

ant-design/ant-design#30396

@cha0s
Copy link

cha0s commented Mar 20, 2022

Please add an option to suppress this warning.

@johncmunson
Copy link

A more robust version written in typescript

export function UseLayoutEffectParent(props: {
  children: ReactNode
  placeholder?: ReactNode
}) {
  const [showChild, setShowChild] = useState(false)

  // Wait until after client-side hydration to show
  useEffect(() => {
    setShowChild(true)
  }, [])

  if (!showChild) {
    // You can show some kind of placeholder UI here
    return props.placeholder ? <>{props.placeholder}</> : null
  }

  return <>{props.children}</>
}

@dr-skot
Copy link

dr-skot commented May 1, 2022

The problem with some of the solutions here is they require changing your actual code. You have to replace all your useLayoutEffect calls with a custom useBrowserLayoutEffect—or worse, wrap them all in if statements! which is specifically forbidden. You’re kludging perfectly correct code throughout your codebase, for the sole purpose of suppressing a bogus console warning.

And if the offending code is in an imported module, you can't kludge it.

This solution is one line at the top level of your project and you’re done. Note that it changes nothing that’s actually executed. It replaces a function that never runs with a function that does nothing.

// suppress useLayoutEffect (and its warnings) when not running in a browser
if (typeof window === "undefined") React.useLayoutEffect = () => {};

On the server side, useLayoutEffect is never executed but generates a warning. To suppress the warning, replace the never-executed function with noop. On the client side, this replacement doesn't happen, and all your plain old useLayoutEffects run normally, unkludged and unconditional the way God intended.

@cha0s
Copy link

cha0s commented May 2, 2022

the way God intended

I didn't come from no monkey(patch)! ;)

I actually think creating an explicit new hook to paper over the lack of error suppression is a better approach than hiding it behind a monkeypatch -- your code works great until you port it to an environment that doesn't patch React...

@dr-skot
Copy link

dr-skot commented May 2, 2022

@cha0s maybe you're right. I'm not aware of an environment where React isn't patchable. I do see your point about making things explicit. But in this case monkeypatching away a method that does literally nothing but pollute the console with unhelpful warnings seems less offensive to me than refactoring the whole codebase to accomplish the same thing. Especially if some of the warnings are coming from imported modules I can't refactor.

And if a future React makes the workaround unnecessary, there's only one line of code to delete.

@tenorok
Copy link

tenorok commented May 24, 2022

Why do we need a warning about useLayoutEffect() when we haven't warning about useEffect()? Does anyone expect that useLayoutEffect() will do something on the server?

@dr-skot
Copy link

dr-skot commented May 24, 2022

@tenorok The reason is that useLayoutEffect runs before the DOM is first rendered, whereas useEffect runs after.

With useEffect, the first render on the client is going to be consistent with the server-side render. Because the server doesn't run useEffect, and the client doesn't run it until after the first render.

But useLayoutEffect runs before the client's first render, and the server doesn't run it at all. So there might be a mismatch between the server's render and the client's first render. That's what the warning is telling us.

(Edit: of course, to be technically precise, it's not the functions useEffect and useLayoutEffect, it's the callbacks passed to them, that run at different points in the component lifecycle).

That's the reasoning, but this distinction is really splitting hairs. If useEffect changes the view (without waiting for some sort of promise to resolve) the update probably happens before the user can see the first render. And if useLayoutEffect waits for a promise, the server's render will display in the meantime.

So yeah, warning about one and not the other is highly questionable.

@catamphetamine
Copy link

catamphetamine commented Jun 6, 2022

Bad change. No logic. Disagree.

You’re kludging perfectly correct code throughout your codebase, for the sole purpose of suppressing a bogus console warning.

+1

@xenobytezero
Copy link

Arrived here cause my console is being polluted with this too. For non SSR WebComponents, all of the recommendation is to use useLayoutEffect to dynamically import the component, but it means I have a ton of these warnings. Need to figure out a way to suppress that works for me.

@xav-ie
Copy link

xav-ie commented Jul 28, 2022

After learning a lot more about hooks and the lifecycle of SSR with those hooks, I think it is important to mention that useEffect and useLayoutEffect will always run after initial load from the server (I did not get this for some reason the first few times I was on this thread!). This means that whatever you choose to SSR render, will simply render as whatever you put in your return (…) first. Then, your useLayoutEffect will run and then your useEffect. But these happen after your component is already being shown to the user! The lifecycle of useLayoutEffect running before anything has rendered has changed in SSR.

My main use of using useLayoutEffect was the affect height of elements based on window size before they were rendered by SSR, but that is not possible—they have to first render by SSR, which does not have access to the window! If you want something to change visually based on some browser/window state and variables before it is rendered, you cannot use useLayoutEffect or useEffect! The component will first render from server and then run these hooks, leading to size flickering, etc.

For me, it boils down to three options:

  1. Change useLayoutEffect to useEffect: This is important because useEffect you can add css transitions to make the re-sizing less jarring and almost unnoticeable. With useLayoutEffect, these transitions often run less smoothly due to the way the hook works. Source: https://blog.thoughtspile.tech/2021/11/15/unintentional-layout-effect/
  2. Simply dynamically import the component as a client component only, making useLayoutEffect actually run before first render of the component! (However, the client component renders after server rendered components, making it flicker in if you do not animate it in :( )
  3. Lazily show component with useLayoutEffect: This is nearly the same as the approach as I just mentioned in the previous option and the result is nearly the same. Also, it will flicker in unless you animate it in. The main difference with this approach is that the code for the component is not dynamically imported, so it will be bundled (but not show!) with the first render. This approach also seems easier to add animations to.

With SSR, both useLayoutEffect and useEffect run after your first render from the server, so I don’t really see any reason for someone needing to use useLayoutEffect.

For some reason, I just did not grasp this when I first was dealing with this, and came up with many convoluted workarounds. Hopefully this word dump will help someone.

@bsimone
Copy link

bsimone commented Nov 23, 2022

Everyone is discussing this issue in context of testing. I am seeing this issue due to integrations with non-react library (e.g. leaflet). I'd like to use IconButton and Avatar from material UI as marker on the map. The only solution from leaflet perspective to use DivIcon but it requires to render React Elements to string

const html = new L.DivIcon({
    className: 'my-div-ico',
    html: ReactDOMServer.renderToString(
       <IconButton>
          <Avatar>My</Avatar>
       </IconButton>
    ),
    ...
  })

Are there any suggestions about eliminating the warning in this case? None of proposals discussed in this thread do not work.

Hi @fogfish , have you been able to solve this? I've the same problem.

Thanks

@co-bby
Copy link

co-bby commented Feb 6, 2023

Just put this in pages/_app.js or some other top-level place:

// suppress useLayoutEffect warnings when running outside a browser
if (!process.browser) React.useLayoutEffect = React.useEffect;

useLayoutEffect and useEffect have the same argument signature, and neither runs if we're not in a browser. So if we're not in a browser it's safe to globally replace the one that triggers warnings with the one that doesn't.

(If you don't want to rely on the process global, if (typeof window === 'undefined') works too.)

Edit: the above still works, but replacing with () => {} is probably clearer.

this worked for me! but it is showing .browser is depreciated!

@bacher
Copy link

bacher commented Feb 8, 2023

Just put this in pages/_app.js or some other top-level place:

// suppress useLayoutEffect warnings when running outside a browser
if (!process.browser) React.useLayoutEffect = React.useEffect;

useLayoutEffect and useEffect have the same argument signature, and neither runs if we're not in a browser. So if we're not in a browser it's safe to globally replace the one that triggers warnings with the one that doesn't.
(If you don't want to rely on the process global, if (typeof window === 'undefined') works too.)
Edit: the above still works, but replacing with () => {} is probably clearer.

this worked for me! but it is showing .browser is depreciated!

You could use typeof window === undefined instead of !process.browser. It's recommended way now.

@portedison
Copy link

@AndyBoat
Copy link

AndyBoat commented Mar 3, 2023

After learning a lot more about hooks and the lifecycle of SSR with those hooks, I think it is important to mention that useEffect and useLayoutEffect will always run after initial load from the server (I did not get this for some reason the first few times I was on this thread!). This means that whatever you choose to SSR render, will simply render as whatever you put in your return (…) first. Then, your useLayoutEffect will run and then your useEffect. But these happen after your component is already being shown to the user! The lifecycle of useLayoutEffect running before anything has rendered has changed in SSR.

My main use of using useLayoutEffect was the affect height of elements based on window size before they were rendered by SSR, but that is not possible—they have to first render by SSR, which does not have access to the window! If you want something to change visually based on some browser/window state and variables before it is rendered, you cannot use useLayoutEffect or useEffect! The component will first render from server and then run these hooks, leading to size flickering, etc.

For me, it boils down to three options:

  1. Change useLayoutEffect to useEffect: This is important because useEffect you can add css transitions to make the re-sizing less jarring and almost unnoticeable. With useLayoutEffect, these transitions often run less smoothly due to the way the hook works. Source: https://blog.thoughtspile.tech/2021/11/15/unintentional-layout-effect/
  2. Simply dynamically import the component as a client component only, making useLayoutEffect actually run before first render of the component! (However, the client component renders after server rendered components, making it flicker in if you do not animate it in :( )
  3. Lazily show component with useLayoutEffect: This is nearly the same as the approach as I just mentioned in the previous option and the result is nearly the same. Also, it will flicker in unless you animate it in. The main difference with this approach is that the code for the component is not dynamically imported, so it will be bundled (but not show!) with the first render. This approach also seems easier to add animations to.

With SSR, both useLayoutEffect and useEffect run after your first render from the server, so I don’t really see any reason for someone needing to use useLayoutEffect.

For some reason, I just did not grasp this when I first was dealing with this, and came up with many convoluted workarounds. Hopefully this word dump will help someone.

I just came into a same place like your description, and finally I had to use dynamic and set {ssr:false} in my Next.js app to presever useLayoutEffect. I agree with you that in the situation where useLayoutEffect is needed, mostly it can't be replaced by useEffect.

@joshborseth
Copy link

thanks Dan!!!!

@sairion
Copy link

sairion commented Mar 30, 2023

It should be noticed the warning is removed for the upcoming versions.
See PR from the last week [Remove layout effect warning on the server #26395]:
facebook/react#26395

@icobg123
Copy link

icobg123 commented Apr 4, 2023

Everyone is discussing this issue in context of testing. I am seeing this issue due to integrations with non-react library (e.g. leaflet). I'd like to use IconButton and Avatar from material UI as marker on the map. The only solution from leaflet perspective to use DivIcon but it requires to render React Elements to string

const html = new L.DivIcon({
    className: 'my-div-ico',
    html: ReactDOMServer.renderToString(
       <IconButton>
          <Avatar>My</Avatar>
       </IconButton>
    ),
    ...
  })

Are there any suggestions about eliminating the warning in this case? None of proposals discussed in this thread do not work.

Hi @fogfish , have you been able to solve this? I've the same problem.

Thanks

did you find a solution to this? I'm in running into the same issue

@pm0u
Copy link

pm0u commented Jul 27, 2023

So if we're on React 18.2.0 and still seeing this error what does that mean given that a "fix" was supposedly merged?

@Viktorherald
Copy link

So if we're on React 18.2.0 and still seeing this error what does that mean given that a "fix" was supposedly merged?

I also having the same issue as you

i notice 18.2 is released on 2022, and the code is merged on 2023
so it is just a matter of time if React is releasing a new version anytime soon... or not

@pm0u
Copy link

pm0u commented Aug 7, 2023

@Viktorherald thanks, hadn't noticed the dates. waiting patiently!

@andrecasal
Copy link

andrecasal commented Sep 14, 2023

A better solution is to use an isomorphic effect.

hooks.ts:

import { useEffect, useLayoutEffect } from 'react'

export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect

Component.tsx:

import { useIsomorphicLayoutEffect } from 'hooks.ts'

const Component = () => {
	// Server? useEffect(). Client? useLayoutEffect() 🎉
	useIsomorphicLayoutEffect(() => {})

	return <p>Cool component</p>
}

If you're going to be using a lot of these hooks, you can also just npm i usehooks-ts.

@AlexIsMaking
Copy link

A better solution is to use an isomorphic effect.

This works great, thanks for sharing!

@nwazuo
Copy link

nwazuo commented Apr 6, 2024

A better solution is to use an isomorphic effect.

hooks.ts:

import { useEffect, useLayoutEffect } from 'react'

export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect

Component.tsx:

import { useIsomorphicLayoutEffect } from 'hooks.ts'

const Component = () => {
	// Server? useEffect(). Client? useLayoutEffect() 🎉
	useIsomorphicLayoutEffect(() => {})

	return <p>Cool component</p>
}

If you're going to be using a lot of these hooks, you can also just npm i usehooks-ts.

I use this, works great!

@gmjacob
Copy link

gmjacob commented Apr 15, 2024

A better solution is to use an isomorphic effect.

hooks.ts:

import { useEffect, useLayoutEffect } from 'react'

export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect

Component.tsx:

import { useIsomorphicLayoutEffect } from 'hooks.ts'

const Component = () => {
	// Server? useEffect(). Client? useLayoutEffect() 🎉
	useIsomorphicLayoutEffect(() => {})

	return <p>Cool component</p>
}

If you're going to be using a lot of these hooks, you can also just npm i usehooks-ts.

Thank you!

This solved my problem as well 🙏

@bryanrasmussen
Copy link

just because useLayoutEffect is called does not mean that the display will not look correct unless useLayoutEffect can run, otherwise if this were the case the whole concept of Progressive enhancement would not make sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment