-
Notifications
You must be signed in to change notification settings - Fork 47.2k
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
Provide a way to trigger useEffect from tests #14050
Comments
Try using I think this is intentional behaviour, as far as I understand, EDIT: typo'd |
Thanks @arianon. The same issue exists for 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 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/ |
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 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 const Comp = () => {
useEffect(() => console.log('effect'));
return null;
}
const tree = renderer.create(<Comp />); // nothing logged
tree.update(); // console.log => 'effect' |
Gave up on testing useEffect: facebook/react#14050
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
|
Just to clarify and bookend this, the recommend solution is to use |
@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 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 The main scenario I'm unsure about is this: Sure, I wrap any events I trigger in my test in I can illustrate this with a 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
The odd part is that in my codebase, which has more convoluted tests that are not practical to share, the situation is different:
Any 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 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
|
No. it has to be at least within the next 'tick', ie a macrotask on the js task queue, as soon as (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): // 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:
This is how I will emulate the resize: 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()
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? |
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:
And here's a minimal reproducing repo: https://github.com/skidding/react-test-useeffect
The text was updated successfully, but these errors were encountered: