Enzyme: ReactWrapper::setState() can only be called on the root

Created on 21 Oct 2017  路  19Comments  路  Source: enzymejs/enzyme

Preface

I had went through #814 and #361 , but I still don't get the answer I want.

Explanation

I know you may say that in practice you should only test a child component in isolation, but I'm in a situation where the child component cannot be rendered without the parent component.
For example, because I'm using React Material-UI, I got to mount the element-to-be-tested inside a theme provider, if not error will be thrown during mounting :

const wrapper = mount(
        <MuiThemeProvider>
            <SubjectListView subjects={subjects}/>
        </MuiThemeProvider>);

Problem

I need to set the state of SubjectListView but then I got this ReactWrapper::setState() can only be called on the root error.

Conclusion

I really hope I can call setState() on child element, if not I have to wrap all my components with a <MuiThemeProvider> which is obviously not a good thing.

mount feature request help wanted

Most helpful comment

Anyway, I still think that allowing client to call setState() on child element is a must have in the future.

All 19 comments

You can use shallow, and .dive() - is there a reason you need to use mount?

@ljharb Thanks for the reply. I need to use mount because I have to check on element which are quite deep in the rendered DOM.

Fair.

However I'd say a series of shallow-rendering tests, for each component in your tree, would get you more coverage and be easier to write.

Anyway, I still think that allowing client to call setState() on child element is a must have in the future.

There is another use case I've run into. In the case of a single page app, when rendering links from react-router-dom, it's necessary to nest a component inside of a router. This prevents the test from being able to access the state of the component being tested, because ReactWrapper.state() returns the state of the router component at the root.

Have exactly the same issue with "react-router-dom" as above. Can't test my components due to router wrapper.

The workaround I've used now is to assign the component instance to a ref and use that to get access to the state.

<HashRouter><MyComponent ref={ref => this.componentInstance = ref} /></HashRouter>

Then you can read and manipulate state directly:

this.componentInstance.getState();
this.componentInstance.setState({});

I had a problem with React Router too and solve it by using the dive method.

const getComponent = props =>
  <MemoryRouter>
    <AppHeader {...props} />
  </MemoryRouter>

it('should set the state `isMenuOpened` to `true`', () => {
  const component = shallow(getComponent()).find(AppHeader).dive()
  expect(component.state('isMenuOpened')).toBeFalsy()

  component.find(Toggle).simulate('click')
  expect(component.state('isMenuOpened')).toBeTruthy()
})



Note in the above, you're really testing the AppHeader component without testing the getComponent at all, since component is assigned the value of find(AppHeader).dive().

I have a situation where a third party component does some work and then triggers an onChange method. I'd like to be able to set that third party component state to see the changes in my shallow rendered component. Simplified code:

render() {
  <div>
    <3rdParty onChange={setClass}>
      <some html...>
    </3rdParty>

    <div styleName={getClass()} />
  </div>
}

If I could set state on 3rdParty, I could observe the changes in the rest of the component in a snapshot.

I'd be happy to review a PR that allows setState to be called on non-root custom component instances in mount.

Duplicate of #635.

I found this works to set state of the inner component:

const wrapper = mount(
        <MuiThemeProvider>
            <SubjectListView subjects={subjects}/>
        </MuiThemeProvider>);
wrapper.find(SubjectListView).instance().setState({ foo: bar });
wrapper.update();

@maxcrystal Great solution.

@maxcrystal I was starting to wonder why on earth would it be so hard to do something so simple? Great job 馃憤 馃憤

So how can i set props on non root elements?

You can wait until the updated version is released, and then update to it.

It looks like this merge only fixed setting state on child comps though? Or is there another issue for setProps?

ahh sorry, yes, this is only for setState. It doesn't really make sense to arbitrarily set props on a non-root element - the next render would just wipe that out with the props the parent passed. Happy to discuss this in a new issue, though.

I found this works to set state of the inner component:

const wrapper = mount(
        <MuiThemeProvider>
            <SubjectListView subjects={subjects}/>
        </MuiThemeProvider>);
wrapper.find(SubjectListView).instance().setState({ foo: bar });
wrapper.update();

Thank you! Your solution helped me.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

AdamYahid picture AdamYahid  路  3Comments

ivanbtrujillo picture ivanbtrujillo  路  3Comments

benadamstyles picture benadamstyles  路  3Comments

modemuser picture modemuser  路  3Comments

heikkimu picture heikkimu  路  3Comments