0

I would like to test out the method, shouldComponentUpdate but the test I wrote doesn't catch the side effects. How do I test this?

I have tried the solution proposed here but there are issues with it which I will show in the code: How to unit test React Component shouldComponentUpdate method

I have tried using shallow rather than mount but when I did that, my test fails at this point:

    expect(shouldComponentUpdate).to.have.property("callCount", 1);
    // AssertionError: expected [Function: proxy] to have a property 'callCount' of 1, but got 0

Here's my component's shouldComponentUpdate function:

    shouldComponentUpdate(nextProps, nextState) {
        if (this.props.storageValue !== nextProps.storageValue) {
            localStorage.setItem(constantFile.storageKey, nextProps.storageValue);
            localStorage.setItem(constantFile.timestampKey, now());
            this.setState({
                newStorage: nextProps.storageValue
            });
            return true;
        }

        ...

        return false;
    }

Here's my test:

    it("Test shouldComponentUpdate", () => {
        /* eslint-disable one-var */
        const wrapper = mount(
            <Provider store={store}>
                <MyComponent />
            </Provider>),
            shouldComponentUpdate = sinon.spy(CapitalHomeAccessPointContainer.prototype, "shouldComponentUpdate");
        wrapper.setProps({
            storageValue: true
        });
        expect(shouldComponentUpdate).to.have.property("callCount", 1);   
        expect(shouldComponentUpdate.returned(true)).to.be.equal(true);
  expect(localStorage.getItem(constantFile.timestampKey)).to.equal(someExpectedTimestamp);
    });
expect(shouldComponentUpdate).to.have.property("callCount", 1);

fails with

AssertionError: expected false to equal true

Why does this happen? I thought shouldComponentUpdate should have returned true? Later, I'm hoping to test the state and local storage side effects but I do not understand this error.

valerianna
  • 33
  • 4
  • 1
    There needs to be a state change in order for `shouldComponentUpdate` to trigger. You also should be testing the results of the function, not if it's being called. – MinusFour Aug 24 '19 at 23:36
  • I thought the documentation said, "Use shouldComponentUpdate() to let React know if a component’s output is not affected by the current change in state or props. The default behavior is to re-render on every state change, and in the vast majority of cases you should rely on the default behavior". `https://reactjs.org/docs/react-component.html#shouldcomponentupdate` So I thought a change in props should also trigger true if specified in `shouldComponentUpdate`? – valerianna Aug 24 '19 at 23:55
  • You also mentioned to test the side effects of the function. I tried to do that as well. ``` expect(wrapper.find("MyComponent").state("newStorage")).to.be.true; ``` results in this error: ``` AssertionError: expected false to be true ``` – valerianna Aug 25 '19 at 00:21

1 Answers1

2

You're handling of the shouldComponentUpdate lifecycle is wrong. You don't setState within it, instead you only return a boolean (true or false) to let React know when to re-render the DOM. This allows you to block updates from outside props. For example:

shouldComponentUpdate(nextProps, nextState) {
 return this.state.newStorage !== nextState.newStorage
}

The above states: if current newStorage state doesn't match next newStorage state, then re-render the component. The above would block prop changes from rerendering the component. If you changed storage props, it WOULD NOT update this component because the state wasn't changed. You want to use shouldComponentUpdate to prevent unnecessary double re-renders and/or to prevent a parent component's re-renders from unnecessarily re-rendering a child component. In this case, you won't need to use this lifecycle method.

Instead, you should either be using static getDerivedStateFromProps for props to update state before a render OR using componentDidUpdate to update state after a render.

In your case, since you only want to update state if this.props.storageValue changes, then you should use componentDidUpdate:

componentDidUpdate(prevProps) {
   if (this.props.storageValue !== prevProps.storageValue) {
     localStorage.setItem(constantFile.storageKey, nextProps.storageValue);
     localStorage.setItem(constantFile.timestampKey, now());
     this.setState({ newStorage: nextProps.storageValue });
   }
}

The above checks if the current props don't match previous props, then updates the state to reflect the change.

You won't be able to use static getDerivedStateFromProps because there's no comparison between old and new props, but only comparison to incoming props and what's current in state. That said, you COULD use it if you stored something in state that is directly related to the storage props. An example might be if you stored a key from the props and into state. If the props key was to ever change and it didn't match the current key in state, then it would update the state to reflect the key change. In this case, you would return an object to update state or null to not update the state.

For example:

static getDerivedStateFromProps(props, state) {
  return props.storage.key !== state.storageKey 
  ? { storageKey: props.storage.key }
  : null
}

When it comes to testing, you'll want manually update props to affect state changes. By testing against state changes, you'll indirectly test against componentDidUpdate.

For example (you'll have to mock localStorage, otherwise state won't update -- I'd also recommend exporting the class and importing it, instead of using the Redux connected component):

import { MyComponent } from "../MyComponent.js";

const initialProps = { storageValue: "" };

describe("Test Component", () => {
  let wrapper;
  beforeEach(() => {
    wrapper = mount(<MyComponent { ...initialProps } />);
  })

  it("updates 'newStorage' state when 'storageValue' props have changed, () => {
    wrapper.setProps({ storageValue: "test" });  
    expect(wrapper.state('NewStorage')).toEqual(...); // this should update NewStorage state

    wrapper.setProps({ someOtherProp: "hello" )};
    expect(wrapper.state('NewStorage')).toEqual(...); // this should not update NewStorage state 
  });
});
Matt Carlotta
  • 18,972
  • 4
  • 39
  • 51