Skip to content

feat: add support for multiple arguments in renderHook() #1403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
20 changes: 20 additions & 0 deletions src/__tests__/renderHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}) {
Expand Down
25 changes: 19 additions & 6 deletions src/pure.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -343,13 +356,13 @@ function renderHook(renderCallback, options = {}) {
}

const {rerender: baseRerender, unmount} = render(
<TestComponent renderCallbackProps={initialProps} />,
<TestComponent renderCallbackArgs={initial} />,
renderOptions,
)

function rerender(rerenderCallbackProps) {
function rerender(...rerenderCallbackArgs) {
return baseRerender(
<TestComponent renderCallbackProps={rerenderCallbackProps} />,
<TestComponent renderCallbackArgs={rerenderCallbackArgs} />,
)
}

Expand Down
40 changes: 37 additions & 3 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,12 @@ export function render(
options?: Omit<RenderOptions, 'queries'> | undefined,
): RenderResult

export interface RenderHookResult<Result, Props> {
export interface RenderHookArgsResult<Result, Args extends any[]> {
/**
* 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
Expand All @@ -203,6 +204,11 @@ export interface RenderHookResult<Result, Props> {
unmount: () => void
}

export type RenderHookResult<Result, Props> = RenderHookArgsResult<
Result,
[Props?]
>

/** @deprecated */
export type BaseRenderHookOptions<
Props,
Expand Down Expand Up @@ -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<Q, Container, BaseElement> {
/**
* 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.
Expand All @@ -271,6 +290,21 @@ export function renderHook<
options?: RenderHookOptions<Props, Q, Container, BaseElement> | undefined,
): RenderHookResult<Result, Props>

/**
* 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<Args, Q, Container, BaseElement> | undefined,
): RenderHookArgsResult<Result, Args>

/**
* Unmounts React trees that were mounted with render.
*/
Expand Down
10 changes: 10 additions & 0 deletions types/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, typeof result.current>(result.current)
rerender('b', 3)
unmount()
}

export function testContainer() {
render('a', {container: document.createElement('div')})
render('a', {container: document.createDocumentFragment()})
Expand Down
Loading