admin管理员组文章数量:1302406
Component:
const MyComponent = props => {
const {price} = props;
const result1 = useResult(price);
return (
<div>...</div>
)
}
Custom Hook:
export const useResult = (price) => {
const [result, setResult] = useState([]);
useEffect(() => {
const data = [{price: price}]
setResult(data);
}, [price]);
return result;
};
Jest test:
it('should ...', async () => {
render(
<MyComponent price={300}/>)
)
await waitFor(() => {
expect(...).toBeInTheDocument();
});
});
What it does happen with the above code is that MyComponent
, when running the test, renders only once instead of two (when the application runs). After the initial render where result1
is an empty array, useEffect
of useResult
is running and since there is a state change due to setResult(data)
, I should expect MyComponent
to be re-rendered. However, that's not the case and result1
still equals to []
whereas it should equal to [{price:300}]
.
Hence, it seems custom hooks under testing behave differently than the real app. I thought it would be okay to test them indirectly through the ponent that calls them.
Any explanation/thoughts for the above?
UPDATE
The issue that invoked the above erroneous behaviour was state mutation!! It worked with the app but not with the test! My mistake was to attempt to use push
in order to add an element to an array that was a state variable...
Component:
const MyComponent = props => {
const {price} = props;
const result1 = useResult(price);
return (
<div>...</div>
)
}
Custom Hook:
export const useResult = (price) => {
const [result, setResult] = useState([]);
useEffect(() => {
const data = [{price: price}]
setResult(data);
}, [price]);
return result;
};
Jest test:
it('should ...', async () => {
render(
<MyComponent price={300}/>)
)
await waitFor(() => {
expect(...).toBeInTheDocument();
});
});
What it does happen with the above code is that MyComponent
, when running the test, renders only once instead of two (when the application runs). After the initial render where result1
is an empty array, useEffect
of useResult
is running and since there is a state change due to setResult(data)
, I should expect MyComponent
to be re-rendered. However, that's not the case and result1
still equals to []
whereas it should equal to [{price:300}]
.
Hence, it seems custom hooks under testing behave differently than the real app. I thought it would be okay to test them indirectly through the ponent that calls them.
Any explanation/thoughts for the above?
UPDATE
The issue that invoked the above erroneous behaviour was state mutation!! It worked with the app but not with the test! My mistake was to attempt to use push
in order to add an element to an array that was a state variable...
-
const data = ... //we build an array somehow
- is this a synchronous operation? – Konrad Commented Nov 8, 2022 at 19:14 - Yes. It is a synchronous one. – Unknown developer Commented Nov 8, 2022 at 19:20
- 1 Tests are synchronous, React state updates are not synchronously processed. The test needs to wait for the ponent to rerender with any updated UI you are trying to assert on. – Drew Reese Commented Nov 8, 2022 at 19:52
- Okay. However, what's the answer to my question? When debugging the test, there is no re-rendering of the ponent. Why? – Unknown developer Commented Nov 8, 2022 at 21:28
- 2 What's the goal of your test from a user's point of view? The user won't really care how many times it re-renders. I'd suggest filling out both the code and test a bit more to give us more insights into what you're actually trying to test. – Cathal Mac Donnacha Commented Nov 9, 2022 at 0:15
4 Answers
Reset to default 3Well, it seems that you are asking a very specific thing about testing a custom hook. In that case, I also had some issues in the past testing custom hooks through @testing-library
and a different package was created (and recently incorporated into the @testing-library
) that provides the renderHook()
function for testing custom hooks. I suggest you to test that.
- Original Package (do not use it. Use directly the TL one)
- Docs about the
renderHook()
call inside the TL docs
You can read more about it in this blog post from Kent C. Dodds.
I also suggest you create a "state change" to test your ponent and test the hook with the renderHook()
.
Here is a simple codesandbox with some tests for a ponent similar to your case.
Original Answer
Essentially, your test is not waiting for the ponent to perform the side effects. There are 2 ways of waiting for that:
- Using
waitFor()
import { waitFor, screen } from '@testing-library/react'
// ...
// add the `async` before the callback function
it('should ...', async () => {
render(<MyComponent price={300}/>);
await waitFor(() =>
expect(screen.getByText('your-text-goes-here')).toBeInTheDocument()
)
});
- Using the
findBy*
query from RTL, that returns a Promise (read the Docs here) and is a bination from thewaitFor
andgetBy*
query (read docs here)
import { screen } from '@testing-library/react'
// ...
// add the `async` before the callback function
it('should ...', async () => {
render(<MyComponent price={300}/>);
expect(await screen.findByText('your-text-goes-here')).toBeInTheDocument();
});
Step 1: the code being tested
If, as mentioned in the ments of the question, the operation inside the effect is synchronous, then using useEffect
for setting this state based on the props is undesirable in all cases. Not only for testing.
The ponent will render, update the DOM and immediately need to re render the following frame because it's state was updated. It causes a flash effect for the user and needlessly slows the app down.
If the operation is cheap, it's way more efficient to just execute it on every render.
If the operation can be more expensive, you can wrap it in useMemo
to ensure it only happens when there's changes to the inputs.
export const useResult = (price) => {
return useMemo(
// I assume this is a stub for a expensive operation.
() => [{price: price}],
[price]
);
};
If, for some obscure reason, you do need to do this in an effect anyway (you probably don't but there's edge cases), you can use a layoutEffect
instead. It will be processed synchronously and avoid the flashing frame. Still wouldn't remend it but it's a slight improvement over a regular effect.
Step 2: Testing
If you changed the ponent to not use an effect, it should now be correct from the first render, and you don't have the problem anymore. Avoiding having a problem in the first place is also a valid solution :D
If you do find the need to flush something synchronously in a test, there's now the flushSync
function which does just that.
Perhaps it would also flush the state update in the effect, causing your test to work with no other changes. I guess it should, as new updates triggered by effects while flushing should continue to be processed before returning.
flushSync(() => {
render(
<MyComponent price={300}/>)
)
})
In any case there's no point doing this if you can instead improve the ponent to fix the additional render introduced by setting state in an effect.
you can do:
The test will have to be async: it('should ...', async() => { ....
await screen.findByText('whatever');
This is async so it will wait to find whatever and fail if it can't find it
or you can do
await waitFor (() => {
const whatever = screen.getByText('whatever');
expect(whatever).toBeInTheDocument();
})
You are not waiting for the ponent to be rerendered
import { waitFor, screen } from 'testing-library/react'
it('should ...', async () => {
render(
<MyComponent price={300}/>)
)
await waitFor (() => {
// check that props.price is shown
screen.debug() // check what's renderered
expect(screen.getByText(300)).toBeInTheDocument();
});
});
本文标签: javascriptReact component does not rerender under Jest on state changeStack Overflow
版权声明:本文标题:javascript - React component does not re-render under Jest on state change - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741673679a2391752.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论