Hello,
I tried testing components that use the cool new hooks API, but useEffect
doesn't seem to work with the test renderer.
Here's a small failling Jest test:
import React, { useEffect } from "react";
import { create as render } from "react-test-renderer";
it("calls effect", () => {
return new Promise(resolve => {
render(<EffectfulComponent effect={resolve} />);
});
});
function EffectfulComponent({ effect }) {
useEffect(effect);
return null;
}
And here's a minimal reproducing repo: https://github.com/skidding/react-test-useeffect
Note that other _use_ APIs seemed to work (eg.
useContext
).
Try using useLayoutEffect
like how currently the only Hook test executing on react-test-renderer
is doing it
I think this is intentional behaviour, as far as I understand, useEffect
executes after paint, but react-test-renderer
never paints, because it doesn't use a DOM at all.
EDIT: typo'd useLayoutEffect
as asLayouteffect
:smile:
Thanks @arianon. asLayoutEffect
works. But I want to use useEffect
.
The same issue exists for react-dom
. Here's another test
import React, { useEffect } from "react";
import { render } from "react-dom";
it("calls effect", () => {
const container = document.body.appendChild(document.createElement("div"));
return new Promise(resolve => {
render(<EffectfulComponent effect={resolve} />, container);
});
});
function EffectfulComponent({ effect }) {
useEffect(effect);
return null;
}
Maybe the "passive" (post-paint) hooks don't work inside JSDOM?
We should have something to trigger them.
Thanks for updating the issue.
What about the fact that useEffect isn't triggered also when I render using react-dom? Or should I use a different API for rendering?
Heh, I haven't been cut by the bleeding edge for years, but this one got me.
@skidding My guess is that we'll just have to wait.
Working on a workaround for react-testing-library
(which uses ReactDOM) here: https://github.com/kentcdodds/react-testing-library/pull/216
It seems like you can trigger the effects by either rerendering the element in place, or rendering another root somewhere else in the document (even a detached node).
I am not sure why the hooks don't called in the first place though, since requestAnimationFrame/~requestIdleCallback~ are both available in the Jest/JSDOM environment.
TIP: until it will be fixed in library I fixed my tests by mocking useEffect to return useLayoutEffect just in tests.
I have own useEffect module where is just
// file app/hooks/useEffect.js
export {useEffect} from 'react';
then in my component I use
// file app/component/Component.js
import {useEffect} from '../hooks/useEffect';
...
and then in the component test I mock it like following
// file app/component/Component.test.js
jest.mock('../hooks/useEffect', () => {
return { useEffect: require('react').useLayoutEffect };
});
...
I think you don't need such a hassle for this. Jest would automock if you create a file <root>/__mocks__/react.js
and in there you can just...
const React = require.actual('react')
module.exports = { ...React, useEffect: React.useLayoutEffect }
This is a great workaround as you don't need to touch any code when this is somehow fixed, you will just remove the mock.
@gaearon useEffect is triggered after state change. Why it's not true for initial render?
It's triggered for initial render after the browser is idle.
The only reason next updates trigger it is because we flush passive effects before committing the next render. This is important to avoid inconsistencies.
So as a result for every Nth render, you'll see N-1th passive effects flushed.
We'll likely offer a way to flush them on demand too.
You can manually run tree.update()
and effect hooks will be ran. Example:
const Comp = () => {
useEffect(() => console.log('effect'));
return null;
}
const tree = renderer.create(<Comp />); // nothing logged
tree.update(); // console.log => 'effect'
I don't know if that's the best path but mocking both react and react-test-renderer like below solved my problems:
// <Root>/__mocks__/react.js
let React = require("react");
module.exports = {
...React,
useState: initialState => {
let [state, setState] = React.useState(initialState);
return [
state,
update => {
require("react-test-renderer").act(() => {
setState(update);
});
}
];
}
};
// <Root>/__mocks__/react-test-renderer.js
let TestRenderer = require("react-test-renderer");
module.exports = {
...TestRenderer,
create: next => {
let ctx;
TestRenderer.act(() => {
ctx = TestRenderer.create(next);
});
return ctx;
}
};
I wrote a longish doc on how to use .act() to flush effects and batch user and api interactions with React. https://github.com/threepointone/react-act-examples
@threepointone, awesome documentation! Do you see any problem in wrapping setState with act like I've demonstrated in the example above?
@malbernaz I would NOT recommend wrapping every setState like you did. The Act warning is useful for surfacing bugs, and your hack is equivalent to silencing it.
@blainekasten your approach didn't work because the update
function requires a param.
@FredyC's approach didn't work for me - I received TypeError: require.actual is not a function
I did get it working with act()
import * as React from 'react'
import { act, create } from 'react-test-renderer'
import NoSSR from './NoSSR'
describe('NoSSR', () => {
it('renders correctly', () => {
let wrapper: any
act(() => {
wrapper = create(<NoSSR>something</NoSSR>)
})
const x = wrapper.toJSON()
expect(x).toMatchSnapshot()
})
})
Just to clarify and bookend this, the recommend solution is to use act()
in tests to flush effects and updates. We also just released an alpha that includes an async version of act()
for situations that involve promises and the like. If there are no objections, I'll close this issue soon.
@threepointone Thanks for taking care of this, I appreciate your efforts in building and communicating this API!
You can _probably_ close it, but I'll try to share my experience while we're at it since I was the one who reported this issue in the first place.
I first tried Hooks in October, but because I couldn't write tests for components using them I hit a wall and returned to using classes for the time being.
About a month ago, when the new act
API became available I resumed my efforts and managed to properly test my components, so I'm all _hooked_ now!
But while I did manage to make my tests work, there was a bit of trial and error involved and I'm not sure I'm calling act
in the right places or calling it more times than necessary.
The main scenario I'm unsure about is this: Sure, I wrap any events I trigger in my test in React.act
, but what if those events interact with my component asynchronously?
I can illustrate this with a window.postMessage
example:
import React from 'react';
function YayOrNay() {
const [yay, setYay] = React.useState(false);
React.useEffect(() => {
function onMessage() {
setYay(true);
}
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
});
return yay ? 'yay' : 'nay';
}
And here's my attempt to test this:
import ReactTestRenderer from 'react-test-renderer';
import retry from '@skidding/async-retry';
it('should yay, async () => {
// I avoid this kind of hoisting in my tests but let's ignore for now
let renderer;
// act() isn't really useful here, because the message handler isn't called until the
// next event loop
ReactTestRenderer.act(() => {
renderer = ReactTestRenderer.create(<YayOrNay />);
window.postMessage({}, '*');
});
await retry(() => {
expect(renderer.toJSON()).toEqual('yay');
});
});
This test passes but I get the not-wrapped-in-act-update warning
An update the YayOrNay inside a test was not wrapped in act(...).
The odd part is that in my codebase, which has more convoluted tests that are not practical to share, the situation is different:
act()
eitherjsx
parent.postMessage(msg, '*');
setTimeout(() => { act(() => {}); });
retry
blocksAny guidance would be greatly appreciated!
@skidding async act (in 16.9.0-apha.0) will be your friend here. I could be certain if you had a git repo where I could mess with this example, but I think this should solve your problem
await ReactTestRenderer.act(async () => {
renderer = ReactTestRenderer.create(<YayOrNay />);
window.postMessage({}, '*');
});
I'm not sure what retry
does, but the broad principle applies - wrap blocks where the update could occur with await act(async () => ...)
and the warnings should disappear. so -
await ReactTestRenderer.act(async () => {
parent.postMessage(msg, '*');
})
should work. feel free to reach out if it doesn't.
I'll close this once we ship 16.9.0.
Ohh, so ReactTestRenderer.act
will wait until updates are scheduled even if I don't explicitly await
inside the callback? That would be sweet but it also raises some questions, like _does it wait until at least one update has been scheduled?_ If the question doesn't make sense feel free to ignore. I'll come back with a repro repo if problems persist.
16.9.0-apha.0
sounds great, I'll give it a try!
retry
just calls the callback until it doesn't throw -- something I find quite useful in async tests, because once the callback "passes" or times out you get the rich output from the assertion library used inside
does it wait until at least one update has been scheduled?
No. it has to be at least within the next 'tick', ie a macrotask on the js task queue, as soon as act()
exits (Or within the act() callback scope itself). I hope my explainer doc makes this clearer next week.
(Bonus: it will also recursively flush effects/updates until the queue is empty, so you don't miss any hanging effects/updates)
16.9 got released, including async act, and updated documentation https://reactjs.org/blog/2019/08/08/react-v16.9.0.html Closing this, cheers.
@threepointone I am trying to understand how async act fixes it. Here is an example of component I want to test. It should listen to size changes of the screen:
const withScreenSize = Comp => ({ ...props}) => {
const [ size, setSize ] = React.useState(Dimensions.get('window'))
React.useEffect(() => {
const sizeListener = () => {
const { width, height } = Dimensions.get('window')
setSize({ width, height })
}
Dimensions.addEventListener('change', sizeListener)
return () => Dimensions.removeEventListener('change', sizeListener)
}, [setSize])
return <Comp screenSize={size} {...props} />
To test it, I mock the Dimensions object (in React Native):
```javascript
// Mock Dimensions object to emulate a resize
const listeners = []
const oldDimensions = {...Dimensions}
Dimensions.addEventListener = (type, listener) => type==='change' && listeners.push(listener)
Dimensions.removeEventListener = (type, listener) => type==='change' && listeners.splice(listeners.indexOf(listener), 1)
Dimensions.get = () => ({ width, height })
Now I am trying to test the following:
- The resizeListener is correctly added on mount
- The resizeListener is correctly removed on unmount
- The new screen size is taken into account and the component's state is updated
This is how I will emulate the resize:
```javascript
function resizeScreen() {
Dimensions.get = () => ({ width: 200, height: 200 })
listeners.forEach(listener => listener())
}
While trying, I encountered so many weird errors that I don't understand...
const wrapper = TestRenderer.create(<Comp />);
resizeScreen()
wrapper.update()
const updatedView = wrapper.root.findByType('View');
// When using update with empty parameter, I get `Can't access .root on unmounted test renderer`. Though it is what appears in the code of @blainekasten above: tree.update()
await act(async () => {
const wrapper = TestRenderer.create(<Comp />);
const view = wrapper.root.findByType('View');
// Here I get: "Can't access .root on unmounted test renderer" directly on the line above.
In the end, this is how I got it to work:
const wrapper = TestRenderer.create(<Comp />);
const view = wrapper.root.findByType('View');
await act(async () => {})
resizeScreen()
Is this how I am supposed to do it?
Most helpful comment
We should have something to trigger them.