Skip to content

Commit 6505301

Browse files
committed
[IMPL] useDerivedState hook
1 parent 4e0b6e0 commit 6505301

10 files changed

Lines changed: 333 additions & 29 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { memo, useEffect, useState } from "react";
2+
import { useDerivedState } from "../../../../../../packages/react-tools/src";
3+
4+
const serverAPI = (user: string) => {
5+
const names = [
6+
"Keith Allison",
7+
"Ora Nelson",
8+
"Thomas Chavez",
9+
"Claudia Wheeler",
10+
"Iva Gibson",
11+
"Jose Daniel",
12+
"Mattie Greer",
13+
"Belle Schultz",
14+
"Milton Weber",
15+
"Olive Rivera",
16+
"Patrick Caldwell",
17+
"Adeline Wheeler",
18+
"Arthur Russell",
19+
"Anthony Hogan",
20+
"Rodney Munoz",
21+
"Ricky Woods",
22+
"Sean Stone",
23+
"Leona Leonard",
24+
"Brent Turner",
25+
"Cecelia Parks",
26+
]
27+
return new Promise(res => setTimeout(res, 2000))
28+
.then(() => {
29+
return names.filter(n => n.toLowerCase().includes(user.toLowerCase()));
30+
})
31+
};
32+
33+
const renders = {
34+
1: 0,
35+
2: 0,
36+
3: 0
37+
}
38+
39+
/**
40+
The component has _three internal string states_ and renders three input fields and three components that receive one state each. These three components have an object as internal state with two properties _loading_, initially set to __true__, and _friends_ which is an initially empty array.
41+
Based on the _user_ prop they receive, they set the _loading_ property of the internal state to __true__ and invoke a _serverAPI_ function that simulates a backend call and returns a list of names filtered by the passed _prop_. This list values ​​the _friends_ property of the internal state and this list together with the passed _user_ prop are rendered:
42+
- The _Without useDerivedState_ component uses the _useState_ and _useEffect_ hooks to implement this logic.
43+
- The _With useDerivedState_ component uses the _useDerivedState_ hook and the _useEffect_.
44+
- The _With useDerivedStateAndCompute_ component uses the _useDerivedState_ hook and the optional third parameter to implement all logic.
45+
46+
Each component also renders a counter of the times it is rendered.
47+
48+
The component without _useDerivedState_ hook is rendered one more time every time its _prop_ changes while the other two have the same number of renders.
49+
50+
Furthermore, if you debug the code you can see how in the first component there is no synchronization in the updating of the values ​​since in a first render the rendered _prop_ user is updated and in a second render the writing `loading` is rendered instead of the list of names.
51+
*/
52+
export const UseDerivedState = () => {
53+
const [state, setState] = useState("");
54+
const [state1, setState1] = useState("");
55+
const [state2, setState2] = useState("");
56+
57+
return <div style={{ display: "grid", gridTemplateColumns: "auto auto auto", justifyContent: "center", gap: 50, maxHeight: 350, overflow: "auto" }}>
58+
<div>
59+
<p>Without useDerivedState</p>
60+
<input type="text" placeholder="User.." value={state1} onChange={(e) => setState1(e.target.value)} />
61+
<WithoutUseDerivedState user={state1} />
62+
</div>
63+
<div>
64+
<p>With useDerivedState</p>
65+
<input type="text" placeholder="User.." value={state} onChange={(e) => setState(e.target.value)} />
66+
<WithUseDerivedState user={state} />
67+
</div>
68+
<div>
69+
<p>With useDerivedState and compute</p>
70+
<input type="text" placeholder="User.." value={state2} onChange={(e) => setState2(e.target.value)} />
71+
<WithUseDerivedStateAndCompute user={state2} />
72+
</div>
73+
</div>
74+
}
75+
76+
const WithoutUseDerivedState = memo(({user}:{user:string}) => {
77+
renders[1]++;
78+
const [state, setState] = useState<{ loading: boolean, friends: string[] }>({ loading: true, friends: [] });
79+
80+
useEffect(() => {
81+
setState({ loading: true, friends: [] });
82+
serverAPI(user).then(data => setState({ loading: false, friends: data }));
83+
}, [user])
84+
85+
return <>
86+
<h2>User: {user}</h2>
87+
<h3>Renders: {renders[1]}</h3>
88+
{
89+
state.loading
90+
? "Loading..."
91+
: <ul>
92+
{
93+
state.friends.map(f => <li key={f}>{f}</li>)
94+
}
95+
</ul>
96+
}
97+
</>
98+
})
99+
100+
const WithUseDerivedStateAndCompute = memo(({ user }: { user: string }) => {
101+
renders[2]++;
102+
const [state, setState] = useDerivedState<{ loading: boolean, friends: string[] }>(
103+
{ loading: true, friends: [] },
104+
[user],
105+
() => {
106+
state.loading && serverAPI(user).then(data => {
107+
setState({ loading: false, friends: data })
108+
});
109+
}
110+
);
111+
112+
return <>
113+
<h2>User: {user}</h2>
114+
<h3>Renders: {renders[2]}</h3>
115+
{
116+
state.loading
117+
? "Loading..."
118+
: <ul>
119+
{
120+
state.friends.map(f => <li key={f}>{f}</li>)
121+
}
122+
</ul>
123+
}
124+
</>
125+
})
126+
127+
const WithUseDerivedState = memo(({ user }: { user: string }) => {
128+
renders[3]++;
129+
const [state, setState] = useDerivedState<{loading: boolean, friends: string[]}>(
130+
{ loading: true, friends: [] },
131+
[user]
132+
);
133+
134+
useEffect(() => {
135+
serverAPI(user).then(data => {
136+
setState({ loading: false, friends: data })
137+
});
138+
}, [setState, user]);
139+
140+
return <>
141+
<h2>User: {user}</h2>
142+
<h3>Renders: {renders[3]}</h3>
143+
{
144+
state.loading
145+
? "Loading..."
146+
: <ul>
147+
{
148+
state.friends.map(f => <li key={f}>{f}</li>)
149+
}
150+
</ul>
151+
}
152+
</>
153+
})

apps/react-tools-demo/src/constants/components.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const COMPONENTS = [
1818
"useArray",
1919
"useProxyState",
2020
"useSyncExternalStore",
21+
"useDerivedState"
2122
],
2223
//LIFECYCLE
2324
[
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# useDerivedState
2+
Hook useful when the internal state of a component depends on one or more props. It receives an _initial state_ and a _dependency array_ that works the same way as that of a _useEffect_, _useMemo_, and _useCallback_. Every time the dependencies change, the __derived state__ is resetted to _initial state_. A third optional parameter can be passed, to execute a _compute_ function after the dependencies are updated, without having a _useEffect_ within the component.
3+
4+
## Usage
5+
6+
```tsx
7+
const WithoutUseDerivedState = memo(({user}:{user:string}) => {
8+
renders[1]++;
9+
const [state, setState] = useState<{ loading: boolean, friends: string[] }>({ loading: true, friends: [] });
10+
11+
useEffect(() => {
12+
setState({ loading: true, friends: [] });
13+
serverAPI(user).then(data => setState({ loading: false, friends: data }));
14+
}, [user])
15+
16+
return <>
17+
<h2>User: {user}</h2>
18+
<h3>Renders: {renders[1]}</h3>
19+
{
20+
state.loading
21+
? "Loading..."
22+
: <ul>
23+
{
24+
state.friends.map(f => <li key={f}>{f}</li>)
25+
}
26+
</ul>
27+
}
28+
</>
29+
})
30+
31+
const WithUseDerivedStateAndCompute = memo(({ user }: { user: string }) => {
32+
renders[2]++;
33+
const [state, setState] = useDerivedState<{ loading: boolean, friends: string[] }>(
34+
{ loading: true, friends: [] },
35+
[user],
36+
() => {
37+
state.loading && serverAPI(user).then(data => {
38+
setState({ loading: false, friends: data })
39+
});
40+
}
41+
);
42+
43+
return <>
44+
<h2>User: {user}</h2>
45+
<h3>Renders: {renders[2]}</h3>
46+
{
47+
state.loading
48+
? "Loading..."
49+
: <ul>
50+
{
51+
state.friends.map(f => <li key={f}>{f}</li>)
52+
}
53+
</ul>
54+
}
55+
</>
56+
})
57+
58+
const WithUseDerivedState = memo(({ user }: { user: string }) => {
59+
renders[3]++;
60+
const [state, setState] = useDerivedState<{loading: boolean, friends: string[]}>(
61+
{ loading: true, friends: [] },
62+
[user]
63+
);
64+
65+
useEffect(() => {
66+
serverAPI(user).then(data => {
67+
setState({ loading: false, friends: data })
68+
});
69+
}, [setState, user]);
70+
71+
return <>
72+
<h2>User: {user}</h2>
73+
<h3>Renders: {renders[3]}</h3>
74+
{
75+
state.loading
76+
? "Loading..."
77+
: <ul>
78+
{
79+
state.friends.map(f => <li key={f}>{f}</li>)
80+
}
81+
</ul>
82+
}
83+
</>
84+
})
85+
86+
export const UseDerivedState = () => {
87+
const [state, setState] = useState("");
88+
const [state1, setState1] = useState("");
89+
const [state2, setState2] = useState("");
90+
91+
return <div style={{ display: "grid", gridTemplateColumns: "auto auto auto", justifyContent: "center", gap: 50 }}>
92+
<div>
93+
<p>Without useDerivedState</p>
94+
<input type="text" placeholder="User.." value={state1} onChange={(e) => setState1(e.target.value)} />
95+
<WithoutUseDerivedState user={state1} />
96+
</div>
97+
<div>
98+
<p>With useDerivedState</p>
99+
<input type="text" placeholder="User.." value={state} onChange={(e) => setState(e.target.value)} />
100+
<WithUseDerivedState user={state}/>
101+
</div>
102+
<div>
103+
<p>With useDerivedState and compute</p>
104+
<input type="text" placeholder="User.." value={state2} onChange={(e) => setState2(e.target.value)} />
105+
<WithUseDerivedStateAndCompute user={state2} />
106+
</div>
107+
</div>
108+
}
109+
```
110+
111+
> The component has _three internal string states_ and renders three input fields and three components that receive one state each. These three components have an object as internal state with two properties _loading_, initially set to __true__, and _friends_ which is an initially empty array.
112+
> Based on the _user_ prop they receive, they set the _loading_ property of the internal state to __true__ and invoke a _serverAPI_ function that simulates a backend call and returns a list of names filtered by the passed _prop_. This list values ​​the _friends_ property of the internal state and this list together with the passed _user_ prop are rendered:
113+
> - The _Without useDerivedState_ component uses the _useState_ and _useEffect_ hooks to implement this logic.
114+
> - The _With useDerivedState_ component uses the _useDerivedState_ hook and the _useEffect_.
115+
> - The _With useDerivedStateAndCompute_ component uses the _useDerivedState_ hook and the optional third parameter to implement all logic.
116+
>
117+
> Each component also renders a counter of the times it is rendered.
118+
>
119+
> The component without _useDerivedState_ hook is rendered one more time every time its _prop_ changes while the other two have the same number of renders.
120+
>
121+
> Furthermore, if you debug the code you can see how in the first component there is no synchronization in the updating of the values ​​since in a first render the rendered _prop_ user is updated and in a second render the writing `loading` is rendered instead of the list of names.
122+
123+
124+
## API
125+
126+
```tsx
127+
useDerivedState<T>(initialState: T | (() => T), deps: DependencyList, compute?: EffectCallback): [T, Dispatch<SetStateAction<T>>]
128+
```
129+
130+
> ### Params
131+
>
132+
> - __initialState__: _T|()=>T_
133+
> - __deps__: _DependencyList_
134+
dependencies list from which depends derived state.
135+
> - __compute?__: _EffectCallback_
136+
function that will be executed when dependencies list change after resetting derived state to __initialState__.
137+
>
138+
139+
> ### Returns
140+
>
141+
> __result__: array with a stateful value and a function to update it.
142+
> - __Array__:
143+
> - _T_
144+
> - _Dispatch<SetStateAction<T>>_
145+
>

packages/react-tools/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
- [x] useArray
1515
- [x] useProxyState
1616
- [x] useSyncExternalStore
17-
- [ ] useDerivedState (https://hackernoon.com/whats-the-right-way-to-fetch-data-in-react-hooks-a-deep-dive-2jc13230)
17+
- [x] useDerivedState
1818
- [ ] useStateValidator (???)
1919
- [ ] useStore
2020
- [ ] createStore (example: https://github.com/streamich/react-use/blob/master/src/factory/createGlobalState.ts)
@@ -126,7 +126,7 @@
126126
- [ ] For: A referentially keyed loop with efficient updating of only changed items. The callback takes the current item as the first argument
127127
- [ ] Index: Non-keyed list iteration (rendered nodes are keyed to an array index). This is useful when there is no conceptual key, like if the data consists of primitives and it is the index that is fixed rather than the value.
128128
- [ ] RestrictedRoute (maybe)
129-
- [ ] ErrorBoundary (??)
129+
- [ ] ErrorBoundary (?? error event listener)
130130
- [ ] Suspense: Suspence compontent react-like for async component
131131
- [ ] Dynamic: This component lets you insert an arbitrary Component or tag and passes the props through to it.
132132
- [ ] ImageOpt (???)

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,5 @@ export { usePinchZoom } from './usePinchZoom';
7474
export { useLogger } from './useLogger';
7575
export { useDeviceMotion } from './useDeviceMotion';
7676
export { useDeviceOrientation } from './useDeviceOrientation';
77-
export { useVibrate } from './useVibrate';
77+
export { useVibrate } from './useVibrate';
78+
export { useDerivedState } from './useDerivedState';

packages/react-tools/src/hooks/useDeferredValue.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useDeferredValue as legacy, useEffect, useState } from "react"
1+
import { useDeferredValue as legacy, useRef, useState } from "react"
22

33
/**
44
* __`useDeferredValue`__: _useDeferredValue_ hook polyfilled for React versions below 18.
@@ -7,15 +7,12 @@ import { useDeferredValue as legacy, useEffect, useState } from "react"
77
*/
88
function useDeferredValuePolyfill<T>(value: T): T {
99
const [state, setState] = useState<T>(value);
10+
const idTimeout = useRef<number>();
1011

11-
useEffect(() => {
12-
const id = setTimeout(() => {
13-
setState(value);
14-
}, 50);
15-
return () => {
16-
clearTimeout(id);
17-
}
18-
}, [value])
12+
if (value !== state) {
13+
idTimeout.current !== null && clearTimeout(idTimeout.current);
14+
idTimeout.current = setTimeout(() => setState(value)) as unknown as number;
15+
}
1916

2017
return state;
2118
}

0 commit comments

Comments
 (0)