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?)
I have a Cart component with react-redux and I have a showProducts component that fetches the products from an API (using await- async) inside a useEffect, and then I use useState to set some of the states and use dispatch to update the redux state as well.
I keep getting this warning:
Warning: Cannot update a component (`Cart`) while rendering a different component (`ShowProducts`). To locate the bad setState() call inside `ShowProducts`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
ShowProducts#http://localhost:3000/static/js/main.chunk.js:3099:73
I have a Store page and in my store page I have:
<Grid item xs container >
<ShowProducts />
</Grid>
<Grid item xs container direction="column">
<Cart />
</Grid>
and in my show products:
useEffect(async () => {
await getData();
}, []);
.
dispatch(setShippingCosts);
dispatch(setCompanyNam);
.
.
.
async function getData() {
fetch(
`url`
)
.then((res) => res.json())
.then((data) => {
.
.
.
setProducts(...);
setShippingCost(...);
.
})
.catch((error) => {
setError(error);
});
}
and in my cart, I am using the shipping cost that comes from the show products component. I am not sure how to fix this warning, I have been searching and I haven't found a solution yet. how serious is this warning, and I am not sure why I am getting it.
full warning:
The problem is when one component queues an update in another component, while the first component is rendering.
(Bug: too hard to fix "Cannot update a component from inside the function body of a different component.")
Problem:
While ShowProducts is rendered it also dispatching an action that causing to queue an update in the Cart.
Fix:
Move the dispatch inside useEffect.
Explanation:
By using this Hook, you tell React that your component needs to do something after render. React will remember the function you passed (we’ll refer to it as our “effect”), and call it later after performing the DOM updates.
What does useEffect do?
The following is sandbox pen to demonstrate the bug and the fix.
(Open the console in the right bottom corner to see the warning, you can comment out the dispatch in the render and see the warning disappears)
Note that you are using useEffect async and it is supposed to be sync only. Read here
I had the same problem and the root of my problem was a little different, and it caused the same error.
I was trying to update the state of parent component (through callback props), in the body of updater callback of useState's setState. like this:
...
const [state, setState] = useState();
useState((prev) => {
const newState = someOperationToGenerateNewStateFromTheOldOne(prev);
updateStateOfParentComponent();//this call caused the issue because as the error says causes update of parent component during the state change(render) of this component
return newState;
})
As a fix you can use useEffect like this:
...
const [state, setState] = useState();
useEffect(() => {
updateStateOfParentComponent();
}, [state])
useState((prev) => {
return someOperationToGenerateNewStateFromTheOldOne(prev);
})
I'm having a really hard time to figure out what's happening when there is nothing being used to trigger re-render the component.
Events.js Component renders twice when I remove the useState() from the Event.js it renders once, but I need to keep it. when I use useEffect() inside Event components, renders fourth time.
I just kept the dummy data to give you to fill the emptiness and tried to remove React.memo, nothing happens. the problem is with the Event.js component I believe. I'm also using the Context API, but forth time rendering is too much.
useEffect inside App.js is getting some value from the localStorage, I can't access that direct 'cause the value is undefined by default
sandbox code here: https://codesandbox.io/s/event-manager-reactjs-nbz8z?file=/src/Pages/Events/Events.js
The Events.js file is located on /Pages/Events/Events.js
example code is below
Event.js ( child component )
function Events() {
// Sate Managing
const [allEvents, setAllEvents] = React.useState(null);
console.log('Rendering EventsJs', allEvents);
React.useEffect(() => {
setAllEvents(['apple', 'banana']);
}, []);
return (
<div className="events">
{ console.log('Event Rendered.js =>') }
</div>
)
}
export default React.memo(Events, (prevProps, nextProps) => {
return true;
} );
App.js ( parent component )
import { BrowserRouter, Route, Redirect } from 'react-router-dom';
function App() {
const [userId, setUserId] = React.useState(null);
React.useEffect(() => {
setUserId(1);
}, []);
// Login
return (
<BrowserRouter>
<Navigation />
<Route path='/events' component={Events} />
{console.log('App Rendered')}
</BrowserRouter>
);
}
export default App;
Error:
Your app is working fine. It is rendering as it should. As we know:
A React component re-renders whenever its props or state change.
And react component lifecycle order is:
Initial props/state --> render --> DOM update --> mounted
props/state changed --> render --> DOM update --> updated ... so on
In the example below, it is rendering 2 times and that's correct:
First one (first console.log) is due to initial render with state as []
Second one (second console.log) is due to state change (caused by useEffect) to ['apple', 'banana']
function Events() {
const [allEvents, setAllEvents] = React.useState([]);
console.log('Event Rendered', allEvents);
useEffect(() => {
setAllEvents(['apple', 'banana']);
}, []);
return <>Events</>;
}
About using React.memo:
React.memo only checks for props changes. If your function component wrapped in React.memo has a useState or useContext Hook in its implementation, it will still rerender when state or context change.
You can not skip re-render using React.memo due to change in state. You can only optimize to skip re-rendering caused by change in props.
But in the example above, you don't have props passed from the parent component, the only props passed to Events are those passed by react-router i.e. route props. So, there is no need to use React.memo.
Here is sandbox, check the console.logs. You will see only 3 logs: "App render", "Event render with initial state", "Event render with new state".
EDIT:
If we remove StrictMode from index.html, and add below console.logs in components:
App.js --> console.log('App rendered')
Evenets.js --> console.log('Event rendered', allEvents, isLoading) // (allEvents and isLoading are state variables here)
And go to http://localhost:3000, we see 1 log:
App Rendered
Now click on "Events", we see 3 logs:
1: Event Rendered, [], true
2: Event Rendered, [{}, ... 54 items], true
3: Event Rendered, [{}, ... 54 items], false
which is correct behavior (refer lifecycles order written above):
1st log: render with initial state ([], true)
2nd log: render with new allEvents (54 items) and old isLoading (true)
3rd log: render with old allEvents (54 items) and new isLoading (false)
Below are the right questions to ask now:
Question1:
Why 2nd and 3rd render (log) are separate, should not they be batched (merged) and applied together as they are written in the same function?
fetch('url').then(() => {
// ... code here
setAllEvents([...events])
setLoading(false)
})
Answer:
No, they will not be batched in above code. As explained by Dan Abramov:
This is implementation detail and may change in future versions.
In current release, they will be batched together if you are inside a React event handler. React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.
With current version, several setStates outside of event handlers (e.g. in network responses) will not be batched. So you would get two re-renders in that case.
There exists a temporary API to force batching. If you write ReactDOM.unstable_batchedUpdates(() => { this.fn1(); }); then both calls will be batched. But we expect to remove this API in the future and instead batch everything by default.
So, you can write (inside fetch's then), if you want, it will save 1 render:
ReactDOM.unstable_batchedUpdates(() => {
setAllEvents([...events])
setLoading(false)
})
Question2:
What's React event handler in above quote?
Answer: foo in example below. These 2 set states will be batched.
const foo = () => {
setAllEvents([
{ _id: '5ede5af03915bc469a9d598e', title: 'jfklsd', },
])
setLoading(false)
}
<button onClick={foo}>CLICK</button>
Question3:
Does it update HTML DOM as many times as it renders (prints console.log)?
Answer: No. React compares calculated virtual DOMs before updating real DOM, so only those changes are applied to real DOM which are required to update the UI.
Question4:
Why was rendering doubled when we use StrictMode?
Answer: Yes, StrictMode will intentionally double invoke "render" and some other lifecycle methods to detect side-effects. Strict mode checks are run in development mode only; they do not impact the production build.
Well actually this is caused by your usage of React.memo, its second parameter is called areEqual, and you pass in () => false, so you are basically telling React that the props are always changing. Therefore whenever App rerenders, Events rerenders too.
You should let React.memo check for prop changes. By passing () => false you are actually telling that its props always change (they are never equal).
export default React.memo(Events);
Here's a working example.
I'm trying to set up a React app where clicking a map marker in one component re-renders another component on the page with data from the database and changes the URL. It works, sort of, but not well.
I'm having trouble figuring out how getting the state from Redux and getting a response back from the API fit within the React life cycle.
There are two related problems:
FIRST: The commented-out line "//APIManager.get()......" doesn't work, but the hacked-together version on the line below it does.
SECOND: The line where I'm console.log()-ing the response logs infinitely and makes infinite GET requests to my database.
Here's my component below:
class Hike extends Component {
constructor() {
super()
this.state = {
currentHike: {
id: '',
name: '',
review: {},
}
}
}
componentDidUpdate() {
const params = this.props.params
const hack = "/api/hike/" + params
// APIManager.get('/api/hike/', params, (err, response) => { // doesn't work
APIManager.get(hack, null, (err, response) => { // works
if (err) {
console.error(err)
return
}
console.log(JSON.stringify(response.result)) // SECOND
this.setState({
currentHike: response.result
})
})
}
render() {
// Allow for fields to be blank
const name = (this.state.currentHike.name == null) ? null : this.state.currentHike.name
return (
<div>
<p>testing hike component</p>
<p>{this.state.currentHike.name}</p>
</div>
)
}
}
const stateToProps = (state) => {
return {
params: state.hike.selectedHike
}
}
export default connect(stateToProps)(Hike)
Also: When I click a link on the page to go to another url, I get the following error:
"Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op."
Looking at your code, I think I would architect it slightly differently
Few things:
Try to move the API calls and fetch data into a Redux action. Since API fetch is asynchronous, I think it is best to use Redux Thunk
example:
function fetchHikeById(hikeId) {
return dispatch => {
// optional: dispatch an action here to change redux state to loading
dispatch(action.loadingStarted())
const hack = "/api/hike/" + hikeId
APIManager.get(hack, null, (err, response) => {
if (err) {
console.error(err);
// if you want user to know an error happened.
// you can optionally dispatch action to store
// the error in the redux state.
dispatch(action.fetchError(err));
return;
}
dispatch(action.currentHikeReceived(response.result))
});
}
}
You can map dispatch to props for fetchHikeById also, by treating fetchHikeById like any other action creator.
Since you have a path /hike/:hikeId I assume you are also updating the route. So if you want people to book mark and save and url .../hike/2 or go back to it. You can still put the the fetch in the Hike component.
The lifecycle method you put the fetchHikeById action is.
componentDidMount() {
// assume you are using react router to pass the hikeId
// from the url '/hike/:hikeId'
const hikeId = this.props.params.hikeId;
this.props.fetchHikeById(hikeId);
}
componentWillReceiveProps(nextProps) {
// so this is when the props changed.
// so if the hikeId change, you'd have to re-fetch.
if (this.props.params.hikeId !== nextProps.params.hikeId) {
this.props.fetchHikeById(nextProps.params.hikeId)
}
}
I don't see any Redux being used at all in your code. If you plan on using Redux, you should move all that API logic into an action creator and store the API responses in your Redux Store. I understand you're quickly prototyping now. :)
Your infinite loop is caused because you chose the wrong lifecycle method. If you use the componentDidUpdate and setState, it will again cause the componentDidUpdatemethod to be called and so on. You're basically updating whenever the component is updated, if that makes any sense. :D
You could always check, before sending the API call, if the new props.params you have are different than the ones you previously had (which caused the API call). You receive the old props and state as arguments to that function.
https://facebook.github.io/react/docs/react-component.html#componentdidupdate
However, if you've decided to use Redux, I would probably move that logic to an action creator, store that response in your Redux Store and simply use that data in your connect.
The FIRST problem I cannot help with, as I do not know what this APIManager's arguments should be.
The SECOND problem is a result of you doing API requests in "componentDidUpdate()". This is essentially what happens:
Some state changes in redux.
Hike receives new props (or its state changes).
Hike renders according to the new props.
Hike has now been updated and calls your "componentDidUpdate" function.
componentDidUpdate makes the API call, and when the response comes back, it triggers setState().
Inner state of Hike is changed, which triggers an update of the component(!) -> goto step 2.
When you click on a link to another page, the infinite loop is continued and after the last API call triggered by an update of Hike is resolved, you call "setState" again, which now tries to update the state of a no-longer-mounted component, hence the warning.
The docs explain this really well I find, I would give those a thorough read.
Try making the API call in componentDidMount:
componentDidMount() {
// make your API call and then call .setState
}
Do that instead of inside of componentDidUpdate.
There are many ways to architect your API calls inside of your React app. For example, take a look at this article: React AJAX Best Practices. In case the link is broken, it outlines a few ideas:
Root Component
This is the simplest approach so it's great for prototypes and small apps.
With this approach, you build a single root/parent component that issues all your AJAX requests. The root component stores the AJAX response data in it's state, and passes that state (or a portion of it) down to child components as props.
As this is outside the scope of the question, I'll leave you to to a bit of research, but some other methods for managing state and async API calls involved libraries like Redux which is one of the de-facto state managers for React right now.
By the way, your infinite calls come from the fact that when your component updates, it's making an API call and then calling setState which updates the component again, throwing you into an infinite loop.
Still figuring out the flow of Redux because it solved the problem when I moved the API request from the Hike component to the one it was listening to.
Now the Hike component is just listening and re-rendering once the database info catches up with the re-routing and re-rendering.
Hike.js
class Hike extends Component {
constructor() {
super()
this.state = {}
}
componentDidUpdate() {
console.log('dealing with ' + JSON.stringify(this.props.currentHike))
}
render() {
if (this.props.currentHike == null || undefined) { return false }
const currentHike = this.props.currentHike
return (
<div className="sidebar">
<p>{currentHike.name}</p>
</div>
)
}
}
const stateToProps = (state) => {
return {
currentHike: state.hike.currentHike,
}
}
And "this.props.currentHikeReceived()" got moved back to the action doing everything in the other component so I no longer have to worry about the Hikes component infinitely re-rendering itself.
Map.js
onMarkerClick(id) {
const hikeId = id
// Set params to be fetched
this.props.hikeSelected(hikeId)
// GET hike data from database
const hack = "/api/hike/" + hikeId
APIManager.get(hack, null, (err, response) => {
if (err) {
console.error(err)
return
}
this.props.currentHikeReceived(response.result)
})
// Change path to clicked hike
const path = `/hike/${hikeId}`
browserHistory.push(path)
}
const stateToProps = (state) => {
return {
hikes: state.hike.list,
location: state.newHike
}
}
const dispatchToProps = (dispatch) => {
return {
currentHikeReceived: (hike) => dispatch(actions.currentHikeReceived(hike)),
hikesReceived: (hikes) => dispatch(actions.hikesReceived(hikes)),
hikeSelected: (hike) => dispatch(actions.hikeSelected(hike)),
locationAdded: (location) => dispatch(actions.locationAdded(location)),
}
}