React JS: Why is my cleanup function triggering on render? [duplicate] - javascript

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;

Related

React Warning: Cannot update during an existing state transition in UseEffect

In React App, I am triggering API request / data load in useEffect() in the screen which shows loading indicator.
export default function LoadingScreen({ onLoaded}: {
onLoaded: (result: Result) => void;
}) {
useEffect(() => {
const getProductByCode = async () => {
const result = await getProduct();
onLoaded(result)
};
getProductByCode().catch(console.error);
});
return (…)
And when data is loaded, trying to switch the state here:
<LoadingScreen onLoaded={(result) => dispatch({ type: 'success', result })} />
It kind of works but I get this warning and I think it is not ok still
Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
Can someone give me a bit more explanation on what is happening here (why warning is coming)? I thought useEffect() is called after rendering is done so invoking async dispatch after server response should be very much ok, no?
P.S. Added call stack. The line mentioned is const history = useHistory(); which does not make sense to me.
index-057ed940.js:1246 Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
at App (https://localhost:8080/dist/App.js:69:19)
at Router (https://localhost:8080/_snowpack/pkg/react-router-dom.js:1683:30)
at BrowserRouter (https://localhost:8080/_snowpack/pkg/react-router-dom.js:2409:35)
at default
at InnerThemeProvider (https://localhost:8080/_snowpack/pkg/#material-ui/core.js:8806:17)
at ThemeProvider$1 (https://localhost:8080/_snowpack/pkg/#material-ui/core.js:4006:5)
at ThemeProvider (https://localhost:8080/_snowpack/pkg/#material-ui/core.js:8826:5)

Jest/RTL Test Component Not Unmounting - Or is it? Can't perform a React state update on an unmounted component

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?)

How to access current state of component from useEffect without making it a dependency?

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 <>{/* ... */}</>
}

React functional component is taking snapshot of state at the time of registering handler on websocket

react functional component is taking snapshot of state at the time of subscription.
For ex. PFB code.
If i click setSocketHandler button and then press setWelcomeString button. Now if i receive message over socket when i log welcomestring it is empty.
But if i click setWelcomeString button and then click setSocketHandler button. Now if i receive message on socket Welcome is getting logged on console.
I have seen same behaviour in project so just created this simple app to prove.
If i use class component which is commented below.. everything works fine.
So my question is why react functional component is working on a state at the time of reg and not on actual state at the time message is received.
This is very weird. How to make it work in functional component correctly.
import React, {useEffect, useState} from 'react';
import logo from './logo.svg';
import './App.css';
const io = require('socket.io-client');
const socket = io.connect('http://localhost:3000/');
const App : React.FunctionComponent = () => {
const [welcomeString, setWelcomeString] = useState("");
const buttonCliecked = () => {
console.log("clocked button");
setWelcomeString("Welcome")
}
const onsockethandlerclicked = () => {
console.log("socket handler clicked");
socket.on('out', () => {
console.log("Recived message")
console.log(welcomeString);
});
}
return (
<div>
<header className="component-header">User Registration</header>
<label>{welcomeString}</label>
<button onClick={buttonCliecked}>setWelcomeString</button>
<button onClick={onsockethandlerclicked}>setSocketHandler</button>
</div>
);
}
/*class App extends React.Component {
constructor(props) {
super(props);
this.state = {
welcomeString:""
}
}
buttonCliecked = () => {
console.log("clocked button");
this.setState({ welcomeString:"Welcome"})
}
onsockethandlerclicked = () => {
console.log("socket handler clicked");
socket.on('out', () => {
console.log("Recived message")
console.log(this.state.welcomeString);
});
}
render() {
return (
<div>
<header className="component-header">User Registration</header>
<label>{this.state.welcomeString}</label>
<button onClick={this.buttonCliecked}>setwelcomestring</button>
<button onClick={this.onsockethandlerclicked}>setSocketHandler</button>
</div>
);
}
}*/
export default App;
For those of us coming from a Redux background, useReducer can seem deceptively complex and unnecessary. Between useState and context, it’s easy to fall into the trap of thinking that a reducer adds unnecessary complexity for the majority of simpler use cases; however, it turns out useReducer can greatly simplify state management. Let’s look at an example.
As with my other posts, this code is from my booklist project. The use case is that a screen allows users to scan in books. The ISBNs are recorded, and then sent to a rate-limited service that looks up the book info. Since the lookup service is rate limited, there’s no way to guarantee your books will get looked up anytime soon, so a web socket is set up; as updates come in, messages are sent down the ws, and handled in the ui. The ws’s api is dirt simple: the data packet has a _messageType property on it, with the rest of the object serving as the payload. Obviously a more serious project would design something sturdier.
With component classes, the code to set up the ws was straightforward: in componentDidMount the ws subscription was created, and in componentWillUnmount it was torn down. With this in mind, it’s easy to fall into the trap of attempting the following with hooks
const BookEntryList = props => {
const [pending, setPending] = useState(0);
const [booksJustSaved, setBooksJustSaved] = useState([]);
useEffect(() => {
const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
ws.onmessage = ({ data }) => {
let packet = JSON.parse(data);
if (packet._messageType == "initial") {
setPending(packet.pending);
} else if (packet._messageType == "bookAdded") {
setPending(pending - 1 || 0);
setBooksJustSaved([packet, ...booksJustSaved]);
} else if (packet._messageType == "pendingBookAdded") {
setPending(+pending + 1 || 0);
} else if (packet._messageType == "bookLookupFailed") {
setPending(pending - 1 || 0);
setBooksJustSaved([
{
_id: "" + new Date(),
title: `Failed lookup for ${packet.isbn}`,
success: false
},
...booksJustSaved
]);
}
};
return () => {
try {
ws.close();
} catch (e) {}
};
}, []);
//...
};
We put the ws creation in a useEffect call with an empty dependency list, which means it’ll never re-fire, and we return a function to do the teardown. When the component first mounts, our ws is set up, and when the component unmounts, it’s torn down, just like we would with a class component.
The problem
This code fails horribly. We’re accessing state inside the useEffect closure, but not including that state in the dependency list. For example, inside of useEffect the value of pending will absolutely always be zero. Sure, we might call setPending inside the ws.onmessage handler, which will cause that state to update, and the component to re-render, but when it re-renders our useEffect will not re-fire (again, because of the empty dependency list)—as a result that closure will go on closing over the now-stale value for pending.
To be clear, using the Hooks linting rule, discussed below, would have caught this easily. More fundamentally, it’s essential to break with old habits from the class component days. Do not approach these dependency lists from a componentDidMount / componentDidUpdate / componentWillUnmount frame of mind. Just because the class component version of this would have set up the web socket once, in componentDidMount, does not mean you can do a direct translation into a useEffect call with an empty dependency list.
Don’t overthink, and don’t be clever: any value from your render function’s scope that’s used in the effect callback needs to be added to your dependency list: this includes props, state, etc. That said—
The solution
While we could add every piece of needed state to our useEffect dependency list, this would cause the web socket to be torn down, and re-created on every update. This would hardly be efficient, and might actually cause problems if the ws sends down a packet of initial state on creation, that might already have been accounted for, and updated in our ui.
If we look closer, however, we might notice something interesting. Every operation we’re performing is always in terms of prior state. We’re always saying something like “increment the number of pending books,” “add this book to the list of completed,” etc. This is precisely where a reducer shines; in fact, sending commands that project prior state to a new state is the whole purpose of a reducer.
Moving this entire state management to a reducer would eliminate any references to local state within the useEffect callback; let’s see how.
function scanReducer(state, [type, payload]) {
switch (type) {
case "initial":
return { ...state, pending: payload.pending };
case "pendingBookAdded":
return { ...state, pending: state.pending + 1 };
case "bookAdded":
return {
...state,
pending: state.pending - 1,
booksSaved: [payload, ...state.booksSaved]
};
case "bookLookupFailed":
return {
...state,
pending: state.pending - 1,
booksSaved: [
{
_id: "" + new Date(),
title: `Failed lookup for ${payload.isbn}`,
success: false
},
...state.booksSaved
]
};
}
return state;
}
const initialState = { pending: 0, booksSaved: [] };
const BookEntryList = props => {
const [state, dispatch] = useReducer(scanReducer, initialState);
useEffect(() => {
const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
ws.onmessage = ({ data }) => {
let packet = JSON.parse(data);
dispatch([packet._messageType, packet]);
};
return () => {
try {
ws.close();
} catch (e) {}
};
}, []);
//...
};
While slightly more lines, we no longer have multiple update functions, our useEffect body is much more simple and readable, and we no longer have to worry about stale state being trapped in a closure: all of our updates happen via dispatches against our single reducer. This also aids in testability, since our reducer is incredibly easy to test; it’s just a vanilla JavaScript function. As Sunil Pai from the React team puts it, using a reducer helps separate reads, from writes. Our useEffect body now only worries about dispatching actions, which produce new state; before it was concerned with both reading existing state, and also writing new state.
You may have noticed actions being sent to the reducer as an array, with the type in the zero slot, rather than as an object with a type key. Either are allowed with useReducer; this is just a trick Dan Abramov showed me to reduce the boilerplate a bit :)
What about functional setState()
Lastly, some of you may be wondering why, in the original code, I didn’t just do this
setPending(pending => pending - 1 || 0);
rather than
setPending(pending - 1 || 0);
This would have removed the closure problem, and worked fine for this particular use case; however, the minute updates to booksJustSaved needed access to the value of pending, or vice versa, this solution would have broken down, leaving us right where we started. Moreover, I find the reducer version to be a bit cleaner, with the state management nicely separated in its own reducer function.
All in all, I think useReducer() is incredibly under-utilized at present. It’s nowhere near as scary as you might think. Give it a try!
Happy coding!

How to implement componentDidMount with hooks in React to be in line with the EsLint rule "react-hooks/exhaustive-deps": "warn"?

According to the official React documentation, componentDidMount is translated in hooks as:
useEffect(() => {
//code here
},[])
So assuming I want to do an api call within this hook:
useEffect(() => {
getActiveUser();
},[])
After adding the eslint rule "react-hooks/exhaustive-deps", this is a lint error. In order to silence it I can just drop the getActiveUser function inside the array and everything works just fine.
But does that go against the documentation? I was under the impression that the array checks for prop changes. I would like also to point out that the API call is being made without a prop/id, so I could understand the fact of having to do something like that:
useEffect(() => {
getActiveUser(someId);
},[getActiveUser, someId])
So what's going on here? Adding the Eslint rule mean that the array inside the effect can't be empty again?
It matters where getActiveUser is declared. The question doesn't specify, but I assume your component looks something like this:
const MyComponent = (props) => {
const getActiveUser() => {
//...
}
useEffect(() => {
getActiveUser();
}, []) // Lint error.
return <></>;
}
If instead your component looked like this, you wouldn't get a linter error:
const getActiveUser() => {
//...
}
const MyComponent = (props) => {
useEffect(() => {
getActiveUser();
}, []) // No error
return <></>;
}
So why is the first a linter error and the second not? The point of the linter rule is to avoid issue due to stale props or state. While getActiveUser is not itself a prop or state, when its defined inside the component, it may depend on props or state, which may be stale.
Consider this code:
const MyComponent = ({userId}) => {
const [userData, setUserData] = useState(null);
const getActiveUser() => {
setUserData(getData(userId)); // More realistically this would be async
}
useEffect(() => {
getActiveUser();
}, []);
//...
}
Even though that useEffect depends on the userId prop, it only runs once, and so the userId and the userData will be out of sync if the userId changes. Maybe this is your intent, but for the purposes of the linter rule it looks like a bug.
In the case where getActiveUser is defined outside the component, it can't possibly (or at least not reasonably) depend on the state or props of the component, so there's no issue for the linter rule.
So how to fix this? Well, if getActiveUser doesn't need to be defined inside the component, just move it out of the component.
Alternatively, if you're sure you only want this behavior to run when the component mounts, and that won't cause issue due to props changing (it's best to assume all props can change), then you can just disable the linter rule.
But assuming neither of those is the case...
A non-solution (too many effects)
As you've noted, adding getActiveUser to the linter array makes the issue go away:
const MyComponent = ({userId}) => {
const getActiveUser() => {
//...
}
useEffect(() => {
getActiveUser();
}, [getActiveUser]) // No error... but probably not right.
return <></>;
}
But getActiveUser is a different function instance every render, so as far as useEffect is concerned, the deps array changes every render, which will cause an API call after every render, which is almost certainly not what you want.
A fragile solution
Since the root issue in my example is that the userId prop might change, you could also fix this issue by adding userId to the useEffect dependencies:
const MyComponent = ({userId}) => {
const getActiveUser() => {
// Uses userId
}
useEffect(() => {
getActiveUser();
// Linter is still unhappy, so:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId])
return <></>;
}
This behaves correctly - no extra API calls or stale data - but the linter is still unhappy: it isn't clever enough to know that we've fixed the dependency on getActiveUser by depending on all the things that getActiveUser depends on.
And this is fragile: if you add a prop or state in the future that getActiveUser depends on, and forget to add it here, you're going to have stale data issues.
A better solution
So the recommended solution is:
const MyComponent = ({userId}) => {
const getActiveUsers = useCallback(() => {
// uses userId
}, [userId]);
useEffect(() => {
getActiveUser();
}, [getActiveUsers]) // No error
return <></>;
}
By wrapping getActiveUsers in useCallback, the function instance is only replaced when needed: when userId changes. This means that the useEffect also only runs when needed: when getActiveUsers changes (which is whenever userId changes).
The linter is happy with this solution and if you introduce new dependencies to getActiveUser, you'll only need to change its useCallback deps, not the useEffect.
Dan Abramov's blogpost A Complete Guide to useEffect goes into this in more detail.

Categories