|
| 1 | +--- |
| 2 | +name: Advanced Hooks |
| 3 | +menu: Usage |
| 4 | +route: '/usage/advanced-hooks' |
| 5 | +--- |
| 6 | + |
| 7 | +# Advanced Hooks |
| 8 | + |
| 9 | +## Providing Props |
| 10 | + |
| 11 | +Sometimes a hook relies on the props passed to it in order to do it's thing. For example the `useCounter` hook we built in the [Basic Hooks](/usage/basic-hooks) section could easily accept the initial value of the counter: |
| 12 | + |
| 13 | +```js |
| 14 | +import { useState, useCallback } from 'react' |
| 15 | + |
| 16 | +export default function useCounter(initialValue = 0) { |
| 17 | + const [count, setCount] = useState(initialValue) |
| 18 | + const increment = useCallback(() => setCount(count + 1), [count]) |
| 19 | + return { count, increment } |
| 20 | +} |
| 21 | +``` |
| 22 | + |
| 23 | +Overriding the `initialValue` prop in out test is as easy as calling the hook with the value we want to use: |
| 24 | + |
| 25 | +```js |
| 26 | +import { renderHook, act } from 'react-hooks-testing-library' |
| 27 | +import useCounter from './useCounter' |
| 28 | + |
| 29 | +test('should increment counter from custom initial value', () => { |
| 30 | + const { result } = renderHook(() => useCounter(9000)) |
| 31 | + |
| 32 | + act(() => { |
| 33 | + result.current.increment() |
| 34 | + }) |
| 35 | + |
| 36 | + expect(result.current.count).toBe(9001) |
| 37 | +}) |
| 38 | +``` |
| 39 | + |
| 40 | +### Changing Props |
| 41 | + |
| 42 | +Many of the hook primitives use an array of dependent values to determine when to perform specific actions, such as recalculating an expensive value or running an effect. If we update our `useCounter` hook to have a `reset` function that resets the value to the `initialValue` it might look something like this: |
| 43 | + |
| 44 | +```js |
| 45 | +import { useState, useCallback } from 'react' |
| 46 | + |
| 47 | +export default function useCounter(initialValue = 0) { |
| 48 | + const [count, setCount] = useState(initialValue) |
| 49 | + const increment = useCallback(() => setCount(count + 1), [count]) |
| 50 | + const reset = useCallback(() => setCount(initialValue), [initialValue]) |
| 51 | + return { count, increment, reset } |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +Now, the only time the `reset` function will be updated is if `initialValue` changes. The most basic way to handle changing the input props of our hook in a test is to simply update the value in a variable and rerender the hook: |
| 56 | + |
| 57 | +```js |
| 58 | +import { renderHook, act } from 'react-hooks-testing-library' |
| 59 | +import useCounter from './useCounter' |
| 60 | + |
| 61 | +test('should reset counter to updated initial value', () => { |
| 62 | + let initialValue = 0 |
| 63 | + const { result, rerender } = renderHook(() => useCounter(initialValue)) |
| 64 | + |
| 65 | + initialValue = 10 |
| 66 | + rerender() |
| 67 | + |
| 68 | + act(() => { |
| 69 | + result.current.reset() |
| 70 | + }) |
| 71 | + |
| 72 | + expect(result.current.count).toBe(10) |
| 73 | +}) |
| 74 | +``` |
| 75 | + |
| 76 | +This is fine, but if there are lots of props, it can become a bit difficult to have variables to keep track of them all. Another option is to use the `initialProps` option and `newProps` of `rerender`: |
| 77 | + |
| 78 | +```js |
| 79 | +import { renderHook, act } from 'react-hooks-testing-library' |
| 80 | +import useCounter from './useCounter' |
| 81 | + |
| 82 | +test('should reset counter to updated initial value', () => { |
| 83 | + const { result, rerender } = renderHook(({ initialValue }) => useCounter(initialValue), { |
| 84 | + initialProps: { initialValue: 0 } |
| 85 | + }) |
| 86 | + |
| 87 | + rerender({ initialValue: 10 }) |
| 88 | + |
| 89 | + act(() => { |
| 90 | + result.current.reset() |
| 91 | + }) |
| 92 | + |
| 93 | + expect(result.current.count).toBe(10) |
| 94 | +}) |
| 95 | +``` |
| 96 | + |
| 97 | +Another case where this is useful is when you want limit the scope of the variables being closed over to just be inside the hook callback. The following (contrived) example fails because the `id` value changes for both the setup and cleanup of the `useEffect` call: |
| 98 | + |
| 99 | +```js |
| 100 | +import { useEffect } from 'react' |
| 101 | +import { renderHook } from "react-hooks-testing-library" |
| 102 | +import sideEffect from './sideEffect |
| 103 | +
|
| 104 | +test("should clean up side effect", () => { |
| 105 | + let id = "first" |
| 106 | + const { rerender } = renderHook(() => { |
| 107 | + useEffect(() => { |
| 108 | + sideEffect.start(id) |
| 109 | + return () => { |
| 110 | + sideEffect.stop(id) // this id will get the new value when the effect is cleaned up |
| 111 | + } |
| 112 | + }, [id]) |
| 113 | + }) |
| 114 | +
|
| 115 | + id = "second" |
| 116 | + rerender() |
| 117 | +
|
| 118 | + expect(sideEffect.get("first")).toBe(false) |
| 119 | + expect(sideEffect.get("second")).toBe(true) |
| 120 | +}) |
| 121 | +``` |
| 122 | +
|
| 123 | +By using the `initialProps` and `newProps` the captured `id` value from the first render is used to clean up the effect, allowing the test to pass as expected: |
| 124 | +
|
| 125 | +```js |
| 126 | +import { useEffect } from 'react' |
| 127 | +import { renderHook } from "react-hooks-testing-library" |
| 128 | +import sideEffect from './sideEffect |
| 129 | + |
| 130 | +test("should clean up side effect", () => { |
| 131 | + const { rerender } = renderHook( |
| 132 | + ({ id }) => { |
| 133 | + useEffect(() => { |
| 134 | + sideEffect.start(id) |
| 135 | + return () => { |
| 136 | + sideEffect.stop(id) // this id will get the new value when the effect is cleaned up |
| 137 | + } |
| 138 | + }, [id]) |
| 139 | + }, |
| 140 | + { |
| 141 | + initialProps: { id: "first" } |
| 142 | + } |
| 143 | + ) |
| 144 | + |
| 145 | + rerender({ id: "second" }) |
| 146 | + |
| 147 | + expect(thing.get("first")).toBe(false) |
| 148 | + expect(thing.get("second")).toBe(true) |
| 149 | +}) |
| 150 | +``` |
| 151 | + |
| 152 | +This is a fairly obscure case, so pick the method that fits best for you and your test. |
0 commit comments