What's a good pattern for invoking event handler props in useEffect? - reactjs

Let's assume a component:
const Foo = ({id, onError}) => {
useEffect(() => {
subscribe(id).catch(error => onError(error));
return () => cleanup(id);
}, [id, onError]);
return <div>...</div>;
}
The idea is simple-- run an effect that subscribes using the current "id". If the subscription fails, invoke an event handler onError that is passed down as a prop.
However, for this to work correctly, the onError prop that's passed down must be referentially stable. In other words, if a consumer of my component tried the following, they may run into problems where the effect is run for each render:
const Parent = () => {
// handleError is recreated for each render, causing the effect to run each time
const handleError = error => {
console.log("error", error);
}
return <Foo id="test" onError={handleError} />
}
Instead, they would need to do something like:
const Parent = () => {
// handleError identity is stable
const handleError = useCallback(error => {
console.log("error", error);
},[]);
return <Foo id="test" onError={handleError} />
}
This works, but I'm somehow unhappy with it. The consumers of Foo need to realize that onError must be stable, which is not something that's plainly obvious unless you look at its underlying implementation. It breaks the component encapsulation and consumers can easily run into problems without realizing it.
Is there a better pattern to manage props like event handlers that may be invoked within useEffect?

You need to remove onError from your dependency list, but still call if it changes. For that you can use a ref, and update it via useEffect on each render.
You can also use optional chaining ?. to avoid invoking the function if it's undefined.
const Foo = ({ id, onError }) => {
const onErrorRef = useRef();
useEffect(() => {
onErrorRef.current = onError;
});
useEffect(() => {
subscribe(id).catch(error => onErrorRef.current?.(error));
return () => cleanup(id);
}, [id]);
return <div>...</div>;
}

Related

Previous data showing even though cleaning up in useEffect

I have a component in my react native app that loads sessions related to a particular individual. In the useEffect() of that component I both load the sessions when the component comes into focus, and unload those sessions within the cleanup.
export const ClientScreen = (props) => {
const isFocused = useIsFocused();
const client = useSelector((state) => selectActiveClient(state));
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
return () => dispatch(unloadSessions()); // Cleaning up here...
};
loadSessions(props);
}
}, [isFocused, client?.id]);
const updatedProps = {
...props,
client,
};
return <ClientBottomTabNavigator {...updatedProps} />;
};
Generally the component is working as expected. However, I do notice that if I load the component with one client, then navigate away, and then come back to the component by loading a new client, that for a brief moment the sessions pertaining to the previous client show before being replaced the sessions relevant to the new client.
My question is, shouldn't the unloadVisits() that runs on cleanup -- which sets sessions to an empty array -- prevent this? Or is this some kind of react behavior that's holding onto the previous state of the component? How can I ensure this behavior doesn't occur?
Cleanup function should appear before the closing-brace of the useEffect hook
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
};
loadSessions(props);
}
return () => dispatch(unloadSessions()); // Cleaning up here... // <--- here
}, [isFocused, client?.id]);
as commented, your loadSessions returns a cleanup function, but you don't do anything with it. And the effect where you call loadSessions(props) does not return anything, that's why it does not clean up.
Edit:
I made a mistake, loadSessions returns a Promise of a cleanup function. And it is impossible to "unwrap" this Promise and get to the cleanup function itself in a way that you can return it in your effect. You have to move the cleaup function out of the async function loadSessions.
But you don't need async/await for everything:
useEffect(() => {
if (isFocused && client?.id) {
loadClientSessions(client.id).then(dispatch);
return () => dispatch(unloadSessions());
}
}, [isFocused, client?.id]);

How to create safe dispatch function in React

I am trying to tackle the common warning message in React tests
console.error
Warning: An update to EntryList inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
I have created a hook that can be passed a react dispatcher function
export const useSafeDispatches = (...dispatches) => {
const mounted = useRef(false);
useLayoutEffect(() => {
mounted.current = true;
return () => (mounted.current = false);
}, []);
const safeDispatch = useCallback(
(dispatch) =>
(...args) =>
mounted.current ? dispatch(...args) : void 0,
// eslint-disable-next-line
[mounted.current]
);
return dispatches.map(safeDispatch);
};
and, I am using it like this
function MyComponent() {
const [counter, d] = useState(0);
const [setCounter] = useSafeDispatches(d);
return <button onClick={() => setCounter(counter + 1)}>{counter}<button>
}
Yet, I am getting the same error in my tests (where I try to call setState after the component been unmounted)
You are getting this warning because an update has been made to the state of your component after the test is finished.
Search for an async update to the state, and then include an assertion for it in your test.
This warning is the way React is telling you that something happened to your component after the test is finished, and you are not fully testing the component, and you may have missed testing that.
In case you just need to get it done right I suggest you to use existing hooks library with appropriate functionality.
For example that library does exactly what you want
import { useSafeState } from '#react-hookz/web';
function MyComponent() {
const [counter, setCounter] = useSafeState(0);
return <button onClick={() => setCounter((prev) => prev + 1)}>{counter}<button>
}
The warning is not due to an unmounted component. It's due to a test finishing before the last state update. Therefor as another comment says use act which is designed for that.
Using waitFor from testing-library which uses act under the hood:
// this test is using testing-library waitFor
test('does something', async() => {
const { getByRole } = render(<MyCustomButton />);
await waitFor(() => fireEvent.click(getByRole('button')));
expect(someCallback).toBeCalled(); // or some value to be increased
});

How to properly implement "subscription"-like fetches with React useEffect

I have a question about the "proper" (or most idiomatic) way to implement network fetch behavior in React based on a single changing property.
A simplified example of the functionality I'm building is below: I am looking to build a multi-page form that "auto-saves" a draft of form inputs as the user navigates back/forth between pages.
TL;DR - I thought useEffect hooks would be the right way to save a draft to the backend every time a url slug prop changes, but I'm running into issues, and wondering about suggestions for the "right" tool for this type of behavior.
Here is my attempt so far. My code is technically working how I want it to, but violates React's recommended hook dependency pattern (and breaks the exhaustive-deps ESLint rule).
import React from 'react';
const ALL_SLUGS = [
'apple',
'banana',
'coconut',
];
function randomUrlSlug() {
return ALL_SLUGS[Math.floor((Math.random() * ALL_SLUGS.length))];
}
// just resovles the same object passed in
const dummySaveDraftToBackend = (input) => {
return new Promise((resolve, _reject) => {
setTimeout(() => {
resolve(input);
}, 1000);
});
};
export function App() {
const [urlSlug, setUrlSlug] = React.useState(randomUrlSlug());
return (
<MyComponent urlSlug={urlSlug} setUrlSlug={setUrlSlug} />
);
}
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
// useCallback memoization is technically unnecessary as written here,
// but if i follow the linter's advice (listing handleSave as a dependency of the useEffect below), it also suggests memoizing here.
// However, complexState is also technically a dependency of this callback memo, which causes the fetch to trigger every time state changes.
//
// Similarly, moving all of this inside the effect hook, makes the hook dependent on `complexState`, which means the call to the backend happens every time a user changes input data.
const handleSave = React.useCallback(() => {
console.log('*** : start fetch');
setLoading(true);
dummySaveDraftToBackend(complexState).then((resp) => {
console.log('fetch response: ', resp);
// to keep this example simple, here we are just updating
// a dummy "responseCount", but in the actual implementation,
// I'm using a state reducer, and want to make some updates to form state based on error handling, backend validation, etc.
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState]);
// I know this triggers on mount and am aware of strategies to prevent that.
// Just leaving that behavior as-is for the simplified example.
React.useEffect(() => {
if (urlSlug) {
handleSave();
}
}, [urlSlug]); // <- React wants me to also include my memoized handleSave function here, whose reference changes every time state changes. If I include it, the fetch fires every time state changes.
return (
<div className="App">
<h2>the current slug is:</h2>
<h3>{urlSlug}</h3>
<div>the current state is:</div>
<pre>{JSON.stringify(complexState, null, 2)}</pre>
<div>
<h2>edit foo</h2>
<input value={complexState.foo} onChange={(e) => setComplexState((s) => ({ ...s, foo: e.target.value }))} disabled={loading} />
</div>
<div>
<h2>edit baz</h2>
<input value={complexState.baz} onChange={(e) => setComplexState((s) => ({ ...s, baz: e.target.value }))} disabled={loading} />
</div>
<div>
<button
type="button"
onClick={() => setUrlSlug(randomUrlSlug())}
disabled={loading}
>
click to change to a random URL slug
</button>
</div>
</div>
);
}
As written, this does what I want it to do, but I had to omit my handleSave function as a dependency of my useEffect to get it to work. If I list handleSave as a dependency, the hook then relies on complexState, which changes (and thus fires the effect) every time the user modifies input.
I'm concerned about violating React's guidance for not including dependencies. As-is, I would also need to manually prevent the effect from running on mount. But because of the warning, I'm wondering if I should not use a useEffect pattern for this, and if there's a better way.
I believe I could also manually read/write state to a ref to accomplish this, but haven't explored that in much depth yet. I have also explored using event listeners on browser popstate events, which is leading me down another rabbit hole of bugginess.
I know that useEffect hooks are typically intended to be used for side effects based on event behavior (e.g. trigger a fetch on a button click). In my use case however, I can't rely solely on user interactions with elements on the page, since I also want to trigger autosave behavior when the user navigates with their browser back/forward controls (I'm using react-router; current version of react-router has hooks for this behavior, but I'm unfortunately locked in to an old version for the project I'm working on).
Through this process, I realized my understanding might be a bit off on proper usage of hook dependencies, and would love some clarity on what the pitfalls of this current implementation could be. Specifically:
In my snippet above, could somebody clarify to me why ignoring the ESLint rule could be "bad"? Specifically, why might ignoring a dependency on some complex state can be problematic, especially since I dont want to trigger an effect when that state changes?
Is there a better pattern I could use here - instead of relying on a useEffect hook - that is more idiomatic? I basically want to implement a subscriber pattern, i.e. "do something every time a prop changes, and ONLY when that prop changes"
If all the "state" that is updated after saving it to backend is only a call count, declare this as a separate chunk of state. This eliminates creating a render loop on complexState.
Use a React ref to cache the current state value and reference the ref in the useEffect callback. This is to separate the concerns of updating the local form state from the action of saving it in the backend on a different schedule.
Ideally each useState hook's "state" should be closely related properties/values. The complexState appears to be your form data that is being saved in the backend while the responseCount is completely unrelated to the actual form data, but rather it is related to how many times the data has been synchronized.
Example:
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow' });
const [responseCount, setResponseCount] = React.useState(0);
const complexStateRef = React.useRef();
React.useEffect(() => {
complexStateRef.current = complexState;
}, [complexState]);
React.useEffect(() => {
const handleSave = async (complexState) => {
console.log('*** : start fetch');
setLoading(true);
try {
const resp = await dummySaveDraftToBackend(complexState);
console.log('fetch response: ', resp);
setResponseCount(count => count + 1);
} catch(error) {
// handle any rejected Promises, errors, etc...
} finally {
setLoading(false);
}
};
if (urlSlug) {
handleSave(complexStateRef.current);
}
}, [urlSlug]);
return (
...
);
}
This feels like a move in the wrong direction (towards more complexity), but introducing an additional state to determine if the urlSlug has changed seems to work.
export function MyComponent({ urlSlug, setUrlSlug }) {
const [slug, setSlug] = React.useState(urlSlug);
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
const handleSave = React.useCallback(() => {
if (urlSlug === slug) return // only when slug changes and not on mount
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState, urlSlug, slug]);
React.useEffect(() => {
if (urlSlug) {
handleSave();
setSlug(urlSlug)
}
}, [urlSlug, handleSave]);
Or move handleSave inside the useEffect (with additional slug check)
Updated with better semantics
export function MyComponent({ urlSlug, setUrlSlug }) {
const [autoSave, setAutoSave] = React.useState(false); // false for not on mount
React.useEffect(() => {
setAutoSave(true)
}, [urlSlug])
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
React.useEffect(() => {
const handleSave = () => {
if(!autoSave) return
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}
if (urlSlug) {
handleSave();
setAutoSave(false)
}
}, [autoSave, complexState]);

Prevent updating an unmounted component during async event handler

I have an async event handler that deletes a component, but this component is using state to watch the event handler execution status. The event handler is a mock of deleting an item from a remote database.
The problem is that upon successful deletion, the component is unmounted, so the final state update (to indicate that deletion is done) triggers the error "Can't perform a React state update on an unmounted component".
I understand that it is frequent classical issue, I would like to know what is the best way to solve it.
A sandbox:
The full code for reference:
import React from "react";
export default function App() {
const [fruits, setFruits] = React.useState(["apple", "pear", "peach"]);
return (
<ul>
{fruits.map((fruit) => (
<Row key={fruit} fruit={fruit} setFruits={setFruits} />
))}
</ul>
);
}
function Row({ fruit, setFruits }) {
const [isDeleting, setIsDeleting] = React.useState(false);
const handleDelete = async () => {
setIsDeleting(true);
try {
await deleteFruit(fruit, setFruits);
} catch (error) {
console.log("An error occured");
}
setIsDeleting(false);
};
return (
<li>
{fruit}
<button onClick={handleDelete} disabled={isDeleting}>
X
</button>
</li>
);
}
async function deleteFruit(fruitToDelete, setFruits) {
// mock remote DB
return new Promise((resolve) => {
setTimeout(() => {
setFruits((fruits) => fruits.filter((f) => f !== fruitToDelete));
resolve();
}, 1000);
});
}
I have tried to prevent the issue by recording if the component is mounted with useRef and useEffect. It works, but I find that it is not easily readable. Is there a more explicit method to achieve this behaviour?
In component Row's render function:
const refIsMounted = React.useRef(true);
React.useEffect(() => {
refIsMounted.current = true;
return () => {
refIsMounted.current = false;
};
}, []);
And the async event handler:
if (refIsMounted.current) {
setIsDeleting(false);
}
based on useEffect documents you can return a clean-up function.
Often, effects create resources that need to be cleaned up before the
component leaves the screen, such as a subscription or timer ID. To do
this, the function passed to useEffect may return a clean-up function.
so your code will be like below:
useEffect(()=>{
return ()=>{
setIsDeleting(false);}
},[])
I realized what was the issue: the lifetime of a component that triggers an async event handler is independent of the execution time of this async event handler
So the solution is either:
to put the state modified by the handler in a component which we know will outlive the handler, either higher in the component hierarchy or in Redux.
to use the useRef trick as described above to check whether the triggering component is still in existence
Here I lifted the state in the parent component:

How to stop memory leak in useEffect hook react

I am using Effect hook to fetch the datas from server and these data are passed to the react table there i have used the same api call to load the next set of datas from server.
When the application gets loaded i am getting an warning like below
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Effect Hook:
useEffect(() => {
setPageLoading(true);
props
.dispatch(fetchCourses())
.then(() => {
setPageLoading(false);
})
.catch((error: string) => {
toast.error(error);
setPageLoading(false);
});
}, []);
React Table Page:
<ReactTable
className="-striped -highlight"
columns={columns}
data={coursesData}
defaultPage={currentPage}
defaultPageSize={courses.perPage}
loading={isLoading}
manual={true}
onFetchData={setFilter}
/>
Set Filter function:
const setFilter = (pagination: any) => {
props.dispatch(updateCoursePageSize(pagination.pageSize));
props.dispatch(updateCourseCurrentPage(pagination.page + 1));
setCurrentPage(pagination.page);
setPerPage(pagination.pageSize);
setLoading(true);
props.dispatch(fetchCourses()).then(() => {
setLoading(false);
});
};
Does anyone know how to clean up the hook in react
Update (June 2022):
React 18 has removed this warning message, and the workarounds to get rid of it may no longer be necessary. Part of the reason they removed it is that it has always been a bit misleading. It says you have a memory leak, but often times you don't.
The code in the question -- and indeed most code that causes this warning -- runs for a finite amount of time past the unmounting of the component, then sets state, then is done running. Since it's done running, javascript can free up variables in its closure, and thus there is usually no leak.
The case where you will have a memory leak is if you are setting up a persistent subscription which continues indefinitely. For example, maybe you set up a websocket and listen to messages, but you never tear down that websocket. These cases do need to be fixed (by supplying a cleanup function to the useEffect) but they are uncommon.
The other reason react 18 has removed the warning is that they are working on the ability for components to preserve their state after being unmounted. Once that feature is in react, setting state after unmount will be a perfectly valid thing to do.
Original answer (September 2019):
With useEffect you can return a function that will be run on cleanup. So in your case, you'll want something like this:
useEffect(() => {
let unmounted = false;
setPageLoading(true);
props
.dispatch(fetchCourses())
.then(() => {
if (!unmounted) {
setPageLoading(false);
}
})
.catch((error: string) => {
if (!unmounted) {
toast.error(error);
setPageLoading(false);
}
});
return () => { unmounted = true };
}, []);
EDIT: if you need to have a call that's kicked off outside of useEffect, then it will still need to check an unmounted variable to tell whether it should skip the call to setState. That unmounted variable will be set by a useEffect, but now you need to go through some hurdles to make the variable accessible outside of the effect.
const Example = (props) => {
const unmounted = useRef(false);
useEffect(() => {
return () => { unmounted.current = true }
}, []);
const setFilter = () => {
// ...
props.dispatch(fetchCourses()).then(() => {
if (!unmounted.current) {
setLoading(false);
}
})
}
// ...
return (
<ReactTable onFetchData={setFilter} /* other props omitted */ />
);
}
you can create a custom hook for that like that :
import * as React from 'react';
export default function useStateWhenMounted<T>(initialValue: T) {
const [state, setState] = React.useState(initialValue);
const isMounted = React.useRef(true);
React.useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const setNewState = React.useCallback((value) => {
if (isMounted.current) {
setState(value);
}
}, []);
return [state, setNewState];
}
Memory leak happens, when a thing that is unnecessary and is supposed to be cleared from memory is kept because some other thing is still holding it. In React Component case, the async call made in component may hold the references of setState or other references and will hold them until the call completes.
The warning you see is from React saying that something is still holding and setting state of a component instance that was removed from tree long back when component unmounted. Now using a flag to not set the state only removes the warning but not the memory leak, even using Abort controller does the same. To escape this situation you can use state management tools that helps dispatching an action which will do processing out side of component without holding any memory references of the component, for example redux. If you are not using such tools then you should find a way to clear the callbacks you pass to the async call (then, catch, finally blocks) when component unmounts. In the below snippet I am doing the same detaching the references to the methods passed to async call to avoid memory leaks.
Event Emitter here is an Observer, you can create one or use some package.
const PromiseObserver = new EventEmitter();
class AsyncAbort {
constructor() {
this.id = `async_${getRandomString(10)}`;
this.asyncFun = null;
this.asyncFunParams = [];
this.thenBlock = null;
this.catchBlock = null;
this.finallyBlock = null;
}
addCall(asyncFun, params) {
this.asyncFun = asyncFun;
this.asyncFunParams = params;
return this;
}
addThen(callback) {
this.thenBlock = callback;
return this;
}
addCatch(callback) {
this.catchBlock = callback;
return this;
}
addFinally(callback) {
this.finallyBlock = callback;
return this;
}
call() {
const callback = ({ type, value }) => {
switch (type) {
case "then":
if (this.thenBlock) this.thenBlock(value);
break;
case "catch":
if (this.catchBlock) this.catchBlock(value);
break;
case "finally":
if (this.finallyBlock) this.finallyBlock(value);
break;
default:
}
};
PromiseObserver.addListener(this.id, callback);
const cancel = () => {
PromiseObserver.removeAllListeners(this.id);
};
this.asyncFun(...this.asyncFunParams)
.then((resp) => {
PromiseObserver.emit(this.id, { type: "then", value: resp });
})
.catch((error) => {
PromiseObserver.emit(this.id, { type: "catch", value: error });
})
.finally(() => {
PromiseObserver.emit(this.id, { type: "finally" });
PromiseObserver.removeAllListeners(this.id);
});
return cancel;
}
}
in the useEffect hook you can do
React.useEffect(() => {
const abort = new AsyncAbort()
.addCall(simulateSlowNetworkRequest, [])
.addThen((resp) => {
setText("done!");
})
.addCatch((error) => {
console.log(error);
})
.call();
return () => {
abort();
};
}, [setText]);
I forked someones code from here to use above logic, you can check it in action in the below link
link
The other answers work of course, I just wanted to share a solution I came up with.
I built this hook that works just like React's useState, but will only setState if the component is mounted. I find it more elegant because you don't have to mess arround with an isMounted variable in your component !
Installation :
npm install use-state-if-mounted
Usage :
const [count, setCount] = useStateIfMounted(0);
You can find more advanced documentation on the npm page of the hook.

Resources