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"
11import * as Hydration from "effect/reactivity/Hydration"
12import * as React from "react"
13import { 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 */
22export 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 */
48export 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}