Skip to content

Commit

Permalink
fix useAsyncIterState when rendered in react strict mode
Browse files Browse the repository at this point in the history
  • Loading branch information
shtaif committed Jan 30, 2025
1 parent c0229fa commit 6d41467
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 18 deletions.
30 changes: 22 additions & 8 deletions spec/tests/useAsyncIterState.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import { it, describe, expect, afterEach, vi } from 'vitest';
import { it, describe, expect, afterEach, vi, beforeAll, afterAll } from 'vitest';
import { gray } from 'colorette';
import { range } from 'lodash-es';
import { renderHook, cleanup as cleanupMountedReactTrees, act } from '@testing-library/react';
import {
configure as configureReactTestingLib,
renderHook,
cleanup as cleanupMountedReactTrees,
act,
} from '@testing-library/react';
import { useAsyncIterState } from '../../src/index.js';
import { asyncIterToArray } from '../utils/asyncIterToArray.js';
import { asyncIterTake } from '../utils/asyncIterTake.js';
import { asyncIterTakeFirst } from '../utils/asyncIterTakeFirst.js';
import { checkPromiseState } from '../utils/checkPromiseState.js';
import { pipe } from '../utils/pipe.js';

beforeAll(() => {
configureReactTestingLib({ reactStrictMode: true });
});

afterAll(() => {
configureReactTestingLib({ reactStrictMode: false });
});

afterEach(() => {
cleanupMountedReactTrees();
});
Expand Down Expand Up @@ -165,7 +178,7 @@ describe('`useAsyncIterState` hook', () => {
'Updating states iteratively with the returned setter *in the functional form* works correctly'
),
async () => {
const renderFn = vi.fn<(prevState: number | undefined) => number>();
const valueUpdateInput = vi.fn<(prevState: number | undefined) => number>();
const [values, setValue] = renderHook(() => useAsyncIterState<number>()).result.current;

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

for (let i = 0; i < rounds; ++i) {
await act(() => {
setValue(renderFn.mockImplementation(_prev => i));
setValue(valueUpdateInput.mockImplementation(_prev => i));
currentValues.push(values.value.current);
});
}

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

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

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

for (let i = 0; i < 3; ++i) {
setValue(renderFn.mockImplementation(_prev => i));
setValue(valueUpdateInput.mockImplementation(_prev => i));
currentValues.push(values.value.current);
// await undefined;
}

expect(renderFn.mock.calls).toStrictEqual([[undefined], [0], [1]]);
expect(valueUpdateInput.mock.calls).toStrictEqual([[undefined], [0], [1]]);
expect(currentValues).toStrictEqual([undefined, 0, 1, 2]);
expect(await yieldPromise).toStrictEqual(2);
}
Expand Down
30 changes: 30 additions & 0 deletions src/common/hooks/useEffectStrictModeSafe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useRef, useEffect, type EffectCallback, type DependencyList } from 'react';

export { useEffectStrictModeSafe };

function useEffectStrictModeSafe(effect: EffectCallback, deps?: DependencyList): void {
const isPendingTeardownRef = useRef(false);

useEffect(() => {
const teardown = effect();

if (teardown) {
isPendingTeardownRef.current = false;

return () => {
if (isPendingTeardownRef.current) {
return;
}

isPendingTeardownRef.current = true;

(async () => {
await undefined;
if (isPendingTeardownRef.current) {
teardown();
}
})();
};
}
}, deps);
}
11 changes: 5 additions & 6 deletions src/common/hooks/useRefWithInitialValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ import { useRef, type MutableRefObject } from 'react';
export { useRefWithInitialValue };

function useRefWithInitialValue<T = undefined>(initialValueFn: () => T): MutableRefObject<T> {
const isRefInitializedRef = useRef<boolean>();

const isInitializedRef = useRef<boolean>();
const ref = useRef<T>();

if (!isRefInitializedRef.current) {
isRefInitializedRef.current = true;
if (!isInitializedRef.current) {
isInitializedRef.current = true;
ref.current = initialValueFn();
}

const refNonNull = ref as typeof ref & { current: T };
const refNonNullCurrent = ref as typeof ref & { current: T };

return refNonNull;
return refNonNullCurrent;
}
8 changes: 4 additions & 4 deletions src/useAsyncIterState/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { callOrReturn } from '../common/callOrReturn.js';
import { useRefWithInitialValue } from '../common/hooks/useRefWithInitialValue.js';
import { useEffectStrictModeSafe } from '../common/hooks/useEffectStrictModeSafe.js';
import { type MaybeFunction } from '../common/MaybeFunction.js';
import { type Iterate } from '../Iterate/index.js'; // eslint-disable-line @typescript-eslint/no-unused-vars
import {
Expand Down Expand Up @@ -118,7 +118,7 @@ function useAsyncIterState<TVal, TInitVal>(
channel: AsyncIterableChannel<TVal, TInitVal>;
result: AsyncIterStateResult<TVal, TInitVal>;
}>(() => {
const initialValueCalced = callOrReturn(initialValue) as TInitVal;
const initialValueCalced = callOrReturn(initialValue)!;
const channel = new AsyncIterableChannel<TVal, TInitVal>(initialValueCalced);
return {
channel,
Expand All @@ -128,9 +128,9 @@ function useAsyncIterState<TVal, TInitVal>(

const { channel, result } = ref.current;

useEffect(() => {
useEffectStrictModeSafe(() => {
return () => channel.close();
}, []);
});

return result;
}
Expand Down

0 comments on commit 6d41467

Please sign in to comment.