enzyme: mount/shallow does not rerender when props change or apply new props on update

With Enzyme v2 you could mount/shallow render a component with some props and if those props changed then the component would be updated appropriately. With v3 even when you explicitly call .update() it fails to apply the updated version of the props.

Two example tests are shown below; the first would work in v2, but v3 requires explicitly calling .setProps to force it to update. Is this expected behaviour in v3? I assume it’s a consequence of the new adapters, but I couldn’t see anywhere in the migration guide that it was mentioned.

import React from 'react';                                                      
import { shallow } from 'enzyme';                                               
import { describe, it } from 'mocha';                                           
import { expect } from 'chai';                                                  
                                                                                
import enzyme from 'enzyme';                                                    
import Adapter from 'enzyme-adapter-react-16';                                  
enzyme.configure({ adapter: new Adapter() });                                   
                                                                                
class Thing extends React.Component {                                           
  render() {                                                                    
    return (                                                                    
      <div>                                                                     
        <button onClick={this.props.onClick}>Click me</button>                  
        <ul>                                                                    
          {this.props.things.map(id => <li key={id}>{id}</li>)}                 
        </ul>                                                                   
      </div>                                                                    
    );                                                                          
  }                                                                             
}                                                                               
                                                                                
describe('<Thing>', () => {                                                     
  it('updates the things FAIL', () => {                                         
    const things = [];                                                          
    const onClick = () => things.push(things.length);                           
                                                                                
    const wrapper = shallow(<Thing things={things} onClick={onClick} />);       
    expect(wrapper.find('li')).to.have.length(0);                               
                                                                                
    wrapper.find('button').simulate('click');                                   
                                                                                
    // Does not reapply props?                                                  
    wrapper.update();                                                           
                                                                                
    expect(things).to.have.length(1);  // things has been modified correctly                                           
    expect(wrapper.find('li')).to.have.length(1); // but the change is not reflected here                              
  });                                                                           
                                                                                
  it('updates the things OK', () => {                                           
    const things = [];                                                          
    const onClick = () => things.push(things.length);                           
                                                                                
    const wrapper = shallow(<Thing things={things} onClick={onClick} />);       
    expect(wrapper.find('li')).to.have.length(0);                               
                                                                                
    wrapper.find('button').simulate('click');                                   
                                                                                
    // Forcing new things to be applied does work                               
    wrapper.setProps({ things });                                               
                                                                                
    expect(things).to.have.length(1);                                           
    expect(wrapper.find('li')).to.have.length(1); // this time the change is correctly reflected                              
  });                                                                           
});                                                                             

Package versions

react@16.0.0 react-dom@16.0.0 enzyme@3.1.0 enzyme-adapter-react-16@1.0.1

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 73
  • Comments: 35 (8 by maintainers)

Most upvoted comments

I’ve discovered the solution for me, hope to help others but they may have another issue:

const wrapper = mount(<InputArea/>)
const input = wrapper.find('input')
input.simulate('change', {target: { value: 'Foo' } })

expect(input.props().value).to.equal('Foo') // fails with value: ''

will not work as I was trying to check a found child of the wrapper which is immutable. Per this thread: https://github.com/airbnb/enzyme/issues/1221#issuecomment-334974967 You have to find the child again to get the new instance with the new props. This works instead:

const wrapper = mount(<InputArea/>)
const input = wrapper.find('input')
input.simulate('change', {target: { value: 'Foo' } })

expect(wrapper.state('text')).to.equal('Foo')
// need to re-find the input cause the original is immutable in Enzyme V3
const refoundInput = wrapper.find('input')
expect(refoundInput.props().value).to.equal('Foo') // passes with value: 'Foo'

@ljharb for some reason I didn’t understand what you meant in this thread earlier, but the linked comment is more clear about immutability/re-finding, thanks!

This was an intentional design choice in v3. I’m going to close this; please file new issues for actionable concerns.

Workaround for now is to use wrapper.update() for me http://airbnb.io/enzyme/docs/api/ShallowWrapper/update.html

.render() doesn’t seem to be called when calling .update(). Mine worked only when I called both of these in order:

wrapper.instance().forceUpdate()
wrapper.update()

@adrienDog as noted in the issue description and reproduction code, .update() does not work 😕

@adrienDog it’s recommended to use immutable data with react. In your example you’re pushing data into the array, so it’s reference continues the same and for the component receiving things it hasn’t changed. Try using:

let thing  
const onClick = () => {
  things = [...things, things.length]
}

So this way things reference will change, thus changing the props, thus triggering the update.

I have the same issue, but this seems to also occur when updating state internally in the Component.

@DorianWScout24 because that ensures it gets the issue template filled out, and it avoids pinging all the people on this thread with a potentially different problem, and it helps the maintainers (hi) properly triage, categorize, and pay attention to the fact that there’s a problem. This issue was asking about something that’s not a bug, but rather a design choice for v3, so in this thread there’s nothing to fix.

@marchaos @adrienDog calling wrapper.update() didn’t work for me but calling wrapper.instance().forceUpdate() worked.

I’m seeing this, or something very similar too. Using enzyme 3.1.0, react 16.2.0, enzyme-adapter-react-16 1.0.4, and setState on a component, or simulating a click on a button which causes an internal setState, does not send new props to child components.

@shaundavin13 please file a new issue if you’re having trouble.

No matter which order you set state, change state value, update, or force update, child components never get the props right

wrapper.instance().state.isLoadingCategories = false;
wrapper.setState({ isLoadingCategories: false });
wrapper.instance().forceUpdate();
wrapper.update();

//false as expected
expect(wrapper.state('isLoadingCategories')).toEqual(false);
//category field isLoading prop derives from wrapper isLoadingCategories state, but its props is aways true (initial)
expect(categoryField.props().isLoading).toEqual(false);

Its a shallow wrapper, by the way

@ericschultz generally, you need to always re-find from the root to get updates, and you may need to use wrapper.update() before doing that. The migration guide talks about that here: https://github.com/airbnb/enzyme/blob/master/docs/guides/migration-from-2-to-3.md#element-referential-identity-is-no-longer-preserved

@ljharb As this is an intentional design choice, is there a migration path or explanation on how to do what folks on this thread want? I’m happy to change my tests but based on the thread, I still have no idea what to change it to.

What seemed to work for me was adding a unmount immediatly after a mount on beforeEach

"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"react-test-renderer": "^16.2.0"
"react-dom": "^16.2.0",
"react": "^16.2.0",
let wrapper;

beforeEach(() => {
  wrapper = mount(
    <MemoryRouter>
      <App />
    </MemoryRouter>,
  );
  wrapper.unmount();
});

describe('routing paths work as expected', () => {
  it('root page should render SearchTicket component', () => {
    wrapper.setProps({ initialEntries: ['/'] });
    console.log(wrapper.props());
    expect(wrapper.find(DetailTicket)).toHaveLength(0); // works
    expect(wrapper.find(SearchTicket)).toHaveLength(1);
  });

  it('detail page should render DetailTicket component', () => {
    wrapper.setProps({ initialEntries: ['/detail/05'] });
    console.log(wrapper.props());
    expect(wrapper.find(SearchTicket)).toHaveLength(0); // works
    expect(wrapper.find(DetailTicket)).toHaveLength(1);
  });
});

Same here.

forceUpdate not working for mount, only shallow.

This is surprising to me…if one of my tests calls method _x on a shallow-rendered component, and _x itself calls setState, I would expect that that test can assert that spies called by componentDidUpdate have been called (unless I have disableLifecycleMethods on).

Instead, it seems that’s only true if I call wrapper.setState directly from my test.

Refind still does not work.

Nope, but it looks like while ~6~ 21 people are unhappy about my suggestion about how to get their issue fixed, none of them have been motivated enough to do anything about it.

@shaundavin13 please file a new issue if you’re having trouble.

Why should we open a new issue if this is still not solved?

I had to use a timeout to get the Provider to update:

beforeEach((done) => {
     wrapper = mount(
        <Provider store={store}>
          <MyDialog />
        </Provider>,
      );
          wrapper.find('form');.simulate('submit', { preventDefault: () => {} });
          setTimeout(() => {
            wrapper.update();
            mydialog = wrapper.find(MyDialog);
            done();
          }, 0);
});

I just solved my problem. Diving in the listComponent prevented it from rerendering it. The code worked when I removed the code to dive to tableComponent.

listComponent = shallow(<ListComponent {...props} store={store} />);

listComponent        // returned an array of items of length 3
  .find('TableComponent')
  .dive() 
  .find('Menu')
  .at(0)
  .props().items;

listComponent.setProps({ isEmpty: true } });

listComponent        // returned an array of items of length 0 as expected
  .find('TableComponent')
  .dive() 
  .find('Menu')
  .at(0)
  .props().items;

No need of the listComponent.update() or other calls. By the way, the shallow rendering and dive code was written in the beforeEach segment.

beforeEach(() => {
    listComponent = shallow(<ListComponent {...props} store={store} />);
    // ***Deleted the below line and everything worked*** 
    // tableComponent = listComponent.find('TableComponent').dive()
  });

@adrienDog probably you’ll need to pass the new props anyway, that’s the correct way. In the first example the wrapper is updating the component with the old props.