Skip to content

Commit c7a1d76

Browse files
committed
docs: document that useSignalSelector rejects state mutations
1 parent 9894b9b commit c7a1d76

4 files changed

Lines changed: 19 additions & 18 deletions

File tree

docs/api/SignalProvider.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ All of the standard React Redux APIs - [`useSelector`](./useSelector.md), [`useD
4141

4242
`SignalProvider` accepts the same props as [`<Provider>`](./Provider.md):
4343

44-
| Name | Description |
45-
| :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- |
44+
| Name | Description |
45+
| :----------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
4646
| `store` | The single Redux store in your application. |
4747
| `children` | The top-level React elements in your component tree. |
4848
| `context?` | An optional custom context instance, as with `<Provider>`. |

docs/api/unwrap.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ Resolves a [`useSignalSelector`](./useSignalSelector.md) tracking proxy to the r
2222

2323
### Parameters
2424

25-
| Name | Description |
26-
| :------ | :--------------------------------------- |
25+
| Name | Description |
26+
| :------ | :------------------------------------------------ |
2727
| `value` | The value to unwrap - a proxy or any other value. |
2828

2929
### Returns
@@ -32,7 +32,7 @@ The raw state object if `value` is a tracking proxy, otherwise `value` itself.
3232

3333
## Usage Guide
3434

35-
Inside a `useSignalSelector` selector, nested objects read from `state` are tracking proxies. A proxy is never `===` to the raw object it wraps, so identity comparisons against object references captured *outside* the current selector run always fail. `unwrap` resolves the proxy so the comparison works:
35+
Inside a `useSignalSelector` selector, nested objects read from `state` are tracking proxies. A proxy is never `===` to the raw object it wraps, so identity comparisons against object references captured _outside_ the current selector run always fail. `unwrap` resolves the proxy so the comparison works:
3636

3737
```ts
3838
import { unwrap, useSignalSelector } from 'react-redux'

docs/api/useSelector.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const result: Selected = useSelector(
6767

6868
| Name | Description |
6969
| :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
70-
| `selector` | A function that receives the entire Redux store state as its only argument, and returns the value this component needs from it. |
70+
| `selector` | A function that receives the entire Redux store state as its only argument, and returns the value this component needs from it. |
7171
| `options?` | Either an equality function used to compare selector results (such as `shallowEqual`), or an options object containing an `equalityFn` field and a [`devModeChecks`](#development-mode-checks) field for configuring the development-mode checks below. |
7272

7373
### Returns

docs/api/useSignalSelector.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ const result: Selected = useSignalSelector(
6868

6969
Both parameters behave exactly as they do in [`useSelector`](./useSelector.md#parameters):
7070

71-
| Name | Description |
72-
| :--------- | :---------------------------------------------------------------------------------------------------------------------------------------------- |
71+
| Name | Description |
72+
| :--------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
7373
| `selector` | A function that receives the entire Redux store state and returns the value this component needs. Must be [pure](./useSelector.md). |
7474
| `options?` | Either an equality function (such as `shallowEqual`), or an options object with `equalityFn` and `devModeChecks` fields, as in `useSelector`. |
7575

@@ -98,7 +98,7 @@ This is the same family of technique used by `proxy-memoize` and by fine-grained
9898
- Your app dispatches frequently, and most dispatches are irrelevant to most components
9999
- Selectors read narrow, deep paths (`state.entities.todos[id].completed`)
100100

101-
It helps least (and can cost slightly more than `useSelector`) when most dispatches change state that most components read anyway, or when selectors read broad swaths of the state. Tracked selector evaluation itself costs roughly 3-4x an untracked run, so the win comes from the runs that are *skipped*, not from the runs themselves.
101+
It helps least (and can cost slightly more than `useSelector`) when most dispatches change state that most components read anyway, or when selectors read broad swaths of the state. Tracked selector evaluation itself costs roughly 3-4x an untracked run, so the win comes from the runs that are _skipped_, not from the runs themselves.
102102

103103
Because `useSelector` and `useSignalSelector` coexist under one `<SignalProvider>`, you can adopt it incrementally in the components with the highest subscription counts.
104104

@@ -108,20 +108,21 @@ The observable behavior - what your components render, when they re-render, what
108108

109109
#### Hard constraints
110110

111-
| Constraint | Explanation |
112-
| :--- | :--- |
113-
| **Requires `<SignalProvider>`** | Calling `useSignalSelector` under a plain `<Provider>` throws. |
114-
| **The state root must be a plain object** | Path tracking operates on the root object's keys. If the root is not a plain object, the hook still works, but falls back to untracked evaluation (behaving like `useSelector`). |
115-
| **`state => state` never updates** | A selector that returns the entire root state records no field reads, so no dispatch will ever re-run it. This is already an anti-pattern (`useSelector`'s [identity function check](./useSelector.md#identity-function-state--state-check) warns about it), but with `useSignalSelector` it goes from "wasteful" to "broken": the component will not update. The dev-mode check catches this. |
116-
| **Mutations inside `Map`, `Set`, `Date`, and class instances are not tracked** | Only plain objects and arrays get per-field tracking. Other object types are tracked by reference: replacing the instance triggers an update, but reading `myMap.get('key')` does not record a per-entry dependency. Since Redux state should be [immutably updated](https://redux.js.org/style-guide/#do-not-mutate-state) plain data anyway, this mostly matters for state produced by libraries. |
111+
| Constraint | Explanation |
112+
| :----------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
113+
| **Requires `<SignalProvider>`** | Calling `useSignalSelector` under a plain `<Provider>` throws. |
114+
| **The state root must be a plain object** | Path tracking operates on the root object's keys. If the root is not a plain object, the hook still works, but falls back to untracked evaluation (behaving like `useSelector`). |
115+
| **`state => state` never updates** | A selector that returns the entire root state records no field reads, so no dispatch will ever re-run it. This is already an anti-pattern (`useSelector`'s [identity function check](./useSelector.md#identity-function-state--state-check) warns about it), but with `useSignalSelector` it goes from "wasteful" to "broken": the component will not update. The dev-mode check catches this. |
116+
| **Selectors must not mutate state** | Writing to the state from inside a selector (`state.items.sort()`, `state.foo = 1`, `delete state.bar`) throws a `TypeError` in development explaining which property the selector tried to change. `useSelector` silently allows these writes, which corrupt the store; `useSignalSelector` rejects them because the proxy sees every write. Use non-mutating alternatives (`state.items.slice().sort()`, `toSorted()`). |
117+
| **Mutations inside `Map`, `Set`, `Date`, and class instances are not tracked** | Only plain objects and arrays get per-field tracking. Other object types are tracked by reference: replacing the instance triggers an update, but reading `myMap.get('key')` does not record a per-entry dependency. Since Redux state should be [immutably updated](https://redux.js.org/style-guide/#do-not-mutate-state) plain data anyway, this mostly matters for state produced by libraries. |
117118

118119
#### Gotchas
119120

120121
These are behaviors that differ from `useSelector` in ways you might notice, but that have straightforward workarounds - or need no workaround at all.
121122

122123
##### Proxies and identity
123124

124-
Inside the selector, nested objects you read from `state` are tracking proxies, not the raw state objects. The values are identical; the object identity is not. This matters only for `===` comparisons **inside the selector body** against object references obtained *outside* the current selector run:
125+
Inside the selector, nested objects you read from `state` are tracking proxies, not the raw state objects. The values are identical; the object identity is not. This matters only for `===` comparisons **inside the selector body** against object references obtained _outside_ the current selector run:
125126

126127
```ts
127128
import { unwrap, useSignalSelector } from 'react-redux'
@@ -140,7 +141,7 @@ const selected = useSignalSelector((state) => {
140141

141142
See [`unwrap`](./unwrap.md) for details. Comparisons between two values both read from `state` in the same selector run work correctly without unwrapping.
142143

143-
**Proxies never escape the selector.** The hook unwraps the selector's return value before handing it to React, so your components, effects, equality functions, and dispatched actions always see plain Redux state. `console.log` of a selector result shows plain data. For the same reason, avoid *storing* a state object read during one selector run in an external variable for use later - the same rule as holding onto an Immer draft. Use `unwrap()` first if you need to do this.
144+
**Proxies never escape the selector.** The hook unwraps the selector's return value before handing it to React, so your components, effects, equality functions, and dispatched actions always see plain Redux state. `console.log` of a selector result shows plain data. For the same reason, avoid _storing_ a state object read during one selector run in an external variable for use later - the same rule as holding onto an Immer draft. Use `unwrap()` first if you need to do this.
144145

145146
##### Memoized (Reselect) selectors
146147

@@ -152,7 +153,7 @@ Selectors that enumerate the root state's keys (`Object.keys(state)`, spreading
152153

153154
##### Aliased state objects
154155

155-
If the same object instance is reachable via two different state paths (for example, an entity stored in two lookup tables), tracking attributes reads to the first path encountered. The consequence is only ever *extra* selector re-runs, never missed updates.
156+
If the same object instance is reachable via two different state paths (for example, an entity stored in two lookup tables), tracking attributes reads to the first path encountered. The consequence is only ever _extra_ selector re-runs, never missed updates.
156157

157158
##### Stale props and "zombie children"
158159

0 commit comments

Comments
 (0)