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

Created on 5 Oct 2017  路  31Comments  路  Source: enzymejs/enzyme

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

[email protected]
[email protected]
[email protected]
[email protected]

v3 expected difference

Most helpful comment

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!

All 31 comments

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

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

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

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

I got the same issue here when update state internally.
But if I call setProps() to set any prop(even an irrelevant prop) when initializing the component, the test case will pass as expected.

When I log out the props, state, and rendered components, only rendered components are different depending on if I call setProps in advance or not.

Any thoughts on this?

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

.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()

I am update enzyme-adapter-react-16 to 1.1.0. Run result is OK.I think that is enzyme bug,but fix 1.1.0.
I see the code, The following code fixes to change the problem.
1.1.0 : const node = findCurrentFiberUsingSlowPath(vnode); --fixed
1.0.0 : const node = vnode.alternate !== null ? vnode.alternate : vnode;

Same here.

forceUpdate not working for mount, only shallow.

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.

Well, it _does_ send the new props, but in an unexpected order.

We have a menu component that has child menu items. The menu items can be visible, and have internal state for the visibility, classes, etc. that gets set based on the isVisible prop.

When I setState on the menu to make it visible, I see this sequence:

1) setState sets the internal state of the menu.
2) Menu is rendered, child components are rendered
3) Child components receive new props based on the new state
4) Oh dear, child components did not render based on the new state/props

We found similar out-of-sequence state/prop updates with React 16, around callbacks to setState. It used to be that you could rely on child components getting new props before the callback was called, but now you can't.

(btw, the conclusion that the child components are getting the new props at some point is based on what .debug() says)

@notnownikki React 16 itself makes all of that ordering unreliable; the point is to allow async rendering.

@ljharb so I've been finding out over the past couple of weeks :)

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

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.

@ialexryan it can, if your spies are set up correctly - one common mistake is using arrow functions in class fields, which prevents those functions from being spied upon properly in tests.

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

This is an issue in react 15 adapter as well. I did not require the .update() call in v2 but now that I updated to v3, everything that used to pass fails now. For example:

// my-component.js
class MyComponent extends Component {
  // ...
  handleUpdate = () => this.setState({test: 'updated'});
  render() {
    return (
      <div>
        <OtherComponent
          onUpdate={this.handleUpdate}
          test={this.state.test}
        />
        {/* ... */}
      </div>
    );
  }
}

// my-component.spec.js
rootElement.childAt(0).props().onUpdate();

// used to work in v2, but fails on v3 without the `.update()` call
expect(rootElement.childAt(0).props().test).to.equal('updated');

Mentioning myself I don't loose this issue, and I already did once: @Gopikrishna19

Any updates on this guys?
I'm also facing the same issue on a shallow rendered component.

listComponent = shallow(<ListComponent {...props} store={store} />);
tableComponent = listComponent.find('TableComponent').dive();

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

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

tableComponent     // still returned an array of items of length 3
  .find('Menu')
  .at(0)
  .props().items;

listComponent.instance().forceUpdate();
listComponent.update();

It's having trouble only while rerendering the component. The values are updated in the listComponent.instance().props.

Versions used
"react": "16.3",
"enzyme": "^3.3.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()
  });

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

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

@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

Refind still does not work.

@shaundavin13 please file a new issue if you鈥檙e having trouble.

@shaundavin13 please file a new issue if you鈥檙e having trouble.

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

@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鈥檚 a problem. This issue was asking about something that鈥檚 not a bug, but rather a design choice for v3, so in this thread there鈥檚 nothing to fix.

Did a new issue ever get filed? I reckon I'm too nooberish to properly file it but this issue persists for me and haven't found any solution here. (Edit: OH WAIT, yes I did, see below.)

Nope, but it looks like while 6 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.

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!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

heikkimu picture heikkimu  路  3Comments

abe903 picture abe903  路  3Comments

timhonders picture timhonders  路  3Comments

AdamYahid picture AdamYahid  路  3Comments

ahuth picture ahuth  路  3Comments