-
Notifications
You must be signed in to change notification settings - Fork 9
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
Fix React SSR hydration ID mismatch error #315
Conversation
a00fc0e
to
8a2418b
Compare
Current unit coverage is 88.49693251533742% |
@@ -30,6 +30,10 @@ module.exports = { | |||
framework: '@storybook/react', | |||
staticDirs: ['./public'], | |||
webpackFinal: async (config) => { | |||
//use commonjs entry point for "@reach" packages | |||
config.resolve.alias['@reach/auto-id'] = require.resolve('@reach/auto-id'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
out of curiosity why is this necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
our storybook uses commonjs and doesn't seem to handle mjs files.
@reach/auto-id
does export a commonjs version. But its package.json is set to "module": "./dist/reach-auto-id.mjs",
and "main": "./dist/reach-auto-id.cjs.js",
. I believe Webpack, used by Storybook, by default will try to use "module" over "main". Alternatively, we could also do config.resolve.mainFields = ['main', 'module'];
but I thought the alias approach would be more specific.
tests/ssr/utils.tsx
Outdated
render(<App />, { container, hydrate: true }); | ||
expect(consoleErrorSpy).not.toBeCalledWith( | ||
expect.stringContaining('Warning: Prop `%s` did not match. Server: %s Client: %s%s'), | ||
'id', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't need to use string concatenation for a hardcoded value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed expect.stringContaining
, the rest of the arguments is needed in the order of the params received by console error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here it's react itself that's calling console.error . They probably use the same error call for all props which is why there's the string formatting bit there.
If react changes the implementation of their console.error call this test will stop functioning properly right? is there some other way to test this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea, all prop mismatched error will use this message format. I suppose we don't need to explicitly check for id
mismatched only, although I don't see other ways of testing this beside checking console error. I attempted to check for the html content of server vs client but it didn't work -- the actual of the container after hydration uses the same as server even though the error is present.
I decided to update the test to assert the number of error logged outside of the useLayoutEffect warning. This way, they may change their message format, the test will still fail as long as there is an error logged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure that makes sense, I can't think of a better way to test this. If they switch to console.warn for example maybe we start getting false positives from the test, but we'd also notice console.warn's in the jest output (hopefully). If they remove the console messages then 🤷 but seems unlikely
### Features - Default behavior of `FilterSearch` was changed to better support Locators and Doctor Finders. Additionally, a new `onSelect` prop was added to the Component. The `searchOnSelect` prop is now deprecated. (#323, #343, #333) - A new CSS bundle without the Tailwind resets is exported. (#322) - We've added a `MapboxMap` Component, powered by v2 of their JavaScript API. (#332) ### Changes - Assorted updates to improve our GH Actions. - Styling of Facet Headers is now exposed in `FilterGroupCssClasses`. (#321) ### Bug Fixes - Vulnerabilities were addressed for the repo and its test-site. - Fixed the Dropdown Component to invoke `preventDefault` only when it is active. (#307) - Corrected a small error in the generation of SSR Hydration IDs. (#315)
This PR is to fix the errors
Warning: Prop "id" did not match. Server: "some-id". Client: "another-id"
, reported in https://yext.slack.com/archives/C032CKFARGS/p1664916209295449React expects that the rendered content is identical between the server and the client. However, our usage of
uuid
means different id is generated from the HTML content generated through SSR vs the first render in client side during hydration process. This is a known issues in React SSR (issues in react repo[1][2][3]) and React released a new hookuseId
in React 18 to address it.I found three potential solutions for React <18 to generate unique but predictable id, each with an external library we could use:
SSRProvider
component to reset the count in server side to be zero for each fresh render. Multiple copies of React Aria is not supported so it would be a peer-depuse a global counter to increment on initial renders and expose a reset function for SSR side. This approach is implemented in react-id-generator. There's a function
resetId
required to invoke from SSR side (such as in PageJSserverRenderRoute
or somewhere in user's template page) to avoid mismatch during refresh, since browser generator will always restart from "1" but server's count will keep incrementing.Don't use IDs at all on the server; patch after first render. This approach is implemented in reach/auto-id. ID returned as undefined/empty on the first render so server and client have the same markup during hydration process. After render, patch the components with an incremented ID (second render) in useLayoutEffect. (more info here) No changes needed outside of using
import { useId } from '@reach/auto-id';
in our component lib.I decided to go with the last approach using
react/auto-id
because:useId
if it's availableuseId
. However, since we (1) only update the ID attribute on DOM on second render, and (2) our components that usesuseId
already requires multiple renders to get setup, the performance is not greatly affected. I used Profiler in our test-site to measure performance, the number of renders and time spent forSearchBar
,StaticFilters
, andFilterSearch
did not change as the dispatch fromuseId
is grouped with other changes required in our components on second render. This is only needed for React <18J=SLAP-2403
TEST=manual&auto
npm pack
a build of the component lib with the new changes and tested SearchBar, FilterSearch, and StaticFilers:See that the warning about Prop
id
mismatched no longer appear in consoleadded new jest test for Dropdown and StaticFilter, see that test failed the previous usage of uuid, and passed with reach/auto-id.