// src/reducers/FooReducer.js
export function FooReducer(state, action) {
switch (action.type) {
case 'update': {
return action.newState;
}
// ... other actions
default:
throw new Error('Unknown action type');
}
}
// src/components/BarComponent.js
export function BarComponent() {
const [state, dispatch] = useReducer(FooReducer, []);
return (
{state.map((item) => (<div />))}
);
}
// src/components/BarComponent.test.js
it('should render as many divs as there are items', () => {
act(() => {
const { result } = renderHook(() => useReducer(FooReducer, [1]));
const [, dispatch] = result.current;
wrapper = mount(<BarComponent />);
dispatch({type: 'update', newState: [1, 2, 3]});
});
expect(wrapper.find(div)).toHaveLength(3);
});
The above test example does not work, but serves to demonstrate what I am trying to achieve. and would actually render 0 div, as the initial state declared in the component contains 0 items.
How would I go about modifying a reducer's state or changing the initialState it is deployed with for testing purposes?
I am used to Redux reducers being used throughout multiple components, but useReducer needs a passed initialState... which raises the question: Is react-hook's reducer usable through multiple components as a single instance or will it always be 2 separate instances?
In your example, you're trying to test two things at the same time, which would be better off as separate tests: A unit test for your reducer, and a component test where the component uses the reducer.
- How would I go about modifying a reducer's state or changing the initialState it is deployed with for testing purposes?
Similar to a Redux reducers, your reducer is easily unit testable since you're exporting it as a pure function. Just pass in your initial state into the state
argument, and your action into action
:
it('returns new state for "update" type', () => {
const initialState = [1];
const updateAction = {type: 'update', newState: [1, 2, 3] };
const updatedState = fooReducer(initialState, udpateAction);
expect(updatedState).toEqual([1, 2, 3]);
});
You could also test it in the context of useReducer
if you prefer:
it('should render as many divs as there are items', () => {
act(() => {
const { result } = renderHook(() => useReducer(FooReducer, [1]));
const [state, dispatch] = result.current;
dispatch({type: 'update', newState: [1, 2, 3]});
});
expect(state).toEqual([1, 2, 3]);
// or expect(state).toHaveLenth(3) if you prefer
});
- I am used to Redux reducers being used throughout multiple components, but useReducer needs a passed initialState... which raises the question: Is react-hook's reducer usable through multiple components asa single instance or will it always be 2 separate instances?
Here's how useReducer
is different from Redux: You can reuse the reducer itself, but if you have multiple useReducer
s, the state
and dispatch
returned from each one, as well as the initial state, will be separate instances.
In order to test that your BarComponent updates when the reducer updates, you'll need a way to trigger dispatch
from within the component, since you're calling useReducer
inside your component. Here's an example:
export function BarComponent() {
const [state, dispatch] = useReducer(FooReducer, []);
const handleUpdate = () => dispatch({type: 'update', newState: [1, 2, 3]})
return (
<>
{state.map((item) => (<div />))}
<button onClick={handleUpdate}>Click to update state</button>
</>
);
}
it('should render as many divs as there are items', () => {
wrapper = mount(<BarComponent />);
expect(wrapper.find('div')).toHaveLength(0);
wrapper.find('button').simulate('click');
expect(wrapper.find('div')).toHaveLength(3);
});
This probably isn't very realistic, since I'm hardcoding the new array in the component itself, but hopefully it gives you the idea!