Skip to content

Commit 7207c68

Browse files
authored
fix:(useAsyncIterState): state updates do nothing when rendered in react strict mode (#76)
1 parent c0229fa commit 7207c68

File tree

4 files changed

+61
-18
lines changed

4 files changed

+61
-18
lines changed

spec/tests/useAsyncIterState.spec.tsx

+22-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
1-
import { it, describe, expect, afterEach, vi } from 'vitest';
1+
import { it, describe, expect, afterEach, vi, beforeAll, afterAll } from 'vitest';
22
import { gray } from 'colorette';
33
import { range } from 'lodash-es';
4-
import { renderHook, cleanup as cleanupMountedReactTrees, act } from '@testing-library/react';
4+
import {
5+
configure as configureReactTestingLib,
6+
renderHook,
7+
cleanup as cleanupMountedReactTrees,
8+
act,
9+
} from '@testing-library/react';
510
import { useAsyncIterState } from '../../src/index.js';
611
import { asyncIterToArray } from '../utils/asyncIterToArray.js';
712
import { asyncIterTake } from '../utils/asyncIterTake.js';
813
import { asyncIterTakeFirst } from '../utils/asyncIterTakeFirst.js';
914
import { checkPromiseState } from '../utils/checkPromiseState.js';
1015
import { pipe } from '../utils/pipe.js';
1116

17+
beforeAll(() => {
18+
configureReactTestingLib({ reactStrictMode: true });
19+
});
20+
21+
afterAll(() => {
22+
configureReactTestingLib({ reactStrictMode: false });
23+
});
24+
1225
afterEach(() => {
1326
cleanupMountedReactTrees();
1427
});
@@ -165,7 +178,7 @@ describe('`useAsyncIterState` hook', () => {
165178
'Updating states iteratively with the returned setter *in the functional form* works correctly'
166179
),
167180
async () => {
168-
const renderFn = vi.fn<(prevState: number | undefined) => number>();
181+
const valueUpdateInput = vi.fn<(prevState: number | undefined) => number>();
169182
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;
170183

171184
const rounds = 3;
@@ -175,12 +188,12 @@ describe('`useAsyncIterState` hook', () => {
175188

176189
for (let i = 0; i < rounds; ++i) {
177190
await act(() => {
178-
setValue(renderFn.mockImplementation(_prev => i));
191+
setValue(valueUpdateInput.mockImplementation(_prev => i));
179192
currentValues.push(values.value.current);
180193
});
181194
}
182195

183-
expect(renderFn.mock.calls).toStrictEqual([[undefined], [0], [1]]);
196+
expect(valueUpdateInput.mock.calls).toStrictEqual([[undefined], [0], [1]]);
184197
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
185198
expect(await yieldsPromise).toStrictEqual([0, 1, 2]);
186199
}
@@ -191,7 +204,7 @@ describe('`useAsyncIterState` hook', () => {
191204
'Updating states as rapidly as possible with the returned setter *in the functional form* works correctly'
192205
),
193206
async () => {
194-
const renderFn = vi.fn<(prevState: number | undefined) => number>();
207+
const valueUpdateInput = vi.fn<(prevState: number | undefined) => number>();
195208

196209
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;
197210

@@ -200,11 +213,12 @@ describe('`useAsyncIterState` hook', () => {
200213
const currentValues = [values.value.current];
201214

202215
for (let i = 0; i < 3; ++i) {
203-
setValue(renderFn.mockImplementation(_prev => i));
216+
setValue(valueUpdateInput.mockImplementation(_prev => i));
204217
currentValues.push(values.value.current);
218+
// await undefined;
205219
}
206220

207-
expect(renderFn.mock.calls).toStrictEqual([[undefined], [0], [1]]);
221+
expect(valueUpdateInput.mock.calls).toStrictEqual([[undefined], [0], [1]]);
208222
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
209223
expect(await yieldPromise).toStrictEqual(2);
210224
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useRef, useEffect, type EffectCallback, type DependencyList } from 'react';
2+
3+
export { useEffectStrictModeSafe };
4+
5+
function useEffectStrictModeSafe(effect: EffectCallback, deps?: DependencyList): void {
6+
const isPendingTeardownRef = useRef(false);
7+
8+
useEffect(() => {
9+
const teardown = effect();
10+
11+
if (teardown) {
12+
isPendingTeardownRef.current = false;
13+
14+
return () => {
15+
if (isPendingTeardownRef.current) {
16+
return;
17+
}
18+
19+
isPendingTeardownRef.current = true;
20+
21+
(async () => {
22+
await undefined;
23+
if (isPendingTeardownRef.current) {
24+
teardown();
25+
}
26+
})();
27+
};
28+
}
29+
}, deps);
30+
}

src/common/hooks/useRefWithInitialValue.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@ import { useRef, type MutableRefObject } from 'react';
33
export { useRefWithInitialValue };
44

55
function useRefWithInitialValue<T = undefined>(initialValueFn: () => T): MutableRefObject<T> {
6-
const isRefInitializedRef = useRef<boolean>();
7-
6+
const isInitializedRef = useRef<boolean>();
87
const ref = useRef<T>();
98

10-
if (!isRefInitializedRef.current) {
11-
isRefInitializedRef.current = true;
9+
if (!isInitializedRef.current) {
10+
isInitializedRef.current = true;
1211
ref.current = initialValueFn();
1312
}
1413

15-
const refNonNull = ref as typeof ref & { current: T };
14+
const refNonNullCurrent = ref as typeof ref & { current: T };
1615

17-
return refNonNull;
16+
return refNonNullCurrent;
1817
}

src/useAsyncIterState/index.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useEffect } from 'react';
21
import { callOrReturn } from '../common/callOrReturn.js';
32
import { useRefWithInitialValue } from '../common/hooks/useRefWithInitialValue.js';
3+
import { useEffectStrictModeSafe } from '../common/hooks/useEffectStrictModeSafe.js';
44
import { type MaybeFunction } from '../common/MaybeFunction.js';
55
import { type Iterate } from '../Iterate/index.js'; // eslint-disable-line @typescript-eslint/no-unused-vars
66
import {
@@ -118,7 +118,7 @@ function useAsyncIterState<TVal, TInitVal>(
118118
channel: AsyncIterableChannel<TVal, TInitVal>;
119119
result: AsyncIterStateResult<TVal, TInitVal>;
120120
}>(() => {
121-
const initialValueCalced = callOrReturn(initialValue) as TInitVal;
121+
const initialValueCalced = callOrReturn(initialValue)!;
122122
const channel = new AsyncIterableChannel<TVal, TInitVal>(initialValueCalced);
123123
return {
124124
channel,
@@ -128,9 +128,9 @@ function useAsyncIterState<TVal, TInitVal>(
128128

129129
const { channel, result } = ref.current;
130130

131-
useEffect(() => {
131+
useEffectStrictModeSafe(() => {
132132
return () => channel.close();
133-
}, []);
133+
});
134134

135135
return result;
136136
}

0 commit comments

Comments
 (0)