| 1 | # pruneNonReactiveDependencies |
| 2 | |
| 3 | ## File |
| 4 | `src/ReactiveScopes/PruneNonReactiveDependencies.ts` |
| 5 | |
| 6 | ## Purpose |
| 7 | This pass removes dependencies from reactive scopes that are guaranteed to be **non-reactive** (i.e., their values cannot change between renders). This optimization reduces unnecessary memoization invalidations by ensuring scopes only depend on values that can actually change. |
| 8 | |
| 9 | The pass complements `PropagateScopeDependencies`, which infers dependencies without considering reactivity. This subsequent pruning step filters out dependencies that are semantically constant. |
| 10 | |
| 11 | ## Input Invariants |
| 12 | - The function has been converted to a ReactiveFunction structure |
| 13 | - `InferReactivePlaces` has annotated places with `{reactive: true}` where values can change |
| 14 | - Each `ReactiveScopeBlock` has a `scope.dependencies` set populated by `PropagateScopeDependenciesHIR` |
| 15 | - Type inference has run, so identifiers have type information for `isStableType` checks |
| 16 | |
| 17 | ## Output Guarantees |
| 18 | - **Non-reactive dependencies removed**: All dependencies in `scope.dependencies` are reactive after this pass |
| 19 | - **Scope outputs marked reactive if needed**: If a scope has any reactive dependencies remaining, all its outputs are marked reactive |
| 20 | - **Stable types remain non-reactive through property loads**: When loading properties from stable types (like `useReducer` dispatch functions), the result is not added to the reactive set |
| 21 | |
| 22 | ## Algorithm |
| 23 | |
| 24 | ### Phase 1: Collect Reactive Identifiers |
| 25 | The `collectReactiveIdentifiers` helper builds the initial set of reactive identifiers by: |
| 26 | 1. Visiting all places in the ReactiveFunction |
| 27 | 2. Adding any place marked `{reactive: true}` to the set |
| 28 | 3. For pruned scopes, adding declarations that are not primitives and not stable ref types |
| 29 | |
| 30 | ### Phase 2: Propagate Reactivity and Prune Dependencies |
| 31 | The main `Visitor` class traverses the ReactiveFunction and: |
| 32 | |
| 33 | 1. **For Instructions** - Propagates reactivity through data flow: |
| 34 | - `LoadLocal`: If source is reactive, mark the lvalue as reactive |
| 35 | - `StoreLocal`: If source value is reactive, mark both the local variable and lvalue as reactive |
| 36 | - `Destructure`: If source is reactive, mark all pattern operands as reactive (except stable types) |
| 37 | - `PropertyLoad`: If object is reactive AND result is not a stable type, mark result as reactive |
| 38 | - `ComputedLoad`: If object OR property is reactive, mark result as reactive |
| 39 | |
| 40 | 2. **For Scopes** - Prunes non-reactive dependencies and propagates outputs: |
| 41 | - Delete each dependency from `scope.dependencies` if its identifier is not in the reactive set |
| 42 | - If any dependencies remain after pruning, mark all scope outputs as reactive |
| 43 | |
| 44 | ### Key Insight: Stable Types |
| 45 | The pass leverages `isStableType` to prevent reactivity from flowing through certain React-provided stable values: |
| 46 | |
| 47 | ```typescript |
| 48 | function isStableType(id: Identifier): boolean { |
| 49 | return ( |
| 50 | isSetStateType(id) || // useState setter |
| 51 | isSetActionStateType(id) || // useActionState setter |
| 52 | isDispatcherType(id) || // useReducer dispatcher |
| 53 | isUseRefType(id) || // useRef result |
| 54 | isStartTransitionType(id) ||// useTransition startTransition |
| 55 | isSetOptimisticType(id) // useOptimistic setter |
| 56 | ); |
| 57 | } |
| 58 | ``` |
| 59 | |
| 60 | ## Edge Cases |
| 61 | |
| 62 | ### Unmemoized Values Spanning Hook Calls |
| 63 | A value created before a hook call and mutated after cannot be memoized. However, if it's non-reactive, it still should not appear as a dependency of downstream scopes. |
| 64 | |
| 65 | ### Stable Types from Reactive Containers |
| 66 | When `useReducer` returns `[state, dispatch]`, `state` is reactive but `dispatch` is stable. The pass correctly handles this. |
| 67 | |
| 68 | ### Pruned Scopes with Reactive Content |
| 69 | The `CollectReactiveIdentifiers` pass also examines pruned scopes and adds their non-primitive, non-stable-ref declarations to the reactive set. |
| 70 | |
| 71 | ### Transitive Reactivity Through Scopes |
| 72 | When a scope retains at least one reactive dependency, ALL its outputs become reactive. |
| 73 | |
| 74 | ## TODOs |
| 75 | None in the source file. |
| 76 | |
| 77 | ## Example |
| 78 | |
| 79 | ### Fixture: `unmemoized-nonreactive-dependency-is-pruned-as-dependency.js` |
| 80 | |
| 81 | **Input:** |
| 82 | ```javascript |
| 83 | function Component(props) { |
| 84 | const x = []; |
| 85 | useNoAlias(); |
| 86 | mutate(x); |
| 87 | |
| 88 | return <div>{x}</div>; |
| 89 | } |
| 90 | ``` |
| 91 | |
| 92 | **Before PruneNonReactiveDependencies:** |
| 93 | ``` |
| 94 | scope @2 dependencies=[x$15_@0:TObject<BuiltInArray>] declarations=[$23_@2] |
| 95 | ``` |
| 96 | |
| 97 | **After PruneNonReactiveDependencies:** |
| 98 | ``` |
| 99 | scope @2 dependencies=[] declarations=[$23_@2] |
| 100 | ``` |
| 101 | |
| 102 | The dependency on `x` is removed because `x` is created locally and therefore non-reactive. |
| 103 | |
| 104 | ### Fixture: `useReducer-returned-dispatcher-is-non-reactive.js` |
| 105 | |
| 106 | **Input:** |
| 107 | ```javascript |
| 108 | function f() { |
| 109 | const [state, dispatch] = useReducer(); |
| 110 | |
| 111 | const onClick = () => { |
| 112 | dispatch(); |
| 113 | }; |
| 114 | |
| 115 | return <div onClick={onClick} />; |
| 116 | } |
| 117 | ``` |
| 118 | |
| 119 | **Generated Code:** |
| 120 | ```javascript |
| 121 | function f() { |
| 122 | const $ = _c(1); |
| 123 | const [, dispatch] = useReducer(); |
| 124 | let t0; |
| 125 | if ($[0] === Symbol.for("react.memo_cache_sentinel")) { |
| 126 | const onClick = () => { |
| 127 | dispatch(); |
| 128 | }; |
| 129 | t0 = <div onClick={onClick} />; |
| 130 | $[0] = t0; |
| 131 | } else { |
| 132 | t0 = $[0]; |
| 133 | } |
| 134 | return t0; |
| 135 | } |
| 136 | ``` |
| 137 | |
| 138 | The `onClick` function only captures `dispatch`, which is a stable type. Therefore, `onClick` is non-reactive, and the JSX element can be memoized with zero dependencies. |