diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js index f331e90e..d4d425e0 100644 --- a/src/__tests__/renderHook.js +++ b/src/__tests__/renderHook.js @@ -50,6 +50,26 @@ test('allows rerendering', () => { expect(result.current).toEqual(['right', expect.any(Function)]) }) +test('allows rerendering with multiple arguments', () => { + const useTest = (arg1, arg2, arg3) => arg1 + arg2 + arg3 + const {result, rerender} = renderHook(useTest, {initialArgs: [2, 3, 4]}) + expect(result.current).toBe(9) + rerender(3, 4, -1) + expect(result.current).toBe(6) +}) + +test('throws on invalid options', () => { + const useTest = (arg1, arg2) => arg1 + arg2 + expect(() => { + renderHook(useTest, {initialProps: {}, initialArgs: []}) + }).toThrow( + 'Options `initialProps` and `initialArgs` cannot be used together.', + ) + expect(() => { + renderHook(useTest, {initialArgs: {}}) + }).toThrow('Option `initialArgs` must be an array.') +}) + test('allows wrapper components', async () => { const Context = React.createContext('default') function Wrapper({children}) { diff --git a/src/pure.js b/src/pure.js index 0f9c487d..7ca6a45f 100644 --- a/src/pure.js +++ b/src/pure.js @@ -318,7 +318,7 @@ function cleanup() { } function renderHook(renderCallback, options = {}) { - const {initialProps, ...renderOptions} = options + const {initialProps, initialArgs, ...renderOptions} = options if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') { const error = new Error( @@ -330,10 +330,23 @@ function renderHook(renderCallback, options = {}) { throw error } + if (initialProps && initialArgs) { + throw new Error( + 'Options `initialProps` and `initialArgs` cannot be used together.', + ) + } + + if (initialArgs !== undefined && !Array.isArray(initialArgs)) { + throw new Error('Option `initialArgs` must be an array.') + } + + // convert `initialProps` to an empty or single-element array + const initial = initialArgs || (initialProps ? [initialProps] : []) + const result = React.createRef() - function TestComponent({renderCallbackProps}) { - const pendingResult = renderCallback(renderCallbackProps) + function TestComponent({renderCallbackArgs}) { + const pendingResult = renderCallback(...renderCallbackArgs) React.useEffect(() => { result.current = pendingResult @@ -343,13 +356,13 @@ function renderHook(renderCallback, options = {}) { } const {rerender: baseRerender, unmount} = render( - , + , renderOptions, ) - function rerender(rerenderCallbackProps) { + function rerender(...rerenderCallbackArgs) { return baseRerender( - , + , ) } diff --git a/types/index.d.ts b/types/index.d.ts index bdd60567..9abedc77 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -181,11 +181,12 @@ export function render( options?: Omit | undefined, ): RenderResult -export interface RenderHookResult { +export interface RenderHookArgsResult { /** - * Triggers a re-render. The props will be passed to your renderHook callback. + * Triggers a re-render. The arguments will be passed to your renderHook + * callback. */ - rerender: (props?: Props) => void + rerender: (...args: Args) => void /** * This is a stable reference to the latest value returned by your renderHook * callback @@ -203,6 +204,11 @@ export interface RenderHookResult { unmount: () => void } +export type RenderHookResult = RenderHookArgsResult< + Result, + [Props?] +> + /** @deprecated */ export type BaseRenderHookOptions< Props, @@ -256,6 +262,19 @@ export interface RenderHookOptions< initialProps?: Props | undefined } +export interface RenderHookArgsOptions< + Args extends any[], + Q extends Queries = typeof queries, + Container extends RendererableContainer | HydrateableContainer = HTMLElement, + BaseElement extends RendererableContainer | HydrateableContainer = Container, +> extends RenderOptions { + /** + * The argument passed to the renderHook callback. Can be useful if you plan + * to use the rerender utility to change the values passed to your hook. + */ + initialArgs: Args +} + /** * Allows you to render a hook within a test React component without having to * create that component yourself. @@ -271,6 +290,21 @@ export function renderHook< options?: RenderHookOptions | undefined, ): RenderHookResult +/** + * Allows you to render a hook within a test React component without having to + * create that component yourself. + */ +export function renderHook< + Result, + Args extends any[], + Q extends Queries = typeof queries, + Container extends RendererableContainer | HydrateableContainer = HTMLElement, + BaseElement extends RendererableContainer | HydrateableContainer = Container, +>( + render: (...initialArgs: Args) => Result, + options: RenderHookArgsOptions | undefined, +): RenderHookArgsResult + /** * Unmounts React trees that were mounted with render. */ diff --git a/types/test.tsx b/types/test.tsx index 825d5699..87195002 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -237,6 +237,16 @@ export function testRenderHookProps() { unmount() } +export function testRenderHookArgs() { + const useTest = (s: string, n: number): string => s.repeat(n) + const {result, rerender, unmount} = renderHook(useTest, { + initialArgs: ['a', 2], + }) + expectType(result.current) + rerender('b', 3) + unmount() +} + export function testContainer() { render('a', {container: document.createElement('div')}) render('a', {container: document.createDocumentFragment()})