Skip to content

Commit 97280b0

Browse files
fix: fix render hook typing (#1811)
1 parent e5fa23c commit 97280b0

File tree

8 files changed

+76
-33
lines changed

8 files changed

+76
-33
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Setup deps (React 18)
2+
description: Setup Node.js and install dependencies
3+
4+
runs:
5+
using: composite
6+
steps:
7+
- name: Setup Node.js
8+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
9+
with:
10+
node-version-file: .nvmrc
11+
12+
- name: Cache deps
13+
id: yarn-cache
14+
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
15+
with:
16+
path: |
17+
./node_modules
18+
.yarn/install-state.gz
19+
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }}
20+
restore-keys: |
21+
${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
22+
${{ runner.os }}-yarn-
23+
24+
- name: Install deps
25+
if: steps.yarn-cache.outputs.cache-hit != 'true'
26+
run: yarn install --immutable
27+
shell: bash
28+
29+
- name: Switch to React 18
30+
run: |
31+
32+
shell: bash

.github/workflows/ci.yml

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ jobs:
4040
- name: Typecheck
4141
run: yarn typecheck
4242

43+
typecheck-react-18:
44+
runs-on: ubuntu-latest
45+
name: Typecheck React 18
46+
steps:
47+
- name: Checkout
48+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
49+
50+
- name: Setup Node.js and deps
51+
uses: ./.github/actions/setup-deps-react-18
52+
53+
- name: Typecheck
54+
run: yarn typecheck
55+
4356
test:
4457
runs-on: ubuntu-latest
4558
name: Test
@@ -66,12 +79,7 @@ jobs:
6679
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
6780

6881
- name: Setup Node.js and deps
69-
uses: ./.github/actions/setup-deps
70-
71-
- name: Switch to React 18
72-
run: |
73-
yarn remove react react-test-renderer react-native @react-native/babel-preset
74-
82+
uses: ./.github/actions/setup-deps-react-18
7583

7684
- name: Test
7785
run: yarn test:ci

src/__tests__/render-hook-async.test.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ afterEach(() => {
1313
console.error = originalConsoleError;
1414
});
1515

16+
function useSuspendingHook(promise: Promise<string>) {
17+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
18+
// @ts-ignore: React 18 does not have `use` hook
19+
return React.use(promise);
20+
}
21+
1622
test('renderHookAsync renders hook asynchronously', async () => {
1723
const { result } = await renderHookAsync(() => {
1824
const [state, setState] = React.useState(1);
@@ -124,10 +130,6 @@ test('handles multiple state updates in effects', async () => {
124130
});
125131

126132
testGateReact19('handles hook with suspense', async () => {
127-
function useSuspendingHook(promise: Promise<string>) {
128-
return React.use(promise);
129-
}
130-
131133
let resolvePromise: (value: string) => void;
132134
const promise = new Promise<string>((resolve) => {
133135
resolvePromise = resolve;
@@ -169,10 +171,6 @@ testGateReact19('handles hook suspense with error boundary', async () => {
169171
// eslint-disable-next-line no-console
170172
console.error = excludeConsoleMessage(console.error, ERROR_MESSAGE);
171173

172-
function useSuspendingHook(promise: Promise<string>) {
173-
return React.use(promise);
174-
}
175-
176174
let rejectPromise: (error: Error) => void;
177175
const promise = new Promise<string>((_resolve, reject) => {
178176
rejectPromise = reject;

src/__tests__/render-hook.test.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,32 +60,29 @@ test('allows wrapper components', () => {
6060
expect(result.current).toEqual('provided');
6161
});
6262

63-
const useMyHook = (param: number | undefined) => {
64-
return param;
65-
};
63+
function useMyHook<T>(param: T) {
64+
return { param };
65+
}
6666

6767
test('props type is inferred correctly when initial props is defined', () => {
68-
const { result, rerender } = renderHook((num: number | undefined) => useMyHook(num), {
68+
const { result, rerender } = renderHook((num: number) => useMyHook(num), {
6969
initialProps: 5,
7070
});
71-
72-
expect(result.current).toBe(5);
71+
expect(result.current.param).toBe(5);
7372

7473
rerender(6);
75-
76-
expect(result.current).toBe(6);
74+
expect(result.current.param).toBe(6);
7775
});
7876

7977
test('props type is inferred correctly when initial props is explicitly undefined', () => {
8078
const { result, rerender } = renderHook((num: number | undefined) => useMyHook(num), {
8179
initialProps: undefined,
8280
});
8381

84-
expect(result.current).toBeUndefined();
82+
expect(result.current.param).toBeUndefined();
8583

8684
rerender(6);
87-
88-
expect(result.current).toBe(6);
85+
expect(result.current.param).toBe(6);
8986
});
9087

9188
/**

src/__tests__/suspense-fake-timers.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ afterEach(() => {
1616
});
1717

1818
function Suspending({ promise, testID }: { promise: Promise<unknown>; testID: string }) {
19+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
20+
// @ts-ignore React 18 does not have `use` hook
1921
React.use(promise);
2022
return <View testID={testID} />;
2123
}

src/__tests__/suspense.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ afterEach(() => {
1414
});
1515

1616
function Suspending({ promise, testID }: { promise: Promise<unknown>; testID: string }) {
17+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
18+
// @ts-ignore React 18 does not have `use` hook
1719
React.use(promise);
1820
return <View testID={testID} />;
1921
}

src/render-hook.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import * as React from 'react';
22

33
import render from './render';
44
import renderAsync from './render-async';
5+
import type { RefObject } from './types';
56

67
export type RenderHookResult<Result, Props> = {
7-
result: React.RefObject<Result>;
8+
result: RefObject<Result>;
89
rerender: (props: Props) => void;
910
unmount: () => void;
1011
};
1112

1213
export type RenderHookAsyncResult<Result, Props> = {
13-
result: React.RefObject<Result>;
14+
result: RefObject<Result>;
1415
rerenderAsync: (props: Props) => Promise<void>;
1516
unmountAsync: () => Promise<void>;
1617
};
@@ -39,7 +40,7 @@ export function renderHook<Result, Props>(
3940
hookToRender: (props: Props) => Result,
4041
options?: RenderHookOptions<Props>,
4142
): RenderHookResult<Result, Props> {
42-
const result: React.RefObject<Result | null> = React.createRef();
43+
const result = React.createRef<Result>() as RefObject<Result>;
4344

4445
function HookContainer({ hookProps }: { hookProps: Props }) {
4546
const renderResult = hookToRender(hookProps);
@@ -58,8 +59,7 @@ export function renderHook<Result, Props>(
5859
);
5960

6061
return {
61-
// Result should already be set after the first render effects are run.
62-
result: result as React.RefObject<Result>,
62+
result: result,
6363
rerender: (hookProps: Props) => rerenderComponent(<HookContainer hookProps={hookProps} />),
6464
unmount,
6565
};
@@ -69,7 +69,7 @@ export async function renderHookAsync<Result, Props>(
6969
hookToRender: (props: Props) => Result,
7070
options?: RenderHookOptions<Props>,
7171
): Promise<RenderHookAsyncResult<Result, Props>> {
72-
const result: React.RefObject<Result | null> = React.createRef();
72+
const result = React.createRef<Result>() as RefObject<Result>;
7373

7474
function TestComponent({ hookProps }: { hookProps: Props }) {
7575
const renderResult = hookToRender(hookProps);
@@ -88,8 +88,7 @@ export async function renderHookAsync<Result, Props>(
8888
);
8989

9090
return {
91-
// Result should already be set after the first render effects are run.
92-
result: result as React.RefObject<Result>,
91+
result: result,
9392
rerenderAsync: (hookProps: Props) =>
9493
rerenderComponentAsync(<TestComponent hookProps={hookProps} />),
9594
unmountAsync,

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/** `RefObject` type from React 19. */
2+
export type RefObject<T> = {
3+
current: T;
4+
};
5+
16
/**
27
* Location of an element.
38
*/

0 commit comments

Comments
 (0)