Enzyme: with shallow, redux store update does not trigger componentDidUpdate

Viewed 1596

I'm having trouble testing a very specific use case of my React components. In this case, I have a component that dispatches an action on mount, and then dispatches another action after the results of the first dispatch come in.

The problem, I believe, comes down to enzyme's shallow method not calling componentDidUpdate in my component. I have reproduced the problem with the simplest code I could, and confirmed that the problem disappears if I use mount instead of shallow. However, enzyme's documentation says that since version 3, shallow does call React's lifecycle methods such as componentDidUpdate. And of course, I cannot use mount in my test because I don't want to mount the entire tree (in my case, that would mean loading my entire application).

Thus I wonder: is this a bug in enzyme, or I am missing something in my usage of that library?

Here's my code example:

import * as React from 'react';
import { connect } from 'react-redux';
import { createStore } from 'redux';

import { shallow } from 'enzyme';
import sinon from 'sinon';


const actions = {
    // This would fetch a resource on the network.
    getList: () => {},

    receiveList: list => {
        return {
            type: 'RECEIVE_LIST',
            list,
        };
    },

    // This would fetch a list of resources on the network.
    getObjects: () => {},
};


const reducer = (state = { list: [] }, action) => {
    switch (action.type) {
        case 'RECEIVE_LIST':
            return { ...state, list: action.list };
        default:
            return state;
    }
};


class MyAppBase extends React.Component {
    componentDidMount() {
        this.props.dispatch(actions.getList());
    }

    componentDidUpdate(nextProps) {
        if (nextProps.list !== this.props.list) {
            this.props.dispatch(actions.getObjects(nextProps.list));
        }
    }

    render() {
        return null;
    }
}


const MyApp = connect(state => { return { list: state.list }; })(MyAppBase);



describe('<MyApp>', () => {
    it('fetches objects once list is received', () => {
        // Mock actions that call the network.
        sinon.stub(actions, 'getList');
        actions.getList.returns({type: 'whatever'});
        sinon.stub(actions, 'getObjects');
        actions.getObjects.returns({type: 'whatever'});

        // Create a redux store.
        const store = createStore(reducer);

        // Create the component.
        const wrapper = shallow(<MyApp store={store} />);

        // Verify that `componentDidMount` was called but not `componentDidUpdate`.
        expect(actions.getList.callCount).toEqual(1);
        expect(actions.getObjects.callCount).toEqual(0);

        // Dispatch an action that will change the redux store.
        store.dispatch(actions.receiveList([ 'aa', 'bb' ]));

        // Expect the `componentDidUpdate` method to be called.
        expect(actions.getObjects.callCount).toEqual(1);

        // This doesn't work, for some reason `componentDidUpdate` is never called.
    });
});

Package versions I use for this:

"react": "16.6.0",
"react-dom": "16.6.0",
"react-redux": "5.1.0",
"redux": "4.0.1",
"enzyme": "3.7.0",
"enzyme-adapter-react-16": "1.6.0",
"sinon": "7.1.0"

Thanks!

1 Answers

The "componentDidUpdate" method does not seem to be fully supported by Enzyme's "shallow" method.

A shallow mounted component seems to only call componentDidUpdate following a call to wrapper.setProps(). It doesn't matter that the store is updated with new state, the wrapper is just not updated.

This can be confirmed by changing your test to do the following:

// Dispatch an action that will change the redux store.
store.dispatch(actions.receiveList([ 'aa', 'bb' ]));

// Will print '[]'
console.log(wrapper.instance().props.list);

// Create a new wrapper with the current store:
const wrapper2 = shallow(<MyApp store={store} />);

// Will print '['aa', 'bb']'
console.log(wrapper2.instance().props.list)

A workaround would be to call setProps after the store.dispatch (or preferably just use mount instead of shallow, if it is feasible):

// Dispatch an action that will change the redux store.
store.dispatch(actions.receiveList([ 'aa', 'bb' ]));

// Manually cause componentDidUpdate to trigger
wrapper.setProps({
  list: ['aa', 'bb']
});

Additionally, componentDidUpdate can be triggered by the simulate method for a click or a change on a button or input.

Related