Skip to content

Commit fd8b988

Browse files
author
Brian Vaughn
committed
Updated RFC to include eager snapshot evaluation and other recent changes in implementation
1 parent 4048476 commit fd8b988

File tree

1 file changed

+113
-90
lines changed

1 file changed

+113
-90
lines changed

text/0000-use-mutable-source.md

Lines changed: 113 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,6 @@
1010

1111
This hook is designed to support a variety of mutable sources. Below are a few example cases.
1212

13-
### Redux stores
14-
15-
`useMutableSource()` can be used with Redux stores:
16-
17-
```js
18-
// May be created in module scope, like context:
19-
const reduxSource = createMutableSource(
20-
store,
21-
// Because the state is immutable, it can be used as the "version".
22-
() => reduxStore.getState()
23-
);
24-
25-
// Redux state is already immutable, so it can be returned as-is.
26-
// Like a Redux selector, this method could also return a filtered/derived value.
27-
//
28-
// Because this method doesn't require access to props,
29-
// it can be declared in module scope to be shared between components.
30-
const getSnapshot = store => store.getState();
31-
32-
// Redux subscribe method already returns an unsubscribe handler.
33-
//
34-
// Because this method doesn't require access to props,
35-
// it can be declared in module scope to be shared between components.
36-
const subscribe = (store, callback) => store.subscribe(callback);
37-
38-
function Example() {
39-
const state = useMutableSource(reduxSource, getSnapshot, subscribe);
40-
41-
// ...
42-
}
43-
```
44-
4513
### Browser APIs
4614

4715
`useMutableSource()` can also read from non traditional sources, e.g. the shared Location object, so long as they can be subscribed to and have a "version".
@@ -67,9 +35,12 @@ const getSnapshot = window => window.location.pathname;
6735
//
6836
// Because this method doesn't require access to props,
6937
// it can be declared in module scope to be shared between components.
70-
const subscribe = (window, callback) => {
71-
window.addEventListener("popstate", callback);
72-
return () => window.removeEventListener("popstate", callback);
38+
const subscribe = (window, handleChange) => {
39+
const onPopState = () => {
40+
handleChange(window.location.pathname);
41+
};
42+
window.addEventListener("popstate", onPopState);
43+
return () => window.removeEventListener("popstate", onPopState);
7344
};
7445

7546
function Example() {
@@ -89,19 +60,6 @@ const userDataSource = createMutableSource(store, {
8960
getVersion: () => data.version
9061
});
9162

92-
// This method can subscribe to root level change events,
93-
// or more snapshot-specific events.
94-
// In this case, since Example is only reading the "friends" value,
95-
// we only have to subscribe to a change in that value
96-
// (e.g. a "friends" event)
97-
//
98-
// Because this method doesn't require access to props,
99-
// it can be declared in module scope to be shared between components.
100-
const subscribe = (data, callback) => {
101-
data.addEventListener("friends", callback);
102-
return () => data.removeEventListener("friends", callback);
103-
};
104-
10563
function Example({ onlyShowFamily }) {
10664
// Because the snapshot depends on props, it has to be created inline.
10765
// useCallback() memoizes the function though,
@@ -116,11 +74,88 @@ function Example({ onlyShowFamily }) {
11674
[onlyShowFamily]
11775
);
11876

77+
// This method can subscribe to root level change events,
78+
// or more snapshot-specific events.
79+
// In this case, since Example is only reading the "friends" value,
80+
// we only have to subscribe to a change in that value
81+
// (e.g. a "friends" event)
82+
//
83+
// Because the selector depends on props,
84+
// the subscribe function needs to be defined inline as well.
85+
const subscribe = useCallback(
86+
(data, handleChange) => {
87+
const onFriends = () => handleChange(getSnapshot(data));
88+
data.addEventListener("friends", onFriends);
89+
return () => data.removeEventListener("friends", onFriends);
90+
},
91+
[getSnapshot]
92+
);
93+
11994
const friendIDs = useMutableSource(userDataSource, getSnapshot, subscribe);
12095

12196
// ...
12297
}
98+
```
12399

100+
### Redux stores
101+
102+
Redux users would likely never use the `useMutableSource` hook directly. They would use a hook provided by Redux that uses `useMutableSource` internally.
103+
104+
##### Mock Redux implementation
105+
```js
106+
// Somewhere, the Redux store needs to be wrapped in a mutable source object...
107+
const mutableSource = createMutableSource(
108+
reduxStore,
109+
// Because the state is immutable, it can be used as the "version".
110+
() => reduxStore.getState()
111+
);
112+
113+
// It would probably be shared via the Context API...
114+
const MutableSourceContext = createContext(mutableSource);
115+
116+
// Oversimplified example of how Redux could use the mutable source hook:
117+
function useSelector(selector) {
118+
const mutableSource = useContext(MutableSourceContext);
119+
120+
const getSnapshot = useCallback(
121+
store => selector(store.getState()),
122+
[selector]
123+
);
124+
125+
const subscribe = useCallback(
126+
(store, handleChange) => {
127+
return store.subscribe(() => {
128+
// The store changed, so let's get an updated snapshot.
129+
const newSnapshot = getSnapshot(store);
130+
131+
// Tell React what the snapshot value is after the most recent store update.
132+
// If it has not changed, React will not schedule any render work.
133+
handleChange(newSnapshot);
134+
});
135+
},
136+
[getSnapshot]
137+
);
138+
139+
return useMutableSource(mutableSource, getSnapshot, subscribe);
140+
}
141+
```
142+
143+
#### Example user component code
144+
145+
```js
146+
import { useSelector } from "react-redux";
147+
148+
function Example() {
149+
// The user-provided selector should be memoized with useCallback.
150+
// This will prevent unnecessary re-subscriptions each update.
151+
// This selector can also use e.g. props values if needed.
152+
const memoizedSelector = useCallback(state => state.users, []);
153+
154+
// The Redux hook will connect user code to useMutableSource.
155+
const users = useSelector(memoizedSelector);
156+
157+
// ...
158+
}
124159
```
125160

126161
### Observables
@@ -193,7 +228,10 @@ function createMutableSource<Source>(
193228
function useMutableSource<Source, Snapshot>(
194229
source: MutableSource<Source>,
195230
getSnapshot: (source: Source) => Snapshot,
196-
subscribe: (source: Source, callback: Function) => () => void
231+
subscribe: (
232+
source: Source,
233+
handleChange: (snapshot: Snapshot) => void
234+
) => () => void
197235
): Snapshot {
198236
// ...
199237
}
@@ -210,7 +248,11 @@ Mutable source requires tracking two pieces of info at the module level:
210248

211249
#### Version number
212250

213-
Tracking a source's version allows us to avoid tearing during a mount (before our component has subscribed to the source). Whenever a mounting component reads from a mutable source, this number should be checked to ensure that either (1) this is the first mounting component to read from the source during the current render or (2) the version number has not changed since the last read. A changed version number indicates a change in the underlying store data, which may result in a tear.
251+
Tracking a source's version allows us to avoid tearing when reading from a source that a component has not yet subscribed to.
252+
253+
In this case, the version should be checked to ensure that either:
254+
1. This is the first mounting component to read from the source during the current render, or
255+
2. The version number has not changed since the last read. (A changed version number indicates a change in the underlying store data, which may result in a tear.)
214256

215257
Like Context, this hook should support multiple concurrent renderers (e.g. ReactDOM and ReactART, React Native and React Fabric). To support this, we will track two work-in-progress versions (one for a "primary" renderer and one for a "secondary" renderer).
216258

@@ -225,7 +267,9 @@ This value should be reset either when a renderer starts a new batch of work or
225267
226268
#### Pending update expiration times
227269

228-
Tracking pending update times enables already mounted components to safely reuse cached snapshot values without tearing in order to support higher priority updates. During an update, if the current render’s expiration time is **** the stored expiration time for a source, it is safe to read new values from the source. Otherwise a cached snapshot value should be used temporarily<sup>1</sup>.
270+
Tracking pending updates per source enables newly-mounting components to read without potentially conflicting with components that read from the same source during a previous render.
271+
272+
During an update, if the current render’s expiration time is **** the stored expiration time for a source, it is safe to read new values from the source. Otherwise a cached snapshot value should be used temporarily<sup>1</sup>.
229273

230274
When a root is committed, all pending expiration times that are **** the committed time can be discarded for that root.
231275

@@ -253,64 +297,43 @@ Although useful for updates, pending update expiration times are not sufficient
253297

254298
The `useMutableSource()` hook’s memoizedState will need to track the following values:
255299

256-
- The user-provided config object (with getter functions).
300+
- The user-provided `getSnapshot` and `subscribe` functions.
257301
- The latest (cached) snapshot value.
258302
- The mutable source itself (in order to detect if a new source is provided).
259-
- A destroy function (to unsubscribe from a source)
303+
- The (user-returned) unsubscribe function
260304

261305
### Scenarios to handle
262306

263-
#### Initial mount (before subscription)
307+
#### Reading from a source before subscribing
264308

265-
When a component reads from a mutable source that it has not yet subscribed to<sup>1</sup>, React first checks to see if there are any pending updates for the source already scheduled on the current root.
309+
When a component reads from a mutable source that it has not yet subscribed to<sup>1</sup>, React first checks the version number to see if anything else has read from this source during the current render.
266310

267-
- ✗ If there is a pending update and the current expiration time is **>** the pending time, the read is **not safe**.
268-
- Throw and restart the render.
269-
- If there are no pending updates, or if the current expiration time is **** the pending time, has the component already subscribed to this source?
270-
- ✓ If yes, the read is **safe**.
311+
- If there is a recorded version number (i.e. this is not the first read) does it match the source's current version?
312+
- ✓ If both versions match, the read is **safe**.
271313
- Store the snapshot value on `memoizedState`.
272-
- If no, the the read **may be safe**.
314+
- ✗ If the version has changed, the read is **not safe**.
315+
- Throw and restart the render.
273316

274-
For components that have not yet subscribed to their source, React reads the version of the source and compares it to the tracked work-in-progress version numbers.
317+
If there is no version number, the the read **may be safe**. We'll need to next check pending updates for the source to determine this.
275318

276-
- ✓ If there is no recorded version, this is the first time the source has been used. The read is **safe**.
277-
- Record the current version number (on the root) for later reads during mount.
319+
- ✓ If there are no pending updates the read is **safe**.
278320
- Store the snapshot value on `memoizedState`.
279-
- ✓ If the recorded version matches the store version used previously, the read is **safe**.
321+
- Store the version number for subsequent reads during this render.
322+
- ✓ If the current expiration time is **** the pending time, the read is **safe**.
280323
- Store the snapshot value on `memoizedState`.
281-
- ✗ If the recorded version is different, the read is **not safe**.
324+
- Store the version number for subsequent reads during this render.
325+
- ✗ If the current expiration time is **>** the pending time, the read is **not safe**.
282326
- Throw and restart the render.
283327

284-
¹ This case could occur during a mount or an update (if a new mutable source was read from for the first time).
285-
286-
#### Mutation
287-
288-
React will subscribe to sources after commit so that it can schedule updates in response to mutations. When a mutation occurs<sup>1</sup>, React will calculate an expiration time for processing the change, and will:
289-
290-
- Schedule an update for that expiration time.
291-
- Update a root level entry for this source to specify the next scheduled expiration time.
292-
- This enables us to avoid tearing within the root during subsequent renders.
293-
294-
¹ Component subscriptions may only subscribe to parts of the external source they care about. Updates will only be scheduled for component’s whose subscriptions fire.
328+
<sup>1</sup> This case could occur during a mount or an update (if a new mutable source was read from for the first time).
295329

296-
#### Update (after subscription)
330+
#### Reading from a source after subscription
297331

298332
React will eventually re-render when a source is mutated, but it may also re-render for other reasons. Even in the event of a mutation, React may need to render a higher priority update before processing the mutation. In that case, it’s important that components do not read from a changed source since it may cause tearing.
299333

300-
In order to process updates safely, React will track pending root level expiration times per source.
301-
302-
- ✓ If the current render’s expiration time is **** the stored expiration time for a source, it is **safe** to read.
303-
- Store an updated snapshot value on `memoizedState`.
304-
- If the current render expiration time is **>** than the root priority for a source, consider the config object.
305-
- ✓ If the config object has not changed, we can re-use the **cached snapshot value**.<sup>1</sup>
306-
- ✗ If the config object has changed, the **cached snapshot is stale**.
307-
- Throw and restart the render.
308-
309-
¹ React will later re-render with new data, but it’s okay to use a cached value if the memoized config has not changed- because if the inputs haven’t changed, the output will not have changed.
310-
311-
#### React render new subtree
334+
In the event the a component renders again without its subscription firing (or as part of a high priority update that does not include the subscription change) it will typically be able to re-use the cached snapshot.
312335

313-
React may render a new subtree that reads from a source that was also used to render an existing part of the tree. The rules for this scenario is the same as the initial mount case described above.
336+
The one case where this will not be possible is when the `getSnapshot` function has changed. Snapshot selectors that are dependent on `props` (or other component `state`) may change even if the underlying source has not changed. In that case, the cached snapshot is not safe to reuse, and `useMutableSource` will have to throw and restart the render.
314337

315338
# Design constraints
316339

0 commit comments

Comments
 (0)