From 6d3e3dc6cc3c418903150cca76758fbda1f0f63a Mon Sep 17 00:00:00 2001 From: Josh Black Date: Thu, 9 Jul 2020 16:39:27 -0500 Subject: [PATCH] fix(react): update useId to match id on server and client (#6369) Co-authored-by: Andrea N. Cardona Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../internal/__tests__/useId.server-test.js | 23 ++++++++ packages/react/src/internal/useId.js | 53 +++++++++++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 packages/react/src/internal/__tests__/useId.server-test.js diff --git a/packages/react/src/internal/__tests__/useId.server-test.js b/packages/react/src/internal/__tests__/useId.server-test.js new file mode 100644 index 000000000000..11974ddbf2ee --- /dev/null +++ b/packages/react/src/internal/__tests__/useId.server-test.js @@ -0,0 +1,23 @@ +/** + * Copyright IBM Corp. 2016, 2020 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment node + */ + +import React from 'react'; +import { renderToString } from 'react-dom/server'; +import { useId } from '../useId'; + +describe('useId SSR', () => { + it('should not generate an id on the server', () => { + function Test() { + const id = useId('test'); + return test; + } + const markup = renderToString(); + expect(markup.indexOf('id="')).toBe(-1); + }); +}); diff --git a/packages/react/src/internal/useId.js b/packages/react/src/internal/useId.js index 10bba13133ea..8b5e0e07abf1 100644 --- a/packages/react/src/internal/useId.js +++ b/packages/react/src/internal/useId.js @@ -5,10 +5,39 @@ * LICENSE file in the root directory of this source tree. */ -import { useRef } from 'react'; +// This file was heavily inspired by Reach UI and their work on their auto-id +// package +// https://github.com/reach/reach-ui/blob/86a046f54d53b6420e392b3fa56dd991d9d4e458/packages/auto-id/src/index.ts +// +// The problem that this solves is an id mismatch when auto-generating +// ids on both the server and the client. When using server-side rendering, +// there can be the chance of a mismatch between what the server renders and +// what the client renders when the id value is auto-generated. +// +// To get around this, we set the initial value of the `id` to `null` and then +// conditionally use `useLayoutEffect` on the client and `useEffect` on the +// server. On the client, `useLayoutEffect` will patch up the id to the correct +// value. On the server, `useEffect` will not run. +// +// This ensures that we won't encounter a mismatch in ids between server and +// client, at the cost of runtime patching of the id value in +// `useLayoutEffect` + +import { useEffect, useLayoutEffect, useState } from 'react'; import setupGetInstanceId from '../tools/setupGetInstanceId'; const getId = setupGetInstanceId(); +const useIsomorphicLayoutEffect = canUseDOM() ? useLayoutEffect : useEffect; + +function canUseDOM() { + return !!( + typeof window !== 'undefined' && + window.document && + window.document.createElement + ); +} + +let serverHandoffCompleted = false; /** * Generate a unique ID with an optional prefix prepended to it @@ -16,6 +45,24 @@ const getId = setupGetInstanceId(); * @returns {string} */ export function useId(prefix = 'id') { - const ref = useRef(`${prefix}-${getId()}`); - return ref.current; + const [id, setId] = useState(() => { + if (serverHandoffCompleted) { + return `${prefix}-${getId()}`; + } + return null; + }); + + useIsomorphicLayoutEffect(() => { + if (id === null) { + setId(`${prefix}-${getId()}`); + } + }, [getId]); + + useEffect(() => { + if (serverHandoffCompleted === false) { + serverHandoffCompleted = true; + } + }, []); + + return id; }