I'm experiencing with the whole React and I have a hard time clearly understanding the issue I have right now in one of my Unit Test. I committed (and pushed) my whole project so it will be easier to get all the necessary details to understand the problem.
But basically, I can't make any assertion on updated DOM elements as it keep giving me the original DOM.
Here's the Unit Test :
jest.unmock('../display.js');
import React from 'react';
import ReactDOM from 'react-dom';
import sinon from 'sinon';
import TestUtils from 'react-addons-test-utils';
import EmoticonDisplay from "../display.js";
describe('Selector', () => {
var component, renderedDOM;
beforeEach(() => {
renderComponent(true);
});
afterEach(() => {});
it('is not displayed if IsLoggedIn returns false', () => {
expect(renderedDOM()).not.toBe(null);
//Why do I need to re-render? If I change the value of isLoggedIn and I try to access the DOM
//it is still the same value as before
renderComponent(false);
expect(renderedDOM()).toBe(null);
});
it('sets default states', () => {
expect(component.state.stack).toEqual([]);
expect(component.state.isAnimating).toEqual(false);
expect(component.state.currentItem).toEqual({emote: 'none', username: ''});
expect(TestUtils.findRenderedDOMComponentWithTag(component, 'img').src).toBe('images/none.svg');
});
it('animates a newly received emoticon', () => {
var data = {emote:'cool', username:'swilson'};
component.animate(data);
expect(component.state.stack).toEqual(jasmine.objectContaining(data));
//Same potential issue as explained earlier. Altough everything I find online tells me I should get the
//updated DOM, I still have images/none.svg even though the code changes the source of the picture
console.log(TestUtils.scryRenderedDOMComponentsWithTag(component, 'img')[0].src);
});
function renderComponent(isLoggedIn) {
component = TestUtils.renderIntoDocument(<EmoticonDisplay isLoggedIn={ isLoggedIn } />);
renderedDOM = () => ReactDOM.findDOMNode(component);
}
});
File in repo : https://github.com/jprivard/scrum-companion/blob/9436903050697be3eb15cb31075e832980eb0b9f/app/modules/emoting/tests/display.test.js
Somehow, when I look for an answer online, I can see the exact same code running and working. And some other times, I see other ways to do it, but it seems incompatible with my current version of React/Jest/Jasmine/...
Please help :)
Related
I have a counter and a console.log() in an useEffect to log every change in my state, but the useEffect is getting called two times on mount. I am using React 18. Here is a CodeSandbox of my project and the code below:
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
useEffect(() => {
console.log("rendered", count);
}, [count]);
return (
<div>
<h1> Counter </h1>
<div> {count} </div>
<button onClick={() => setCount(count + 1)}> click to increase </button>
</div>
);
};
export default Counter;
useEffect being called twice on mount is normal since React 18 when you are in development with StrictMode. Here is an overview of what they say in the documentation:
In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React will support remounting trees using the same component state used before unmounting.
This feature will give React better performance out-of-the-box, but requires components to be resilient to effects being mounted and destroyed multiple times. Most effects will work without any changes, but some effects do not properly clean up subscriptions in the destroy callback, or implicitly assume they are only mounted or destroyed once.
To help surface these issues, React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.
This only applies to development mode, production behavior is unchanged.
It seems weird, but in the end, it's so we write better React code, bug-free, aligned with current guidelines, and compatible with future versions, by caching HTTP requests, and using the cleanup function whenever having two calls is an issue. Here is an example:
/* Having a setInterval inside an useEffect: */
import { useEffect, useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000);
/*
Make sure I clear the interval when the component is unmounted,
otherwise, I get weird behavior with StrictMode,
helps prevent memory leak issues.
*/
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
export default Counter;
In this very detailed article called Synchronizing with Effects, React team explains useEffect as never before and says about an example:
This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development.
For your specific use case, you can leave it as it's without any concern. And you shouldn't try to use those technics with useRef and if statements in useEffect to make it fire once, or remove StrictMode, because as you can read on the documentation:
React intentionally remounts your components in development to help you find bugs. The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”.
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).
/* As a second example, an API call inside an useEffect with fetch: */
useEffect(() => {
const abortController = new AbortController();
const fetchUser = async () => {
try {
const res = await fetch("/api/user/", {
signal: abortController.signal,
});
const data = await res.json();
} catch (error) {
if (error.name !== "AbortError") {
/* Logic for non-aborted error handling goes here. */
}
}
};
fetchUser();
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application.
In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned... So even though there is an extra request, it won’t affect the state thanks to the abort.
In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:
function TodoList() {
const todos = useSomeDataFetchingLibraryWithCache(`/api/user/${userId}/todos`);
// ...
Update: Looking back at this post, slightly wiser, please do not do this.
Use a ref or make a custom hook without one.
import type { DependencyList, EffectCallback } from 'react';
import { useEffect } from 'react';
const useClassicEffect = import.meta.env.PROD
? useEffect
: (effect: EffectCallback, deps?: DependencyList) => {
useEffect(() => {
let subscribed = true;
let unsub: void | (() => void);
queueMicrotask(() => {
if (subscribed) {
unsub = effect();
}
});
return () => {
subscribed = false;
unsub?.();
};
}, deps);
};
export default useClassicEffect;
I'm using React, Redux, Redux-Saga, and Jest with React Testing Library.
I'm writing tests for a component based on the Guiding Principles listed on the Redux site.
I have test utilities set up with a reusable test render function also as noted in the Redux docs with the notable exception that, due to current limitations in the codebase, I can not re-create a new store between each test.
Instead, between each test, I just reset the state via reducers.
I have two issues:
I don't see the useEffect unmount called during my test
Redux store is trying to update an unmounted component
My component (simplified for repro) looks like this:
export function SearchResultDisplayer(): JSX.Element {
const searchState = useSelector(data.search.getSearchState);
const onSearchRequest = data.interactions.search.setSearchResults;
React.useEffect(() => {
console.log('SRD Effect');
return () => {
console.log('SRD Unmount');
};
}, []);
function handleClick() {
onSearchRequest({});
}
return (
<>
<div>{Object.values(searchState.results).map((result) => result.id)}</div>
<button
type="button"
onClick={handleClick}
>
Click Me!
</button>
</>
);
}
My tests look like this:
test('test 1', () => {
const { unmount } = render(<SearchResultDisplayer />);
unmount();
cleanup();
});
test('test 2', async () => {
const { getByRole } = render(<SearchResultDisplayer />);
const button = await getByRole('button');
await fireEvent.click(button);
});
I expect to see, during the first test, the console.log('SRD Unmount') - but this doesn't seem to be called (it is called when I test in the browser).
Additionally, during the second test, when I await fireEvent.click(button) I receive the react error:
Warning: Can't perform a React state update on an unmounted component...
which seems to be related to the Redux store trying to update the component from the first test - the error does not show up if I only run the 2nd test. Additionally, this happens regardless of whether the useEffect is present or not - I only added that to try to confirm the component was getting unmounted.
So from the first test, the component is obviously unmounted, given the error - but...
why don't I see the console log during unmount?
why is the Redux Store still trying to perform a state update on that component? (and how can I prevent that in the test?)
Can't find documentation about this anywhere. Will this cause the useEffect to EVER run again? I don't want it to fetch twice, that would cause some issues in my code.
import React, { useEffect } from 'react'
import { useHistory } from 'react-router-dom'
const myComponent = () => {
const { push } = useHistory();
useEffect( () => {
console.log(" THIS SHOULD RUN ONLY ONCE ");
fetch(/*something*/)
.then( () => push('/login') );
}, [push]);
return <p> Hello, World! </p>
}
From testing, it doesn't ever run twice. Is there a case that it would?
For the sake of the question, assume that the component's parent is rerendering often, and so this component is as well. The push function doesn't seem to change between renders - will it ever?
Ciao, the way you write useEffect is absolutely right. And useEffect will be not triggered an infinite number of time. As you said, push function doesn't change between renders.
So you correctly added push on useEffect deps list in order to be called after fetch request. I can't see any error in your code.
I've got the following component (simplified) which, given a note ID, would load and display it. It would load the note in useEffect and, when a different note is loaded or when the component gets unmounted, it saves the note.
const NoteViewer = (props) => {
const [note, setNote] = useState({ title: '', hasChanged: false });
useEffect(() => {
const note = loadNote(props.noteId);
setNote(note);
return () => {
if (note.hasChanged) saveNote(note); // bug!!
}
}, [props.noteId]);
const onNoteChange = (event) => {
setNote({ ...note, title: event.target.value, hasChanged: true });
}
return (
<input value={note.title} onChange={onNoteChange}/>
);
}
The issue is that within the useEffect I use note, which is not part of the dependencies so it means I always get stale data.
However, if I put the note in the dependencies then the loading and saving code will be executed whenever the note is modified, which is not what I need.
So I'm wondering how can I access the current note, without making it a dependency? I've tried to replace the note with a ref, but it means the component no longer updates when the note is changed, and I'd rather not use references.
Any idea what would be the best way to achieve this? Maybe some special React Hooks pattern?
You can't get the current state because this component does not render on the app render that removes it. Which means your effect never runs that last time.
Using an effect cleanup function is not a good place for this sort of thing. That should really be reserved for cleaning up that effect and nothing else.
Instead, whatever logic you have in the app that changes the state to close the NoteViewer should also save the note. So in some parent component (perhaps a NoteList or something) you'd save and close like:
function NoteList() {
const [viewingNoteId, setViewingNoteId] = useState(null)
// other stuff...
function closeNote() {
if (note.hasChanged) saveNote(note)
setViewingNoteId(null)
}
return <>{/* ... */}</>
}
I have a method which uses an ElementRef which is defined below.
#ViewChild('idNaicsRef') idNaicsRef: ElementRef;
ElementRef then sets the focus using .nativeElement.focus().
The method fails while running the spec, saying 'undefined is an object'
Although httpNick's answer should work, I ended up asking an architect on my team about this and he led me to a slightly different solution that may be a bit simpler.
describe(MyComponent.name, () => {
let comp: MyComponent;
describe('myFunction', () => {
it('calls focus', () => {
comp.idNaicsRef = {
nativeElement: jasmine.createSpyObj('nativeElement', ['focus'])
}
comp.myFunction();
expect(comp.idNaicsRef.nativeElement.focus).toHaveBeenCalled();
});
});
This particular example would just test to see if the focus method has been called or not. That's the test that I was interested in when I was testing my method, but you could of course test whatever you wanted. The key is the setup beforehand (which was elusive before it was shown to me).
this should work. this just creates a spy object and then you can populate it with whatever you want, so you could even check if it was called in your unit test.
import createSpyObj = jasmine.createSpyObj;
comp.idNaicsRef = createSpyObj('idNaicsRef', ['nativeElement']);
comp.idNaicsRef.nativeElement = { focus: () => { }};
comp is the reference to the component you are testing.
createSpyObj comes from a jasmine import