1
votes

I am new to React hooks. I'm using a useEffect() hook in a component, and that hook calls a function, searchForVideos() from my props:

useEffect(() => {
  props.searchForVideos();
}, [currentPage]);

That function is mocked in my unit tests using Jest:

const searchForVideos = jest.fn();

So, based on my understanding, useEffect() should run for the first time immediately after component render. I can put a console.log() statement in the useEffect() callback and it does print to the console. However, this expect() statement fails:

const component = mountComponent();
setImmediate(() => {
    expect(searchForVideos).toHaveBeenCalled();
});

This is strange, because I've confirmed the hook is running, and if it is running it should call the function. However, that line always fails.

Is there something I should know about to make the mocked functions work well with React hooks?

Update

In response to a comment, I made a change that fixed the problem. I do not understand why this worked, though.

const component = mountComponent();
requestAnimationFrame(() => {
    expect(searchForVideos).toHaveBeenCalled();
    done();
});

So I just replaced setImmediate() with requestAnimationFrame(), and now everything works. I've never touched requestAnimationFrame() before. My understanding of setImmediate() would be that it basically queues up the callback at the back of the event queue right away, so any other JavaScript tasks in the queue will run before it.

So ideally I'm seeking an explanation about these functions and why this change worked.

1
You should probably wait for an animation frame before expecting your hook to be called. It happens because React have a task queue and your hook is in it, you can't be sure it will be called right after mounting (surely one animation frame after the mounting). - Alexandre Nicolas
How do I wait for one frame? Also I have put a console log statement in the hook and it does print that statement. - user2223059
you make your test function async then use this promise right after mountComponent(): const waitAFrame = () => new Promise(resolve => requestAnimationFrame(resolve)) - Alexandre Nicolas
The post-solution amendment to this question interfered with the original question. I have therefore restored that code, and clarified what part of the question is an update. Please don't amend questions into solutions, as this makes understanding them very difficult for new readers. - halfer

1 Answers

0
votes

As far as your secondary question about why requestAnimationFrame fixed it, see the documentation excerpts below. It just gets into the specific timing of useEffect and useLayoutEffect -- specifically "useEffect is deferred until after the browser has painted". requestAnimationFrame delays your test code in a similar fashion. I would expect that if you changed your code to use useLayoutEffect, the original version of your test would work (but useEffect is the appropriate hook to use for your case).

From https://reactjs.org/docs/hooks-reference.html#timing-of-effects:

Unlike componentDidMount and componentDidUpdate, the function passed to useEffect fires after layout and paint, during a deferred event. This makes it suitable for the many common side effects, like setting up subscriptions and event handlers, because most types of work shouldn’t block the browser from updating the screen.

However, not all effects can be deferred. For example, a DOM mutation that is visible to the user must fire synchronously before the next paint so that the user does not perceive a visual inconsistency. (The distinction is conceptually similar to passive versus active event listeners.) For these types of effects, React provides one additional Hook called useLayoutEffect. It has the same signature as useEffect, and only differs in when it is fired.

Although useEffect is deferred until after the browser has painted, it’s guaranteed to fire before any new renders. React will always flush a previous render’s effects before starting a new update.

From https://reactjs.org/docs/hooks-reference.html#uselayouteffect:

The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.

Prefer the standard useEffect when possible to avoid blocking visual updates.