-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
useTrackedState from reactive-react-redux #1503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 4 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
919c94d
convert useTrackedState from reactive-react-redux
dai-shi 17c266e
createTrackedStateHook for custom context
dai-shi 390921b
add spec for useTrackedState
dai-shi 6377cd6
add docs
dai-shi 63b52cc
Update docs/api/proxy-based-tracking.md
dai-shi 9fcdbd3
apply suggestions in docs
dai-shi 2ecb0b2
move useTrackedState docs into hooks.md
dai-shi c3d5bbc
useDebugValue for tracked info, always unwrap proxy, update 3rd caveat
dai-shi c01ddc0
minor fix in useAffectedDebugValue
dai-shi 7d20db4
Merge branch 'master' into use-tracked-state
dai-shi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
--- | ||
id: proxy-based-tracking | ||
title: Proxy-based Tracking | ||
sidebar_label: Proxy-based Tracking | ||
hide_title: true | ||
--- | ||
|
||
# Proxy-based Tracking | ||
|
||
This document describes about `useTrackedState` hook. | ||
timdorr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## How does this get used? | ||
|
||
`useTrackedState` is a hook that can be used instead of `useSelector`. | ||
timdorr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
It doesn't mean to replace `useSelector` completely. | ||
It gives a new way of connecting Redux store to React. | ||
|
||
> **Note**: It's not completely new in the sense that there already exists a library for `connect`: [beautiful-react-redux](https://github.com/theKashey/beautiful-react-redux) | ||
timdorr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
The usage of `useTrackedState` is extremely simple. | ||
timdorr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```jsx | ||
import React from 'react' | ||
import { useTrackedState } from 'react-redux' | ||
|
||
export const CounterComponent = () => { | ||
const { counter } = useTrackedState() | ||
return <div>{counter}</div> | ||
} | ||
``` | ||
|
||
Using props is intuitive. | ||
|
||
```jsx | ||
import React from 'react' | ||
import { useTrackedState } from 'react-redux' | ||
|
||
export const TodoListItem = props => { | ||
const state = useTrackedState() | ||
const todo = state.todos[props.id] | ||
return <div>{todo.text}</div> | ||
} | ||
``` | ||
|
||
## Why would you want to use it? | ||
|
||
> For beginners: Far easier to understand Redux and R-R without the notion of selectors | ||
timdorr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
> | ||
> For intermediates: Never needs to worry about memoized selectors | ||
> | ||
> For experts: No stale props issue | ||
|
||
## What are the differences in behavior compared to useSelector? | ||
|
||
### Capabilities | ||
|
||
A selector can create a derived values. For example: | ||
|
||
```js | ||
const isYoung = state => state.person.age < 11; | ||
``` | ||
|
||
This selector computes a boolean value. | ||
|
||
```js | ||
const young = useSelector(isYoung); | ||
``` | ||
|
||
With useSelector, a component only re-renders when the result of `isYoung` is changed. | ||
|
||
```js | ||
const young = useTrackedState().person.age < 11; | ||
``` | ||
|
||
Whereas with useTrackedState, a component re-renders whenever the `age` value is changed. | ||
|
||
### Caveats | ||
|
||
Proxy-based tracking may not work 100% as expected. | ||
timdorr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
> - Proxied states are referentially equal only in per-hook basis | ||
> | ||
> ```js | ||
> const state1 = useTrackedState(); | ||
> const state2 = useTrackedState(); | ||
> // state1 and state2 is not referentially equal | ||
> // even if the underlying redux state is referentially equal. | ||
> ``` | ||
> | ||
> You should use `useTrackedState` only once in a component. | ||
> | ||
> - An object referential change doesn't trigger re-render if an property of the object is accessed in previous render | ||
> | ||
> ```js | ||
> const state = useTrackedState(); | ||
> const { foo } = state; | ||
> return <Child key={foo.id} foo={foo} />; | ||
> | ||
> const Child = React.memo(({ foo }) => { | ||
> // ... | ||
> }; | ||
> // if foo doesn't change, Child won't render, so foo.id is only marked as used. | ||
> // it won't trigger Child to re-render even if foo is changed. | ||
> ``` | ||
> | ||
> It's recommended to use primitive values for props with memo'd components. | ||
> | ||
> - Proxied state shouldn't be used outside of render | ||
> | ||
> ```js | ||
> const state = useTrackedState(); | ||
> const dispatch = useUpdate(); | ||
> dispatch({ type: 'FOO', value: state.foo }); // This may lead unexpected behavior if state.foo is an object | ||
> dispatch({ type: 'FOO', value: state.fooStr }); // This is OK if state.fooStr is a string | ||
> ``` | ||
> | ||
> It's recommended to use primitive values for `dispatch`, `setState` and others. | ||
|
||
### Performance | ||
|
||
useSelector is sometimes more performant because Proxies are overhead. | ||
|
||
useTrackedState is sometimes more performant because it doesn't need to invoke a selector when checking for updates. | ||
|
||
## What are the limitations in browser support? | ||
|
||
Proxies are not supported in old browsers like IE11, and React Native (JavaScript Core). | ||
|
||
However, one could use [proxy-polyfill](https://github.com/GoogleChrome/proxy-polyfill) with care. | ||
|
||
There are some limitations with the polyfill. Most notably, it will fail to track undefined properties. | ||
|
||
```js | ||
const state = { count: 0 } | ||
|
||
// this works with polyfill. | ||
state.count | ||
|
||
// this won't work with polyfill. | ||
state.foo | ||
``` | ||
|
||
So, if the state shape is defined initiall and never changed, it should be fine. | ||
dai-shi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
`Object.key()` and `in` operater is not supported. There might be other cases that polyfill doesn't support. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* eslint-env es6 */ | ||
|
||
import { useReducer, useRef, useMemo, useContext } from 'react' | ||
import { useReduxContext as useDefaultReduxContext } from './useReduxContext' | ||
import Subscription from '../utils/Subscription' | ||
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' | ||
import { ReactReduxContext } from '../components/Context' | ||
import { createDeepProxy, isDeepChanged } from '../utils/deepProxy' | ||
|
||
function useTrackedStateWithStoreAndSubscription(store, contextSub) { | ||
const [, forceRender] = useReducer(s => s + 1, 0) | ||
|
||
const subscription = useMemo(() => new Subscription(store, contextSub), [ | ||
store, | ||
contextSub | ||
]) | ||
|
||
const state = store.getState() | ||
const affected = new WeakMap() | ||
const latestTracked = useRef(null) | ||
useIsomorphicLayoutEffect(() => { | ||
latestTracked.current = { | ||
state, | ||
affected, | ||
cache: new WeakMap() | ||
} | ||
}) | ||
useIsomorphicLayoutEffect(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While it's not an immediate concern, I'm noticing that we now have 3 different parts of our codebase where we're subscribing to the store in a hook. That may be worth looking at to see if there's some way to abstract it. |
||
function checkForUpdates() { | ||
const nextState = store.getState() | ||
if ( | ||
latestTracked.current.state !== nextState && | ||
isDeepChanged( | ||
latestTracked.current.state, | ||
nextState, | ||
latestTracked.current.affected, | ||
latestTracked.current.cache | ||
) | ||
) { | ||
forceRender() | ||
} | ||
} | ||
|
||
subscription.onStateChange = checkForUpdates | ||
subscription.trySubscribe() | ||
|
||
checkForUpdates() | ||
|
||
return () => subscription.tryUnsubscribe() | ||
}, [store, subscription]) | ||
|
||
const proxyCache = useRef(new WeakMap()) // per-hook proxyCache | ||
return createDeepProxy(state, affected, proxyCache.current) | ||
} | ||
|
||
/** | ||
* Hook factory, which creates a `useTrackedState` hook bound to a given context. | ||
* | ||
* @param {React.Context} [context=ReactReduxContext] Context passed to your `<Provider>`. | ||
* @returns {Function} A `useTrackedState` hook bound to the specified context. | ||
*/ | ||
export function createTrackedStateHook(context = ReactReduxContext) { | ||
const useReduxContext = | ||
context === ReactReduxContext | ||
? useDefaultReduxContext | ||
: () => useContext(context) | ||
return function useTrackedState() { | ||
const { store, subscription: contextSub } = useReduxContext() | ||
|
||
return useTrackedStateWithStoreAndSubscription(store, contextSub) | ||
} | ||
} | ||
|
||
/** | ||
* A hook to return the redux store's state. | ||
* | ||
* This hook tracks the state usage and only triggers | ||
* re-rerenders if the used part of the state is changed. | ||
* | ||
* @returns {any} the whole state | ||
* | ||
* @example | ||
* | ||
* import React from 'react' | ||
* import { useTrackedState } from 'react-redux' | ||
* | ||
* export const CounterComponent = () => { | ||
* const state = useTrackedState() | ||
* return <div>{state.counter}</div> | ||
* } | ||
*/ | ||
export const useTrackedState = /*#__PURE__*/ createTrackedStateHook() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.