-
Notifications
You must be signed in to change notification settings - Fork 45.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug: startTransition
not working with useSyncExternalStore
#24810
Comments
As noted in But this still leaves a question, how do I avoid showing a fallback during hydration if I need to update the store on the component's mount? |
@stanislav-halyn hi! I recently met a similar problem, and found answer here - #24476 (comment) In this example, you don't pass props from user store to suspended component directly. export default function Layout() {
const user = useUser();
useEffect(() => {
startTransition(() => {
User.updateUser({
data: {
firstName: "Steve"
}
});
});
}, []);
const memoizedContent = useMemo(
() => (
<Suspense fallback={<Loader />}>
<LazyContent />
</Suspense>
),
[]
);
return (
<div>
<header>this is header</header>
<br />
<p>user data: {JSON.stringify(user.data)}</p>.
<br />
<main>{memoizedContent}</main>
</div>
);
} |
In general, updating store “on mount” isn’t a great pattern. Think about it: from the user’s perspective, there was no interaction. Nothing really “changed” in the app. The user did not provide any new information. The app didn’t download any new information. So why does it need to update? What new information is there that hasn’t been there since the beginning? I think you’ll find that with some restructuring, you should be able to get rid of store updates “on mount”. That will both solve this problem and improve performance in general. |
@gaearon Thanks for your reply! I don't know any other way of fetching user's data, but on the component's mount. In the end, the recommended way of using |
The recommended solution with SSR is to fetch data during SSR rather than on mount. Otherwise, SSR somewhat loses its purpose — the app's HTML is a shell that has no useful content. I wonder if you've considered this option? This is what frameworks like Next.js usually do. |
@gaearon in our case we don't render on SSR the whole page content, but only a part of this, which is shared between users. This enables us to use shared cache between users, but disables us from fetching user's data on SSR. So basically in our situation we have to fetch some data on components mount rather than during SSR, because we don't need to render everything during SSR and want to use shared cache between users |
OK, got it. It's a little unfortunate situation but I guess the fix would be to memoize the content to "prove" that it doesn't depend on the data you just fetched, and so it is safe for React to preserve the server HTML. const MainContent = memo(function MainContent() {
return (
<main>
<Suspense fallback={<Loader />}>
<LazyContent />
</Suspense>
</main>
);
}); A more canonical fix would be to SSR the entire page for every user, and then do data fetching on the server. I see your point re: caching, but I'm curious if you need to cache render output or shared data for users. Because if it's enough to cache some shared data then you can still have a shared cache between separate SSR requests. However, if it's important to cache render output, you'd have to wait for React to add special support for caching between SSR. It's something we've been exploring but it's not likely to come very soon. |
@gaearon thanks for the suggestion :) Unfortunately, we cache render output and we can't change that. |
We can preserve server HTML when we have a guarantee that an update at the top does not flow through the dehydrated component. So, there are two ways to break this guarantee:
If either of those things happen, we'll have to replace HTML with the fallback. So, to answer your question, updating the store above by itself is not a huge problem as long as you memoize the lazy content. However, if you pass the store content as a prop to the lazy content, it would have to be replaced (because preserving the server HTML is not safe anymore — e.g. imagine a Theme toggle: you don’t want to see half of the app use a Dark theme and another half use the Light theme). Similarly, if you put the store state into a Context Provider above the lazy content, this would cause the lazy content HTML to be replaced. This is because until we load the code, we can't know whether the lazy content reads that context. Therefore, we have to replace it if any context changes above it. |
Note there's also another option, which is to not SSR LazyContent either. I.e. you can always emit fallback on the server. Then it won't be in HTML (which is unfortunate), but you'll also avoid this problem. If you decide to go that route, you need to throw an Error on the server only from inside the part you want to make client-only (like this lazy content). When you throw an Error on the server, with streaming SSR, the error won't fail the render. Instead, the server will emit the Suspense fallback. The client will retry rendering when the code loads. During retry, you won't throw, so lazy content will be able to show up. The error will be reported in the console, but you can pass |
@gaearon At Unsplash we are using this pattern quite a bit, specifically where we need to store a value in Redux that can only be known on the client-side. For example, the window width or a value that is derived from local storage. These value can not be determined during SSR, and we can't update the store before the first client render because this would result in a "hydration mismatch" whereby the first client render doesn't match the server render. For this reason we dispatch an action on mount to update the store, then React can do an additional render. I don't see any other way of doing this but maybe I'm missing something. For context: I'm coming here from reduxjs/react-redux#1962. We can workaround this issue by using |
Another case is when How can the hydration error be avoided in this case? |
SSR content shows a fallback during hydration if there has been an update to an external state, even if wrapped with
startTransition
.React version: 18.2.0
Steps To Reproduce
useSyncExternalStore
in the same component which has suspensible content.startTransition
;Link to code example: https://codesandbox.io/s/react-18-starttransition-not-working-with-usesyncexternalstore-4pxygp
The current behavior
The content shows a fallback while the update is done.
The expected behavior
The content should not show a fallback, but rather update without showing it.
The text was updated successfully, but these errors were encountered: