Skip to content

Commit 392b2e9

Browse files
authored
feat(useAsyncIterState): support for setting state with a function to calculate the new state from the previous (#45)
1 parent f9d6a4b commit 392b2e9

File tree

3 files changed

+122
-53
lines changed

3 files changed

+122
-53
lines changed

spec/tests/useAsyncIterState.spec.tsx

+89-39
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { it, describe, expect, afterEach } from 'vitest';
1+
import { it, describe, expect, afterEach, vi } from 'vitest';
22
import { gray } from 'colorette';
33
import { range } from 'lodash-es';
44
import { renderHook, cleanup as cleanupMountedReactTrees, act } from '@testing-library/react';
@@ -14,43 +14,6 @@ afterEach(() => {
1414
});
1515

1616
describe('`useAsyncIterState` hook', () => {
17-
it(gray('Updating states iteratively with the returned setter works correctly'), async () => {
18-
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;
19-
20-
const rounds = 3;
21-
22-
const yieldsPromise = pipe(values, asyncIterTake(rounds), asyncIterToArray);
23-
const currentValues = [values.value.current];
24-
25-
for (let i = 0; i < rounds; ++i) {
26-
await act(() => {
27-
setValue(i);
28-
currentValues.push(values.value.current);
29-
});
30-
}
31-
32-
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
33-
expect(await yieldsPromise).toStrictEqual([0, 1, 2]);
34-
});
35-
36-
it(
37-
gray('Updating states as rapidly as possible with the returned setter works correctly'),
38-
async () => {
39-
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;
40-
41-
const yieldPromise = pipe(values, asyncIterTakeFirst());
42-
const currentValues = [values.value.current];
43-
44-
for (let i = 0; i < 3; ++i) {
45-
setValue(i);
46-
currentValues.push(values.value.current);
47-
}
48-
49-
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
50-
expect(await yieldPromise).toStrictEqual(2);
51-
}
52-
);
53-
5417
it(gray('The returned iterable can be async-iterated upon successfully'), async () => {
5518
const [values, setValue] = renderHook(() => useAsyncIterState<string>()).result.current;
5619

@@ -108,6 +71,93 @@ describe('`useAsyncIterState` hook', () => {
10871
}
10972
);
11073

74+
it(gray('Updating states iteratively with the returned setter works correctly'), async () => {
75+
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;
76+
77+
const rounds = 3;
78+
79+
const yieldsPromise = pipe(values, asyncIterTake(rounds), asyncIterToArray);
80+
const currentValues = [values.value.current];
81+
82+
for (let i = 0; i < rounds; ++i) {
83+
await act(() => {
84+
setValue(i);
85+
currentValues.push(values.value.current);
86+
});
87+
}
88+
89+
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
90+
expect(await yieldsPromise).toStrictEqual([0, 1, 2]);
91+
});
92+
93+
it(
94+
gray('Updating states as rapidly as possible with the returned setter works correctly'),
95+
async () => {
96+
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;
97+
98+
const yieldPromise = pipe(values, asyncIterTakeFirst());
99+
const currentValues = [values.value.current];
100+
101+
for (let i = 0; i < 3; ++i) {
102+
setValue(i);
103+
currentValues.push(values.value.current);
104+
}
105+
106+
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
107+
expect(await yieldPromise).toStrictEqual(2);
108+
}
109+
);
110+
111+
it(
112+
gray(
113+
'Updating states iteratively with the returned setter *in the functional form* works correctly'
114+
),
115+
async () => {
116+
const renderFn = vi.fn<(prevState: number | undefined) => number>();
117+
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;
118+
119+
const rounds = 3;
120+
121+
const yieldsPromise = pipe(values, asyncIterTake(rounds), asyncIterToArray);
122+
const currentValues = [values.value.current];
123+
124+
for (let i = 0; i < rounds; ++i) {
125+
await act(() => {
126+
setValue(renderFn.mockImplementation(_prev => i));
127+
currentValues.push(values.value.current);
128+
});
129+
}
130+
131+
expect(renderFn.mock.calls).toStrictEqual([[undefined], [0], [1]]);
132+
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
133+
expect(await yieldsPromise).toStrictEqual([0, 1, 2]);
134+
}
135+
);
136+
137+
it(
138+
gray(
139+
'Updating states as rapidly as possible with the returned setter *in the functional form* works correctly'
140+
),
141+
async () => {
142+
const renderFn = vi.fn<(prevState: number | undefined) => number>();
143+
144+
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;
145+
146+
const yieldPromise = pipe(values, asyncIterTakeFirst());
147+
148+
const currentValues = [values.value.current];
149+
150+
for (let i = 0; i < 3; ++i) {
151+
setValue(renderFn.mockImplementation(_prev => i));
152+
currentValues.push(values.value.current);
153+
}
154+
155+
expect(renderFn.mock.calls).toStrictEqual([[undefined], [0], [1]]);
156+
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
157+
expect(await yieldPromise).toStrictEqual(2);
158+
}
159+
);
160+
111161
it(
112162
gray(
113163
'When hook is unmounted, all outstanding yieldings of the returned iterable resolve to "done"'
@@ -198,7 +248,7 @@ describe('`useAsyncIterState` hook', () => {
198248
const [values] = renderHook(() => useAsyncIterState<number>()).result.current;
199249

200250
expect(() => {
201-
(values.value as any).current = "can't do this...";
251+
(values.value as any).current = `CAN'T DO THIS...`;
202252
}).toThrow(TypeError);
203253
});
204254
});

src/useAsyncIterState/IterableChannel.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@ class IterableChannel<T> {
88
#nextIteration = promiseWithResolvers<IteratorResult<T, void>>();
99
#currentValue: T | undefined;
1010

11-
put(value: T): void {
11+
put(update: T | ((prevState: T | undefined) => T)): void {
1212
if (!this.#isClosed) {
13+
const value =
14+
typeof update !== 'function'
15+
? update
16+
: (() => {
17+
const updateFnTypePatched = update as (prevState: T | undefined) => T;
18+
return updateFnTypePatched(this.#currentValue);
19+
})();
20+
1321
(async () => {
1422
this.#currentValue = value;
1523
await undefined; // Deferring to the next microtick so that an attempt to pull the a value before making multiple rapid synchronous calls to `put()` will make that pull ultimately yield only the last value that was put - instead of the first one as were if this otherwise wasn't deferred.

src/useAsyncIterState/index.ts

+24-13
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ export { useAsyncIterState, type AsyncIterStateResult, type AsyncIterableSubject
88
* Basically like {@link https://react.dev/reference/react/useState `React.useState`}, only that the value
99
* is provided back __wrapped as an async iterable__.
1010
*
11-
* This hook allows a component to declare and manage a piece of state while easily letting it control
12-
* what area(s) specifically within the UI should be bound to it (should re-render in reaction to changes
13-
* in it) - combined for example with one or more {@link Iterate `<Iterate>`}s.
11+
* This hook allows a component to declare and manage a piece of state while easily letting you control
12+
* what specifically area(s) within the UI should be bound to it (should re-render in reaction to changes
13+
* in it) - for example, if combined with one or more {@link Iterate `<Iterate>`}s.
1414
*
1515
* @example
1616
* ```tsx
@@ -36,14 +36,24 @@ export { useAsyncIterState, type AsyncIterStateResult, type AsyncIterableSubject
3636
*
3737
* ---
3838
*
39-
* This is unlike vanila `React.useState` which simply re-renders the entire component. Instead,
40-
* `useAsyncIterState` helps confine UI updates as well as facilitate layers of sub-components that pass
41-
* actual async iterables across one another as props, skipping typical cascading re-renderings down to
42-
* __only the inner-most leafs__ of the UI tree.
4339
*
44-
* The returned async iterable contains a `.current.value` property which shows the current up to date
45-
* state value at all times. Use this any case you just need to read the immediate current state rather
46-
* than directly rendering it, since for rendering you may simply async-iterate it.
40+
*
41+
* The returned async iterable can be passed over to any level down the component tree and rendered
42+
* using `<Iterate>`, `useAsyncIter`, and so on. It also contains a `.current.value` property which shows
43+
* the current up to date state value at all times. Use this any case you just need to read the immediate
44+
* current state rather than directly rendering it, since for rendering you may simply async-iterate it.
45+
*
46+
* Returned also alongside the async iterable is a function for updating the state. Calling it with a new
47+
* value will cause the paired iterable to yield the updated state value as well as immediately set the
48+
* iterable's `.current.value` property to that new state. Just like
49+
* [`React.useState`'s setter](https://react.dev/reference/react/useState#setstate), you can pass it
50+
* the next state directly, or a function that calculates it from the previous state.
51+
*
52+
* Unlike vanila `React.useState`, which simply re-renders the entire component - `useAsyncIterState`
53+
* helps confine UI updates by handing you an iterable which choose how and where in the component tree
54+
* to render it. This work method can facilitate layers of sub-components that pass actual async iterables
55+
* across one another as props, skipping typical cascading re-renderings down to __only the inner-most
56+
* leafs__ of the UI tree.
4757
*
4858
* @example
4959
* ```tsx
@@ -107,7 +117,8 @@ function useAsyncIterState<TVal>(): AsyncIterStateResult<TVal> {
107117
}
108118

109119
/**
110-
* A pair of stateful async iterable and a function which modifies the state and yields the updated value.
120+
* A pair of stateful async iterable and a function which updates the state and making the paired
121+
* async iterable yield the new value.
111122
* Returned from the {@link useAsyncIterState `useAsyncIterState`} hook.
112123
*
113124
* @see {@link useAsyncIterState `useAsyncIterState`}
@@ -125,8 +136,8 @@ type AsyncIterStateResult<TVal> = [
125136
values: AsyncIterableSubject<TVal>,
126137

127138
/**
128-
* A function which modifies the state, causing the paired async iterable to yield the updated state
139+
* A function which updates the state, causing the paired async iterable to yield the updated state
129140
* value and immediately sets its `.current.value` property to the latest state.
130141
*/
131-
setValue: (newValue: TVal) => void,
142+
setValue: (update: TVal | ((prevState: TVal | undefined) => TVal)) => void,
132143
];

0 commit comments

Comments
 (0)