| 1 | /** |
| 2 | * React helpers for applying dehydrated Effect Atom state to a React subtree. |
| 3 | * The `HydrationBoundary` component reads the nearest `RegistryContext`, |
| 4 | * hydrates new Atom values before children render, and delays updates for |
| 5 | * existing Atom values until after commit so React transitions do not update |
| 6 | * the current UI too early. |
| 7 | * |
| 8 | * @since 4.0.0 |
| 9 | */ |
| 10 | "use client" |
| 11 | import * as Hydration from "effect/reactivity/Hydration" |
| 12 | import * as React from "react" |
| 13 | import { RegistryContext } from "./RegistryContext.ts" |
| 14 | |
| 15 | /** |
| 16 | * Props for a boundary that applies dehydrated Atom values to the nearest |
| 17 | * {@link RegistryContext} while rendering its children. |
| 18 | * |
| 19 | * @category components |
| 20 | * @since 4.0.0 |
| 21 | */ |
| 22 | export interface HydrationBoundaryProps { |
| 23 | state?: Iterable<Hydration.DehydratedAtom> |
| 24 | children?: React.ReactNode |
| 25 | } |
| 26 | |
| 27 | /** |
| 28 | * Provides a React hydration boundary that loads dehydrated Atom values into |
| 29 | * the current Atom registry. |
| 30 | * |
| 31 | * **When to use** |
| 32 | * |
| 33 | * Use to apply dehydrated Atom state to a React subtree that reads from the |
| 34 | * nearest `RegistryContext`. |
| 35 | * |
| 36 | * **Details** |
| 37 | * |
| 38 | * New Atom values are hydrated during render so descendants can read them |
| 39 | * immediately, while values for existing Atoms are deferred until after commit |
| 40 | * so transition data does not update the current UI before React accepts it. |
| 41 | * |
| 42 | * @see {@link Hydration.dehydrate} for producing dehydrated Atom state |
| 43 | * @see {@link Hydration.hydrate} for lower-level non-React hydration |
| 44 | * |
| 45 | * @category components |
| 46 | * @since 4.0.0 |
| 47 | */ |
| 48 | export const HydrationBoundary: React.FC<HydrationBoundaryProps> = ({ |
| 49 | children, |
| 50 | state |
| 51 | }) => { |
| 52 | const registry = React.useContext(RegistryContext) |
| 53 | |
| 54 | // This useMemo is for performance reasons only, everything inside it must |
| 55 | // be safe to run in every render and code here should be read as "in render". |
| 56 | // |
| 57 | // This code needs to happen during the render phase, because after initial |
| 58 | // SSR, hydration needs to happen _before_ children render. Also, if hydrating |
| 59 | // during a transition, we want to hydrate as much as is safe in render so |
| 60 | // we can prerender as much as possible. |
| 61 | // |
| 62 | // For any Atom values that already exist in the registry, we want to hold back on |
| 63 | // hydrating until _after_ the render phase. The reason for this is that during |
| 64 | // transitions, we don't want the existing Atom values and subscribers to update to |
| 65 | // the new data on the current page, only _after_ the transition is committed. |
| 66 | // If the transition is aborted, we will have hydrated any _new_ Atom values, but |
| 67 | // we throw away the fresh data for any existing ones to avoid unexpectedly |
| 68 | // updating the UI. |
| 69 | const hydrationQueue: Array<Hydration.DehydratedAtomValue> | undefined = React.useMemo(() => { |
| 70 | if (state) { |
| 71 | const dehydratedAtoms = Array.from(state) as Array<Hydration.DehydratedAtomValue> |
| 72 | const nodes = registry.getNodes() |
| 73 | |
| 74 | const newDehydratedAtoms: Array<Hydration.DehydratedAtomValue> = [] |
| 75 | const existingDehydratedAtoms: Array<Hydration.DehydratedAtomValue> = [] |
| 76 | |
| 77 | for (const dehydratedAtom of dehydratedAtoms) { |
| 78 | const existingNode = nodes.get(dehydratedAtom.key) |
| 79 | |
| 80 | if (!existingNode) { |
| 81 | // This is a new Atom value, safe to hydrate immediately |
| 82 | newDehydratedAtoms.push(dehydratedAtom) |
| 83 | } else { |
| 84 | // This Atom value already exists, queue it for later hydration |
| 85 | existingDehydratedAtoms.push(dehydratedAtom) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | if (newDehydratedAtoms.length > 0) { |
| 90 | // It's actually fine to call this with state that already exists |
| 91 | // in the registry, or is older. hydrate() is idempotent. |
| 92 | Hydration.hydrate(registry, newDehydratedAtoms) |
| 93 | } |
| 94 | |
| 95 | if (existingDehydratedAtoms.length > 0) { |
| 96 | return existingDehydratedAtoms |
| 97 | } |
| 98 | } |
| 99 | return undefined |
| 100 | }, [registry, state]) |
| 101 | |
| 102 | React.useEffect(() => { |
| 103 | if (hydrationQueue) { |
| 104 | Hydration.hydrate(registry, hydrationQueue) |
| 105 | } |
| 106 | }, [registry, hydrationQueue]) |
| 107 | |
| 108 | return React.createElement(React.Fragment, {}, children) |
| 109 | } |