How test a component using the useReducer hook?

Sebastien De Varennes picture Sebastien De Varennes · Dec 10, 2019 · Viewed 10.2k times · Source

Reducer

// 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');
  }
}

Component

// src/components/BarComponent.js

export function BarComponent() {
  const [state, dispatch] = useReducer(FooReducer, []);

  return (
    {state.map((item) => (<div />))}
  );
}

Test

// 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.

  1. How would I go about modifying a reducer's state or changing the initialState it is deployed with for testing purposes?

  2. 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?

Answer

helloitsjoe picture helloitsjoe · Dec 11, 2019

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.

  1. 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
});
  1. 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 useReducers, 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!