Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mount/shallow does not rerender when props change or apply new props on update #1229

Closed
elyobo opened this issue Oct 5, 2017 · 35 comments
Closed

Comments

@elyobo
Copy link

elyobo commented Oct 5, 2017

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

@marchaos
Copy link

marchaos commented Oct 5, 2017

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

@adrienDog
Copy link

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

@elyobo
Copy link
Author

elyobo commented Oct 6, 2017

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

@rodgobbi
Copy link

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

@rodgobbi
Copy link

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

@Namekians
Copy link

Namekians commented Oct 27, 2017

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?

@guigonc
Copy link

guigonc commented Nov 13, 2017

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

@chrisregner
Copy link

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

@zenjava
Copy link

zenjava commented Nov 30, 2017

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;

@Diokuz
Copy link

Diokuz commented Dec 6, 2017

Same here.

forceUpdate not working for mount, only shallow.

@notnownikki
Copy link

notnownikki commented Dec 8, 2017

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.

@notnownikki
Copy link

notnownikki commented Dec 8, 2017

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)

@ljharb
Copy link
Member

ljharb commented Dec 9, 2017

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

@notnownikki
Copy link

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

@heronrs
Copy link

heronrs commented Jan 3, 2018

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

@ialexryan
Copy link
Contributor

ialexryan commented Jan 26, 2018

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.

@ljharb
Copy link
Member

ljharb commented Jan 27, 2018

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

@MarcoNicolodi
Copy link

MarcoNicolodi commented Mar 12, 2018

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

@Gopikrishna19
Copy link

Gopikrishna19 commented Apr 18, 2018

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

@sudheeshcm

This comment has been minimized.

@sudheeshcm

This comment has been minimized.

@ljharb
Copy link
Member

ljharb commented Jun 26, 2018

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

@ljharb ljharb closed this as completed Jun 26, 2018
@wwahammy
Copy link

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

@ljharb
Copy link
Member

ljharb commented Aug 16, 2018

@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

@shaundavin13
Copy link

Refind still does not work.

@ljharb
Copy link
Member

ljharb commented Aug 20, 2018

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

@DorianWScout24
Copy link

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

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

@ljharb
Copy link
Member

ljharb commented Dec 7, 2018

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

@mikelyons
Copy link

mikelyons commented Feb 11, 2019

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

@ljharb
Copy link
Member

ljharb commented Feb 11, 2019

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.

@mikelyons
Copy link

mikelyons commented Feb 11, 2019

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: #1221 (comment) 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!

@ZeCarlosCoutinho
Copy link

ZeCarlosCoutinho commented Jun 11, 2021

From the tests I've been doing, it seems that you have to first .update() and then re-find the wrapper.
Just doing the .find() does update the props of the component and all its children, but does not seem to trigger a rerender

@Stompke
Copy link

Stompke commented Aug 11, 2021

The order of when you get find/get the thing matters!!!!
You need to grab it after the change of state.

@johnflux
Copy link

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

@victorAttar
Copy link

setTimeout(() => {
wrapper.update();
mydialog = wrapper.find(MyDialog);
done();
}, 0);

Worked for me, but it is kind bad need to use setTimeout just because have redux

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests