Skip to content

Commit c3d5bbc

Browse files
committed
useDebugValue for tracked info, always unwrap proxy, update 3rd caveat
1 parent 2ecb0b2 commit c3d5bbc

File tree

3 files changed

+72
-7
lines changed

3 files changed

+72
-7
lines changed

docs/api/hooks.md

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,19 @@ const young = useTrackedState().person.age < 11;
396396

397397
Whereas with useTrackedState, a component re-renders whenever the `age` value is changed.
398398

399+
#### How to debug
400+
401+
Unlike useSelector, useTrackedState's behavior may seem like a magic.
402+
Disclosing the tracked information stored in useTrackedState could mitigate it.
403+
While useSelector shows the selected state with useDebugValue,
404+
useTrackedState shows the tracked state paths with useDebugValue.
405+
406+
By using React Developer Tools, you can investigate the tracked
407+
information in the hook. It is inside `AffectedDebugValue`.
408+
If you experience extra re-renders or missing re-renders,
409+
you can check the tracked state paths which may help finding bugs
410+
in your application code or possible bugs in the library code.
411+
399412
#### Caveats
400413

401414
Proxy-based tracking has limitations.
@@ -427,17 +440,31 @@ const Child = React.memo(({ foo }) => {
427440
428441
It's recommended to use primitive values for props with memo'd components.
429442
430-
- Proxied state shouldn't be used outside of render
443+
- Proxied state might behave unexpectedly outside render
444+
445+
Proxies are basically transparent, and it should behave like normal objects.
446+
However, there can be edge cases where it behaves unexpectedly.
447+
For example, if you console.log a proxied value,
448+
it will display a proxy wrapping an object.
449+
Notice, it will be kept tracking outside render,
450+
so any prorerty access will mark as used to trigger re-render on updates.
451+
452+
useTrackedState will unwrap a Proxy before wrapping with a new Proxy,
453+
hence, it will work fine in usual use cases.
454+
There's only one known pitfall: If you wrap proxied state with your own Proxy
455+
outside the control of useTrackedState,
456+
it might lead memory leaks, because useTrackedState
457+
wouldn't know how to unwrap your own Proxy.
458+
459+
To work around such edge cases, use primitive values.
431460
432461
```js
433462
const state = useTrackedState();
434463
const dispatch = useUpdate();
435-
dispatch({ type: 'FOO', value: state.foo }); // This may lead unexpected behavior if state.foo is an object
436-
dispatch({ type: 'FOO', value: state.fooStr }); // This is OK if state.fooStr is a string
464+
dispatch({ type: 'FOO', value: state.fooObj }); // Instead of using objects,
465+
dispatch({ type: 'FOO', value: state.fooStr }); // Use primitives.
437466
```
438467
439-
It's recommended to use primitive values for `dispatch`, `setState` and others.
440-
441468
#### Performance
442469
443470
useSelector is sometimes more performant because Proxies are overhead.
@@ -446,7 +473,7 @@ useTrackedState is sometimes more performant because it doesn't need to invoke a
446473
447474
### What are the limitations in browser support?
448475
449-
Proxies are not supported in old browsers like IE11, and React Native (JavaScript Core).
476+
Proxies are not supported in old browsers like IE11.
450477
451478
However, one could use [proxy-polyfill](https://github.com/GoogleChrome/proxy-polyfill) with care.
452479

src/hooks/useTrackedState.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,44 @@
11
/* eslint-env es6 */
22

3-
import { useReducer, useRef, useMemo, useContext } from 'react'
3+
import {
4+
useReducer,
5+
useRef,
6+
useMemo,
7+
useContext,
8+
useEffect,
9+
useDebugValue
10+
} from 'react'
411
import { useReduxContext as useDefaultReduxContext } from './useReduxContext'
512
import Subscription from '../utils/Subscription'
613
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
714
import { ReactReduxContext } from '../components/Context'
815
import { createDeepProxy, isDeepChanged } from '../utils/deepProxy'
916

17+
// convert "affected" (WeakMap) to serializable value (array of array of string)
18+
const affectedToPathList = (state, affected) => {
19+
const list = []
20+
const walk = (obj, path) => {
21+
const used = affected.get(obj)
22+
if (used) {
23+
used.forEach(key => {
24+
walk(obj[key], path ? [...path, key] : [key])
25+
})
26+
} else if (path) {
27+
list.push(path)
28+
}
29+
}
30+
walk(state)
31+
return list
32+
}
33+
34+
const useAffectedDebugValue = (state, affected) => {
35+
const pathList = useRef(null)
36+
useEffect(() => {
37+
pathList.current = affectedToPathList(state, affected)
38+
})
39+
useDebugValue(pathList)
40+
}
41+
1042
function useTrackedStateWithStoreAndSubscription(store, contextSub) {
1143
const [, forceRender] = useReducer(s => s + 1, 0)
1244

@@ -49,6 +81,10 @@ function useTrackedStateWithStoreAndSubscription(store, contextSub) {
4981
return () => subscription.tryUnsubscribe()
5082
}, [store, subscription])
5183

84+
if (process.env.NODE_ENV !== 'production') {
85+
useAffectedDebugValue(state, affected)
86+
}
87+
5288
const proxyCache = useRef(new WeakMap()) // per-hook proxyCache
5389
return createDeepProxy(state, affected, proxyCache.current)
5490
}

src/utils/deepProxy.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ const createProxyHandler = () => ({
6666

6767
export const createDeepProxy = (obj, affected, proxyCache) => {
6868
if (!isPlainObject(obj)) return obj
69+
const origObj = obj[GET_ORIGINAL_SYMBOL] // unwrap proxy
70+
if (origObj) obj = origObj
6971
let proxyHandler = proxyCache && proxyCache.get(obj)
7072
if (!proxyHandler) {
7173
proxyHandler = createProxyHandler()

0 commit comments

Comments
 (0)