Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How test a component using the useReducer hook?

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?

like image 436
Sebastien De Varennes Avatar asked Dec 10 '19 21:12

Sebastien De Varennes


People also ask

How do you test a hook?

If you need to test a custom Hook, you can do so by creating a component in your test, and using your Hook from it. Then you can test the component you wrote. To reduce the boilerplate, we recommend using React Testing Library which is designed to encourage writing tests that use your components as the end users do.

How do you use useReducer in react hooks?

Syntax. The useReducer Hook accepts two arguments. The reducer function contains your custom state logic and the initialState can be a simple value but generally will contain an object. The useReducer Hook returns the current state and a dispatch method.

How do you test a custom hook?

The easiest way to test a custom react hook is to make use of the @testing-library/react-hooks library so you can test your react hooks in a similar way to a react component by passing the hook into the renderHook function and then asserting if the results are correct, similar to how you would in normal unit tests.


1 Answers

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 key={item} />))}
      <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!

like image 115
helloitsjoe Avatar answered Sep 27 '22 19:09

helloitsjoe