diff --git a/.changeset/bright-apes-sparkle.md b/.changeset/bright-apes-sparkle.md new file mode 100644 index 0000000000..0afa858d4b --- /dev/null +++ b/.changeset/bright-apes-sparkle.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/wonder-blocks-core": major +--- + +- Remove `RenderState.Root` from exported enum +- Change `useRenderState` to only return `RenderState.Initial` or `RenderState.Standard` diff --git a/__docs__/wonder-blocks-core/exports.use-render-state.mdx b/__docs__/wonder-blocks-core/exports.use-render-state.mdx index a532c93a99..d8f6ed0d85 100644 --- a/__docs__/wonder-blocks-core/exports.use-render-state.mdx +++ b/__docs__/wonder-blocks-core/exports.use-render-state.mdx @@ -1,8 +1,6 @@ import {Meta} from "@storybook/blocks"; - + # useRenderState() @@ -16,6 +14,3 @@ The `useRenderState` hook will return either: the initial rehydration render on the client. - `RenderState.Standard` if the component renders on the client after the initial rehydration. - -NOTE: Although the `RenderState` enum has a third state `Root`, this value is never -returned by `useRenderState`. diff --git a/packages/wonder-blocks-core/src/components/initial-fallback.tsx b/packages/wonder-blocks-core/src/components/initial-fallback.tsx index f9b3b1be7b..6a8db42eb8 100644 --- a/packages/wonder-blocks-core/src/components/initial-fallback.tsx +++ b/packages/wonder-blocks-core/src/components/initial-fallback.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import {RenderState, RenderStateContext} from "./render-state-context"; +import {RenderStateInternal, RenderStateContext} from "./render-state-context"; /** * We use render functions so that we don't do any work unless we need to. @@ -88,7 +88,9 @@ export default class InitialFallback extends React.Component { // do their thing. Components don't mount during SSR, so we won't // hit this when server-side rendering. return ( - + {children()} ); @@ -100,7 +102,9 @@ export default class InitialFallback extends React.Component { // and they're not in charge of initiating the next render. if (fallback) { return ( - + {fallback()} ); @@ -110,22 +114,20 @@ export default class InitialFallback extends React.Component { return null; } - _maybeRender( - renderState: typeof RenderState[keyof typeof RenderState], - ): React.ReactNode { + _maybeRender(renderState: RenderStateInternal): React.ReactNode { const {children, fallback} = this.props; switch (renderState) { - case RenderState.Root: + case RenderStateInternal.Root: return this._renderAsRootComponent(); - case RenderState.Initial: + case RenderStateInternal.Initial: // We're not the root component, so we just have to either // render our placeholder or nothing. // The second render is going to be triggered for us. return fallback ? fallback() : null; - case RenderState.Standard: + case RenderStateInternal.Standard: // We have covered the SSR render, we're now rendering with // standard rendering semantics. return children(); @@ -156,7 +158,7 @@ export default class InitialFallback extends React.Component { // We "fallthrough" to the root case. This is more obvious // and maintainable code than just ignoring the no-fallthrough // lint rule. - return this._maybeRender(RenderState.Root); + return this._maybeRender(RenderStateInternal.Root); } } diff --git a/packages/wonder-blocks-core/src/components/render-state-context.ts b/packages/wonder-blocks-core/src/components/render-state-context.ts index 6b4c7d3e69..f7b8cae3d4 100644 --- a/packages/wonder-blocks-core/src/components/render-state-context.ts +++ b/packages/wonder-blocks-core/src/components/render-state-context.ts @@ -1,8 +1,57 @@ import * as React from "react"; +/** + * The possible states of rendering. + * + * This is used to determine if we are rendering our initial, hydrateable state + * or not. Initial renders must be consistent between the server and client so + * that hydration will succeed. + * + * We use a render state like this instead of a simple check for being mounted + * or not, or some other way of each component knowing if it is rendering itself + * for the first time so that we can avoid cascading initial renders where each + * component has to render itself and its children multiple times to reach a + * stable state. Instead, we track the initial render from the root of the tree + * and switch everything accordingly so that there are fewer additional renders. + */ export enum RenderState { + /** + * The initial render, either on the server or client. + */ + Initial = "initial", + + /** + * Any render after the initial render. Only occurs on the client. + */ + Standard = "standard", +} + +/** + * The internal states of rendering. + * + * This is different to the `RenderState` enum as this is internal to the + * Core package and solely for components that are going to provide new values + * to the render state context. + */ +export enum RenderStateInternal { + /** + * This is the root state. It indicates that nothing has actually changed + * then context value that tracks this. This is used solely by components + * that control the rendering state to know that they are in charge of + * that process. + */ Root = "root", + + /** + * This indicates something has taken charge of the rendering state and + * components should render their initial render state that is hydrateable. + */ Initial = "initial", + + /** + * This indicates that things are now rendering after the initial render + * and components can render without worrying about hydration. + */ Standard = "standard", } @@ -21,9 +70,9 @@ export enum RenderState { * standard: * means that we're all now doing non-SSR rendering */ -const RenderStateContext = React.createContext< - typeof RenderState[keyof typeof RenderState] ->(RenderState.Root); +const RenderStateContext = React.createContext( + RenderStateInternal.Root, +); RenderStateContext.displayName = "RenderStateContext"; export {RenderStateContext}; diff --git a/packages/wonder-blocks-core/src/components/render-state-root.tsx b/packages/wonder-blocks-core/src/components/render-state-root.tsx index 5376a53711..5fcb33e7b7 100644 --- a/packages/wonder-blocks-core/src/components/render-state-root.tsx +++ b/packages/wonder-blocks-core/src/components/render-state-root.tsx @@ -1,9 +1,8 @@ import * as React from "react"; -import {RenderState, RenderStateContext} from "./render-state-context"; -import {useRenderState} from "../hooks/use-render-state"; +import {RenderStateInternal, RenderStateContext} from "./render-state-context"; -const {useEffect, useState} = React; +const {useEffect, useState, useContext} = React; type Props = { children: React.ReactNode; @@ -18,12 +17,12 @@ const RenderStateRoot = ({ throwIfNested = true, }: Props): React.ReactElement => { const [firstRender, setFirstRender] = useState(true); - const renderState = useRenderState(); + const renderState = useContext(RenderStateContext); useEffect(() => { setFirstRender(false); }, []); // This effect will only run once. - if (renderState !== RenderState.Root) { + if (renderState !== RenderStateInternal.Root) { if (throwIfNested) { throw new Error( "There's already a above this instance in " + @@ -35,7 +34,9 @@ const RenderStateRoot = ({ return <>{children}; } - const value = firstRender ? RenderState.Initial : RenderState.Standard; + const value = firstRender + ? RenderStateInternal.Initial + : RenderStateInternal.Standard; return ( diff --git a/packages/wonder-blocks-core/src/hooks/__tests__/use-unique-id.test.tsx b/packages/wonder-blocks-core/src/hooks/__tests__/use-unique-id.test.tsx index b83d9ba157..0698317df2 100644 --- a/packages/wonder-blocks-core/src/hooks/__tests__/use-unique-id.test.tsx +++ b/packages/wonder-blocks-core/src/hooks/__tests__/use-unique-id.test.tsx @@ -43,7 +43,7 @@ describe("useUniqueIdWithoutMock", () => { expect(factoryValues[0]).toBe(null); }); - test("second client render retursn a unique id factory", () => { + test("second client render returns a unique id factory", () => { // Arrange const factoryValues: Array = []; const TestComponent = (): React.ReactElement | null => { @@ -89,20 +89,7 @@ describe("useUniqueIdWithoutMock", () => { expect(factoryValues[1]).toBe(factoryValues[2]); }); - it("should throw an error if it isn't a descendant of ", () => { - // Arrange - - // Act - const underTest = () => - renderHookStatic(() => useUniqueIdWithoutMock()); - - // Assert - expect(underTest).toThrowErrorMatchingInlineSnapshot( - `"Components using useUniqueIdWithoutMock() should be descendants of "`, - ); - }); - - it("Should minimize the number of renders it does", () => { + it("should minimize the number of renders it does", () => { // Arrange const values1: Array = []; const TestComponent1 = (): React.ReactElement | null => { @@ -213,19 +200,6 @@ describe("useUniqueIdWithMock", () => { expect(factoryValues[1]).toBe(factoryValues[2]); }); - it("should throw an error if it isn't a descendant of ", () => { - // Arrange - - // Act - const underTest = () => - renderHookStatic(() => useUniqueIdWithoutMock()); - - // Assert - expect(underTest).toThrowErrorMatchingInlineSnapshot( - `"Components using useUniqueIdWithoutMock() should be descendants of "`, - ); - }); - it("Should minimize the number of renders it does", () => { // Arrange const values1: Array = []; diff --git a/packages/wonder-blocks-core/src/hooks/use-render-state.ts b/packages/wonder-blocks-core/src/hooks/use-render-state.ts index b70bd0e22d..f0eb8fbcb1 100644 --- a/packages/wonder-blocks-core/src/hooks/use-render-state.ts +++ b/packages/wonder-blocks-core/src/hooks/use-render-state.ts @@ -2,9 +2,18 @@ import {useContext} from "react"; import { RenderState, + RenderStateInternal, RenderStateContext, } from "../components/render-state-context"; -export const useRenderState = - (): typeof RenderState[keyof typeof RenderState] => - useContext(RenderStateContext); +export const useRenderState = (): RenderState => { + const rawRenderState = useContext(RenderStateContext); + // For consumers, they do not care if the render state is initial or + // root. That is solely info for the RenderStateRoot component. + // To everything else, it's just the initial render or standard render. + if (rawRenderState === RenderStateInternal.Standard) { + return RenderState.Standard; + } else { + return RenderState.Initial; + } +}; diff --git a/packages/wonder-blocks-core/src/hooks/use-unique-id.ts b/packages/wonder-blocks-core/src/hooks/use-unique-id.ts index 193eddd1c7..ac07b8e3bb 100644 --- a/packages/wonder-blocks-core/src/hooks/use-unique-id.ts +++ b/packages/wonder-blocks-core/src/hooks/use-unique-id.ts @@ -20,12 +20,6 @@ export const useUniqueIdWithMock = (scope?: string): IIdentifierFactory => { const renderState = useRenderState(); const idFactory = useRef(null); - if (renderState === RenderState.Root) { - throw new Error( - "Components using useUniqueIdWithMock() should be descendants of ", - ); - } - if (renderState === RenderState.Initial) { return SsrIDFactory; } @@ -50,12 +44,6 @@ export const useUniqueIdWithoutMock = ( const renderState = useRenderState(); const idFactory = useRef(null); - if (renderState === RenderState.Root) { - throw new Error( - "Components using useUniqueIdWithoutMock() should be descendants of ", - ); - } - if (renderState === RenderState.Initial) { return null; }