diff --git a/src/__tests__/renderHook.test.tsx b/src/__tests__/renderHook.test.tsx
new file mode 100644
index 000000000..389f38ffa
--- /dev/null
+++ b/src/__tests__/renderHook.test.tsx
@@ -0,0 +1,62 @@
+import React, { ReactNode } from 'react';
+import { renderHook } from '../pure';
+
+test('gives comitted result', () => {
+ const { result } = renderHook(() => {
+ const [state, setState] = React.useState(1);
+
+ React.useEffect(() => {
+ setState(2);
+ }, []);
+
+ return [state, setState];
+ });
+
+ expect(result.current).toEqual([2, expect.any(Function)]);
+});
+
+test('allows rerendering', () => {
+ const { result, rerender } = renderHook(
+ (props: { branch: 'left' | 'right' }) => {
+ const [left, setLeft] = React.useState('left');
+ const [right, setRight] = React.useState('right');
+
+ // eslint-disable-next-line jest/no-if
+ switch (props.branch) {
+ case 'left':
+ return [left, setLeft];
+ case 'right':
+ return [right, setRight];
+
+ default:
+ throw new Error(
+ 'No Props passed. This is a bug in the implementation'
+ );
+ }
+ },
+ { initialProps: { branch: 'left' } }
+ );
+
+ expect(result.current).toEqual(['left', expect.any(Function)]);
+
+ rerender({ branch: 'right' });
+
+ expect(result.current).toEqual(['right', expect.any(Function)]);
+});
+
+test('allows wrapper components', async () => {
+ const Context = React.createContext('default');
+ function Wrapper({ children }: { children: ReactNode }) {
+ return {children};
+ }
+ const { result } = renderHook(
+ () => {
+ return React.useContext(Context);
+ },
+ {
+ wrapper: Wrapper,
+ }
+ );
+
+ expect(result.current).toEqual('provided');
+});
diff --git a/src/pure.ts b/src/pure.ts
index 9baffa97f..74468d39e 100644
--- a/src/pure.ts
+++ b/src/pure.ts
@@ -6,6 +6,7 @@ import waitFor from './waitFor';
import waitForElementToBeRemoved from './waitForElementToBeRemoved';
import { within, getQueriesForElement } from './within';
import { getDefaultNormalizer } from './matches';
+import { renderHook } from './renderHook';
export { act };
export { cleanup };
@@ -15,3 +16,4 @@ export { waitFor };
export { waitForElementToBeRemoved };
export { within, getQueriesForElement };
export { getDefaultNormalizer };
+export { renderHook };
diff --git a/src/renderHook.tsx b/src/renderHook.tsx
new file mode 100644
index 000000000..35760384b
--- /dev/null
+++ b/src/renderHook.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import type { ComponentType } from 'react';
+import render from './render';
+
+interface RenderHookResult {
+ rerender: (props: Props) => void;
+ result: { current: Result };
+ unmount: () => void;
+}
+
+type RenderHookOptions = Props extends object | string | number | boolean
+ ? {
+ initialProps: Props;
+ wrapper?: ComponentType;
+ }
+ : { wrapper?: ComponentType; initialProps?: never } | undefined;
+
+export function renderHook(
+ renderCallback: (props: Props) => Result,
+ options?: RenderHookOptions
+): RenderHookResult {
+ const initialProps = options?.initialProps;
+ const wrapper = options?.wrapper;
+
+ const result: React.MutableRefObject = React.createRef();
+
+ function TestComponent({
+ renderCallbackProps,
+ }: {
+ renderCallbackProps: Props;
+ }) {
+ const renderResult = renderCallback(renderCallbackProps);
+
+ React.useEffect(() => {
+ result.current = renderResult;
+ });
+
+ return null;
+ }
+
+ const { rerender: baseRerender, unmount } = render(
+ // @ts-expect-error since option can be undefined, initialProps can be undefined when it should'nt
+ ,
+ { wrapper }
+ );
+
+ function rerender(rerenderCallbackProps: Props) {
+ return baseRerender(
+
+ );
+ }
+
+ // @ts-expect-error result is ill typed because ref is initialized to null
+ return { result, rerender, unmount };
+}
diff --git a/typings/index.flow.js b/typings/index.flow.js
index 4c550cff7..173e5356c 100644
--- a/typings/index.flow.js
+++ b/typings/index.flow.js
@@ -336,6 +336,17 @@ type FireEventAPI = FireEventFunction & {
scroll: (element: ReactTestInstance, ...data: Array) => any,
};
+type RenderHookResult = {
+ rerender: (props: Props) => void,
+ result: { current: Result },
+ unmount: () => void,
+};
+
+type RenderHookOptions = {
+ initialProps?: Props,
+ wrapper?: React.ComponentType,
+};
+
declare module '@testing-library/react-native' {
declare export var render: (
component: React.Element,
@@ -363,4 +374,11 @@ declare module '@testing-library/react-native' {
declare export var getDefaultNormalizer: (
normalizerConfig?: NormalizerConfig
) => NormalizerFn;
+
+ declare type RenderHookFunction = (
+ renderCallback: (props: Props) => Result,
+ options?: RenderHookOptions
+ ) => RenderHookResult;
+
+ declare export var renderHook: RenderHookFunction;
}
diff --git a/website/docs/API.md b/website/docs/API.md
index dd66193c4..392134d06 100644
--- a/website/docs/API.md
+++ b/website/docs/API.md
@@ -465,3 +465,139 @@ expect(submitButtons).toHaveLength(3); // expect 3 elements
## `act`
Useful function to help testing components that use hooks API. By default any `render`, `update`, `fireEvent`, and `waitFor` calls are wrapped by this function, so there is no need to wrap it manually. This method is re-exported from [`react-test-renderer`](https://github.com/facebook/react/blob/main/packages/react-test-renderer/src/ReactTestRenderer.js#L567]).
+
+## `renderHook`
+
+Defined as:
+
+```ts
+function renderHook(
+ callback: (props?: any) => any,
+ options?: RenderHookOptions
+): RenderHookResult;
+```
+
+Renders a test component that will call the provided `callback`, including any hooks it calls, every time it renders. Returns [`RenderHookResult`](#renderhookresult-object) object, which you can interact with.
+
+```ts
+import { renderHook } from '@testing-library/react-native';
+import { useCount } from '../useCount';
+
+it('should increment count', () => {
+ const { result } = renderHook(() => useCount());
+
+ expect(result.current.count).toBe(0);
+ act(() => {
+ // Note that you should wrap the calls to functions your hook returns with `act` if they trigger an update of your hook's state to ensure pending useEffects are run before your next assertion.
+ result.increment();
+ });
+ expect(result.current.count).toBe(1);
+});
+```
+
+```ts
+// useCount.js
+export const useCount = () => {
+ const [count, setCount] = useState(0);
+ const increment = () => setCount((previousCount) => previousCount + 1);
+
+ return { count, increment };
+};
+```
+
+The `renderHook` function accepts the following arguments:
+
+### `callback`
+
+The function that is called each `render` of the test component. This function should call one or more hooks for testing.
+
+The `props` passed into the callback will be the `initialProps` provided in the `options` to `renderHook`, unless new props are provided by a subsequent `rerender` call.
+
+### `options` (Optional)
+
+A `RenderHookOptions` object to modify the execution of the `callback` function, containing the following properties:
+
+#### `initialProps`
+
+The initial values to pass as `props` to the `callback` function of `renderHook`.
+
+#### `wrapper`
+
+A React component to wrap the test component in when rendering. This is usually used to add context providers from `React.createContext` for the hook to access with `useContext`. `initialProps` and props subsequently set by `rerender` will be provided to the wrapper.
+
+### `RenderHookResult` object
+
+The `renderHook` function returns an object that has the following properties:
+
+#### `result`
+
+```jsx
+{
+ all: Array
+ current: any,
+ error: Error
+}
+```
+
+The `current` value of the `result` will reflect the latest of whatever is returned from the `callback` passed to `renderHook`. Any thrown values from the latest call will be reflected in the `error` value of the `result`. The `all` value is an array containing all the returns (including the most recent) from the callback. These could be `result` or an `error` depending on what the callback returned at the time.
+
+#### `rerender`
+
+function rerender(newProps?: any): void
+
+A function to rerender the test component, causing any hooks to be recalculated. If `newProps` are passed, they will replace the `callback` function's `initialProps` for subsequent rerenders.
+
+#### `unmount`
+
+function unmount(): void
+
+A function to unmount the test component. This is commonly used to trigger cleanup effects for `useEffect` hooks.
+
+### Examples
+
+Here we present some extra examples of using `renderHook` API.
+
+#### With `initialProps`
+
+```jsx
+const useCount = (initialCount: number) => {
+ const [count, setCount] = useState(initialCount);
+ const increment = () => setCount((previousCount) => previousCount + 1);
+
+ useEffect(() => {
+ setCount(initialCount);
+ }, [initialCount]);
+
+ return { count, increment };
+};
+
+it('should increment count', () => {
+ const { result, rerender } = renderHook(
+ (initialCount: number) => useCount(initialCount),
+ { initialProps: 1 }
+ );
+
+ expect(result.current.count).toBe(1);
+
+ act(() => {
+ result.increment();
+ });
+
+ expect(result.current.count).toBe(2);
+ rerender(5);
+ expect(result.current.count).toBe(5);
+});
+```
+
+#### With `wrapper`
+
+```jsx
+it('should work properly', () => {
+ function Wrapper({ children }: { children: ReactNode }) {
+ return {children};
+ }
+
+ const { result } = renderHook(() => useHook(), { wrapper: Wrapper });
+ // ...
+});
+```