How to trace sources of state changes? - reactjs

I have following hook in my react app:
const MyPage = React.FC = () => {
const myContext = useContext(MyContext);
useEffect(() => {
console.log(myContext);
}, [myContext]);
}
Effect hook though fires 3 times and that's expected and works as designed. But is there way to trace source of context changes so I could remove unnecessary changes?

1st approach: Don't use an effect hook, log where you are everytime a component updates the value provided by the context
// SomeComponents.js
export default () => {
const myContext = useContext(MyContext)
<button onClick={() => {window.console.log('In SomeComponents.js, onClick handler'); myContext.incrementCount()}>
${myContext.count}
</button>
}
2nd approach
More generally, there is a tool to find how to optimize the performance of your React app. It's called the React Profiler, available since React 16.5.
Failed approach: Throw and catch an error to get the stack
useEffect(() => {
try {
throw new Error('I should be caught')
} catch (e) {
window.console.log(e)
}
}, [state])
However the state was updated, the error has the same stack, so this gives us no clue.

Related

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
});

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:

React and firebase adding document to collection issue - Warning: Can't perform a React state update on an unmounted component

I'm trying to add a document to a firebase collection. It works and adds the document but i get the following warning in my react console -
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 the componentWillUnmount method.
This is the utility function I wrote
export const addDocument = async (title) => {
const collectionRef = firestore.collection('collections')
const newDocRef = collectionRef.doc()
const snapshot = await newDocRef.get()
if(!snapshot.exists) {
try {
await newDocRef.set({
title,
items: []
})
} catch (e) {
console.error('Error creating document.', e.message)
}
}
}
This is the component that i'm calling the utility in and is giving me the error
const AdminPage = () => {
const handleSubmit = () => {
addDocument('hat')
}
return (
<div>
<button onClick={handleSubmit}>Add Collection</button>
</div>
)
}
All my imports and exports are fine i've just omitted it to make things more concise.
The code contains async operations so you should implement method: componentWillUnmount
As your code awaits for all async operation (get and set only I think) it should not be a case in your example. This is just a warning and according to my understanding it is showing that async operation may end when object is already destroyed.
Passing some interesting articles and questions:
one, two, three, four
if you need more there is tones of articles over the internet.
I hope it will help!

Best practice to prevent state update warning for unmounted component from a handler

It is a common use-case to fetch and display the data from an external API (by using XHR requests) when a certain UI component (e.g. a <button />) is clicked. However, if the component was unmounted in the meantime, the following warning appears in the console:
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.
In fact, the most common solution (approved by #dan-abramov) to avoid the warning seems to keep track of the mount state of the component by using the return function of useEffect to cleanup.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
export default function PhotoList() {
const mounted = useRef(true);
const [photos, setPhotos] = useState(null);
useEffect(() => {
return () => {
mounted.current = false;
};
}, []);
function handleLoadPhotos() {
axios("https://jsonplaceholder.typicode.com/photos").then(res => {
if (mounted.current) {
setPhotos(res.data);
}
});
}
return (
<div>
<button onClick={handleLoadPhotos}>Load photos</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
However, this seems to cause unnecessary overhead to keep track of the mounting state and to check it before every state update. This becomes especially obvious when Observables (where you can unsubscribe) instead of Promises are used.
While you indeed can unsubscribe inside of the useEffect using the cleanup function in a very neat way:
useEffect(() => {
// getPhotos() returns an observable of the photo list
const photos$ = getPhotos().subscribe(setPhotos);
return () => photos$.unsubscribe();
}, []);
The same smart cleanup is not possible within a handler:
function handleLoadPhotos() {
const photos$ = getPhotos().subscribe(setPhotos);
// how to unsubscribe on unmounting?
}
Is there a best practice to avoid the warning without the ugly manual tracking of the mounting state with useRef()? Are there good approaches for that when using Observables?
Problem is that you are trying to fetch data in your component. This is not a good idea since the component could be unmounted and you would face many possible errors.
So that, you should look for other ways.
I always do async operations in redux thunks.
You should avoid your approach. Use redux and redux-thunk if you like. If not, try to find another solution to move async operations outside of your components.
In fact, you should be writing declarative ui components which renders for given props. So that, your data should be outside of your components logic too.
That's an awesome question! This is how I would do it:
First, define a helper function (it's not cheating because it really is a highly reusable function whenever you're dealing with React and observables combined):
import * as React from 'react';
import { Observable } from 'rxjs';
export const useObservable = <Value>(
arg: () => {
observable: Observable<Value>;
value: Value;
},
) => {
const { observable, value } = React.useMemo(arg, []);
const [state, setState] = React.useState<Value>(value);
React.useEffect(() => {
const subscription = observable.subscribe(value => setState(value));
return () => subscription.unsubscribe();
}, []);
return state;
};
Just to help illustrate what this function does, the following component will display the latest value emitted by myObservable:
() => {
const value = useObservable(() => ({
observable: myObservable,
value: 'Nothing emitted yet',
}));
return <span>{value}</span>;
};
Your component will then look like this:
export default function PhotoList() {
const clicksSubject = React.useMemo(() => new Subject<undefined>(), []);
const photos = useObservable(() => ({
observable: clicksSubject.pipe(
switchMap(() => axiosApiCallReturningAnObservable()),
map(res => res.data),
),
value: null,
}));
return (
<div>
<button
onClick={() => {
clicksSubject.next(undefined);
}}
>
Load photos
</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
When the component is dismounted, useObservable unsubs from the observable that was passed to it. This makes sure that we don't at a later point attempt to set the state, and that the data fetching API aborts (or at least gets a chance to abort) the HTTP request.

Authentication listeners when refactoring to React hooks

I'm having a little trouble figuring out how to change my authentication handling component when refactoring from a React class to React hooks.
Here's the relavant code in my class:
state = {
user: null
}
componentDidMount() {
authGetUser(user => {
if (user !== this.state.user) {
this.setState({user})
}
})
}
componentWillUnmount() {
authUnsubscribe()
}
handleAuthClick = () => {
if (this.state.user) {
authSignOut()
} else {
authSignIn()
}
}
And here it is with hooks:
const [user, setUser] = useState<firebase.User | null>(null)
useEffect(() => {
return authUnsubscribe() // runs on mount and unmount only
}, [])
useEffect(() => {
authGetUser(usr => setUser(usr))
}, [])
const handleAuthClick = () => {
if (user) {
authSignOut()
} else {
authSignIn()
}
}
Also, here are my other relevant methods:
const authGetUser = (callback: (user: firebase.User | null) => void) => {
initFirebase()
authUnsubscribe()
userUnsubscribe = firebaseAuth.onAuthStateChanged(callback)
}
export const authUnsubscribe = () => {
if (userUnsubscribe) {
userUnsubscribe()
}
}
const authSignIn = () => {
googleAuth.signIn().then((googleUser: any) => {
var credential = firebase.auth.GoogleAuthProvider.credential(googleUser.getAuthResponse().id_token)
firebaseAuth.signInAndRetrieveDataWithCredential(credential)
})
}
const authSignOut = () => {
googleAuth
.signOut()
.then(firebaseAuth.signOut())
}
Both examples work. However, when I log out and log in with the hooks version, I get an error message in console saying
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 the componentWillUnmount method.
which suggests that the cleanup is not done properly.
Yes, I know I could just continue using the version with the class which works. But I want to understand React hooks better by solving this.
Any ideas?
Wouldn't this works for you? You could use a single useEffect().
React Hooks API DOCs
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
setUser(null); // <--- TRY DOING SOMETHING LIKE THIS
subscription.unsubscribe();
};
},
[],
);
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect. In our example, this means a new
subscription is created on every update. To avoid firing an effect on
every update, refer to the next section.
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
Generally, this happens when we have asynchronous requests and the component is unmounted before, occurring memory leak. Obviously, that this not occur in class-based components because we have componentDidMount() and componentWillUnmount() hooks, so it's more confident than useEffect() that we have manipulated the state, so I think that you need to identify the reason for the application unmount and there is the solution.
You should use one useEffect() instead two like this:
useEffect(() => {
authGetUser(usr => setUser(usr))
return authUnsubscribe() // runs on mount and unmount only
}, [])

Resources