Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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