I'm working on my MERN app, and when I'm logging smth in NewsPage component, it logs infinitely.
NewsPage component:
const NewsPage = ({news, fetchNews}) => {
const postNews = (title, body) => {
axios.post("http://localhost:9000/news", { title, body });
}
useEffect(() => {
fetchNews();
}, [fetchNews, postNews])
return (
<>
<AddNewsForm postNews={postNews}/>
<h1>News:</h1>
<NewsItemPage news={news} />
</>
);
};
const mapStateToProps = state => ({
news: state.news
})
export default connect(mapStateToProps, {fetchNews})(NewsPage);
Fetch news action:
export const fetchNews = () => dispatch => {
fetchRequest();
try {
const fetch = async () => {
const res = await axios.get("http://localhost:9000/news");
dispatch({
type: a.FETCH_NEWS_SUCCESS,
payload: res.data
});
}
fetch()
} catch (e) {
dispatch({
type: a.FETCH_NEWS_FAILURE,
error: e
});
}
}
It works correctly, I can fetch news from and post news to my backend, but if I log anything in console, it would be logging infinitely, and I will not get any error.
is there a way to fix this, and is this a real problem?
Its likely because whatever function the console log is located in is being used in render, which itself is a loop. Otherwise, there is no other way that I can see why it would repeat. It probably won't end up being a problem, unless the code you are executing slows down, which might cause performance issues in the future.
You're tracking fetchNews and postNews in your useEffect array, which will re-rerun fetchNews(); on every render.
Unless the values in the useEffect second argument are primitives, you need to use some deep compare methods for those: https://stackoverflow.com/a/54096391/4468021
Actually, you have wrong useEffect usage.
Effect would be called each time when component receive new props, so, it looks like this:
1) Component mounts, call function from useEffect
2) It makes API call, update data in store
3) Data passed to container, updates "dumb" component
4) Dumb component makes re-rendering, calling func from useEffect, and that's infinity loop.
In fact, It is pretty weird that you don't have memory leak.
What you can do, is:
1) Add some conditional rendering. I pretty sure, you need to call it only on initial load.
2) Add something like ImmutableJS, it would not re-render component and would not mutate store if nothing has changed
Related
So I am following Brad Traversy's React course, but it's a little old and I am using hooks and modern React for my own project, but I have a problem. When I enter the Detail page for a user, it calls a function to get the information of that user from an api. However, it turns out I am calling this function hundreds of times, even with useEffect. I will share my code, but I am not sure what I am doing wrong! If anyone can advise me on why this is re-rendering thousands of times, it would definitely be helpful. Thank you!
This is the function that is being called by Detail (setLoading is a useState function, as well as setSingleUser.
const getUser = username => {
setLoading(true);
axios
.get(
`https://api.github.com/users/${username}?client_id=
${BLAHBLAH}&client_secret=
${BLAHBLAH}`,
)
.then(res => {
axios
.get(
`https://api.github.com/users/${username}/repos?per_page=5&sort=created:asc&
client_id=${BLAHBLAH}&client_secret=
${BLAHBLAH}`,
)
.then(repores =>
setSingleUser({ ...res.data, repos: repores.data }),
);
});
setLoading(false);
};
This is where I call it in Detail
const Detail = ({ getUser, user, loading }) => {
const { username } = useParams();
useEffect(() => {
getUser(username);
}, [username, getUser]);
I have also tried useEffect with an empty dependency array and no dependency array, but it does not like any of these options.
As per your question, you seem to be defining getUser directly within the Parent component and passing it to child component.
Since you update state within the getUser function, the parent component will re-render and a new reference of getUser will be created. Now that you are passing getUser as a dependency to useEffect the useEffect runs again as getUser function has changed.
To solve this, you must memoize the getUser function in parent and you can do that using the useCallback hook
const getUser = useCallback(username => {
setLoading(true);
axios
.get(
`https://api.github.com/users/${username}?client_id=
${BLAHBLAH}&client_secret=
${BLAHBLAH}`,
)
.then(res => {
axios
.get(
`https://api.github.com/users/${username}/repos?per_page=5&sort=created:asc&
client_id=${BLAHBLAH}&client_secret=
${BLAHBLAH}`,
)
.then(repores =>
setSingleUser({ ...res.data, repos: repores.data }),
);
});
setLoading(false);
}, []);
Once you do that your code should work fine
Just include the username in the dependency array. Remove the function getUser since when the username is changed, the return is executed, again when getUser is called, username changes and again return is executed.
Try using this code:
useEffect(() => {
getUser(username);
}, [username]);
This is happening because of the second parameter to useEffect [username, getUser]. The array of dependencies for which the useEffect must be called is being called within your useEffect directly, which then triggers the useEffect again. So, The getUser is called again. Which then calls useEffect again. And therefore its stuck in a loop. You are pretty much calling a function (getUser) again inside of a function (useEffect) that you want to call each time (getUser) is called.
You need to add an if condition in the useEffect which makes sure the getUser() function gets called only when username is false or null or undefined, or you can also change the array that you are passing to the useEffect. You can even take advantage of the setSingleUser() that you have in the .then(). And can add the values of those in the if check or else in the array.
There are certainly a lot of questions and answers about setState, and I've tried looking at them, but haven't found a problem (or solution) that resembled mine closely enough to be useful. It's still very possible that there is one out there that deals with this, and if so, please point it out, and apologies for the duplicate.
Here's the relevant code from a functional React component:
const [ state, setState ] = useState({})
useEffect(data => {
const getFromServer = axios.get('http://localhost:3030/poolInfo')
.then(res => {
console.log("RES.DATA LOOKS LIKE THIS:, ", res.data);
setState(res.data); // I also tried setState({...res.data})
console.log("IN AXIOS, STATE IS NOW: ", state);
})
.catch (err => console.error("YO YOU GOT AN ERROR IN AXIOS ", err))
},[])
Here are the results of the two console.logs above:
RES.DATA LOOKS LIKE THIS:, Object { balance: 1000000000000000000, feeAndSplit: Array [500, 20] }
IN AXIOS, STATE IS NOW: Object { }
The first console.log shows the data exactly like I would expect it. (Hitting the same API with Postman returns the same data, too.) I call setState on literally the same variable that just had data a second ago in the logs, and poof! Now it's an empty object.
I thought maybe the console.log itself was doing something weird to res.data, so I also tried commenting out the first console.log, but still got the same results.
I've wondered if maybe this has something to do with setting state inside useEffect, but if it does, I'm at a loss. (For example, this answer seems to indicate that as long as an empty array is passed in at the end of useEffect, everything should be fine and dandy.)
What happened to res.data, and/or how can I set the state to it?
Thanks!
Everything's working, except you've put your log statement in a place where it's not useful.
state is a local const. It will never change, and that's not what setState is trying to do. The purpose of calling setState is to tell react to rerender the component. When the component rerenders, a new local const will be created, which will have that new value. Code in the new render can access that new value, but code in the old render is still referring to the previous value.
So if you'd like to verify that it's rerendering with the new value, put the log statement in the body of the component so it can log when rendering:
const [ state, setState ] = useState({})
console.log("Rendering with: ", state);
useEffect(data => {
const getFromServer = axios.get('http://localhost:3030/poolInfo')
.then(res => {
console.log("RES.DATA LOOKS LIKE THIS:, ", res.data);
setState(res.data);
})
.catch (err => console.error("YO YOU GOT AN ERROR IN AXIOS ", err))
},[])
Setting a state has some delay, so in order to get the value of your state right after setState('value'); is to use React.useEffect like so:
const [state, setState] = useState();
const handleState = () => {
// Here we assume that user has clicked some button
setState('bla bla bla...');
// If I try to log the state here, I get undefined as the output
console.log(state); // ** BAD PRACTICE **
}
React.useEffect(() => {
console.log(state); // Output: 'bla bla bla...'
}, [state]); // Here I passed 'state' as dependency, whenever state changes, the body of useEffect will be called.
Think of useEffect as a callback for whatever you've passed as dependency.
In some cases you have multiple dependencies, so passing more array members is possible:
React.useEffect(() => {
// do your job
}, [state, someOtherState]);
I don't understand why my useEffect() React function can't access my Component's state variable. I'm trying to create a log when a user abandons creating a listing in our app and navigates to another page. I'm using the useEffect() return method of replicating the componentWillUnmount() lifecycle method. Can you help?
Code Sample
let[progress, setProgress] = React.useState(0)
... user starts building their listing, causing progress to increment ...
console.log(`progress outside useEffect: ${progress}`)
useEffect(() => {
return () => logAbandonListing()
}, [])
const logAbandonListing = () => {
console.log(`progress inside: ${progress}`)
if (progress > 0) {
addToLog(userId)
}
}
Expected Behavior
The code would reach addToLog(), causing this behavior to be logged.
Observed Behavior
This is what happens when a user types something into their listing, causing progress to increment, and then leaves the page.
The useEffect() method works perfectly, and fires the logAbandonListing() function
The first console.log() (above useEffect) logs something greater than 0 for the progress state
The second console.log() logs 0 for the progress state, disabling the code to return true for the if statement and reach the addToLog() function.
Environment
Local dev environment of an app built with Next.js running in Firefox 76.0.1
nextjs v 8.1.0
react v 16.8.6
I'd really appreciate some help understanding what's going on here. Thanks.
I think it is a typical stale closure problem. And it is hard to understand at first.
With the empty dependency array the useEffect will be run only once. And it will access the state from that one run. So it will have a reference from the logAbandonListing function from this moment. This function will access the state from this moment also. You can resolve the problem more than one way.
One of them is to add the state variable to your dependency.
useEffect(() => {
return () => logAbandonListing()
}, [progress])
Another solution is that you set the state value to a ref. And the reference of the ref is not changing, so you will always see the freshest value.
let[progress, setProgress] = React.useState(0);
const progressRef = React.createRef();
progressRef.current = progress;
...
const logAbandonListing = () => {
console.log(`progress inside: ${progressRef.current}`)
if (progressRef.current > 0) {
addToLog(userId)
}
}
If userId is changing too, then you should add it to the dependency or a reference.
To do something in the state's current value in the useEffect's return function where the useEffects dependencies are am empty array [], you could use useReducer. This way you can avoid the stale closure issue and update the state from the useReducer's dispatch function.
Example would be:
import React, { useEffect, useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "set":
return action.payload;
case "unMount":
console.log("This note has been closed: " + state); // This note has been closed: 201
break;
default:
throw new Error();
}
}
function NoteEditor({ initialNoteId }) {
const [noteId, dispatch] = useReducer(reducer, initialNoteId);
useEffect(function logBeforeUnMount() {
return () => dispatch({ type: "unMount" });
}, []);
return <div>{noteId}</div>;
}
export default NoteEditor;
More info on this answer
When you return a function from useEffect, it behaves like componentWillUnmount so I think it only runs while cleaning up. You'd need to actually call logAbandonListing like:
useEffect(() => {
logAbandonListing();
}, []);
So it runs everytime a component re-renders. You can read more about useEffect on https://reactjs.org/docs/hooks-effect.html
It's written excellently.
I tried using this sandbox to explain my answer.
Basically you are returning a function from your useEffect Callback. But that returned function is never really invoked so it does no actually execute and thus log the abandon action. If you look at the Code in the sandbox I have added a wrapper Parens and () afterwards to actually cause the method to be invoked leading to console.log executing.
I'm pretty new to React hooks in general, and very new to useSelector and useDispatch in react-redux, but I'm having trouble executing a simple get request when my component loads. I want the get to happen only once (when the component initially loads). I thought I knew how to do that, but I'm running into an ESLint issue that's preventing me from doing what I understand to be legal code.
I have this hook where I'm trying to abstract my state code:
export const useState = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.data);
return {
data: data,
get: (props) => dispatch(actionCreators.get(props))
};
};
Behind the above function, there's a network request that happens via redux-saga and axios, and has been running in production code for some time. So far, so good. Now I want to use it in a functional component, so I wrote this:
import * as React from 'react';
import { useState } from './my-state-file';
export default () => {
const myState = useState();
React.useEffect(
() => {
myState.get();
return () => {};
},
[]
);
return <div>hello, world</div>;
};
What I expected to happen was that because my useEffect has an empty array as the second argument, it would only execute once, so the get would happen when the component loaded, and that's it.
However, I have ESLint running on save in Atom, and every time I save, it changes that second [] argument to be [myState], the result of which is:
import * as React from 'react';
import { useState } from './my-state-file';
export default () => {
const myState = useState();
React.useEffect(
() => {
myState.get();
return () => {};
},
[myState]
);
return <div>hello, world</div>;
};
If I load this component, then the get runs every single render, which of course is the exact opposite of what I want to have happen. I opened this file in a text editor that does not have ESLint running on save, so when I was able to save useEffect with a blank [], it worked.
So I'm befuddled. My guess is the pattern I'm using above is not correct, but I have no idea what the "right" pattern is.
Any help is appreciated.
Thanks!
UPDATE:
Based on Robert Cooper's answer, and the linked article from Dan Abramov, I did some more experimenting. I'm not all the way there yet, but I managed to get things working.
The big change was that I needed to add a useCallback around my dispatch functions, like so:
export const useState = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.data);
const get = React.useCallback((props) => dispatch({type: 'MY_ACTION', payload:props}), [
dispatch
]);
return {
data: data,
get: get,
};
};
I must admit, I don't fully understand why I need useCallback there, but it works.
Anyway, then my component looks like this:
import * as React from 'react';
import { useState } from './my-state-file';
export default () => {
const {get, data} = useState();
React.useEffect(
() => {
get();
return () => {};
},
[get]
);
return <div>{do something with data...}</div>;
};
The real code is a bit more complex, and I'm hoping to abstract the useEffect call out of the component altogether and put it into either the useState custom hook, or another hook imported from the same my-state-file file.
I believe the problem you're encountering is that the value of myState in your dependency array isn't the same value or has a different JavaScript object reference on every render. The way to get around this would be to pass a memoized or cached version of myState as a dependency to your useEffect.
You could try using useMemo to return a memoized version of your state return by your custom useState. This might look something like this:
export const useState = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.data);
return useMemo(() => ({
data: data,
get: (props) => dispatch(actionCreators.get(props))
}), [props]);
};
Here's what Dan Abramov has to say regarding infinite loops in useEffect methods:
Question: Why do I sometimes get an infinite refetching loop?
This can happen if you’re doing data fetching in an effect without the second dependencies argument. Without it, effects run after every render — and setting the state will trigger the effects again. An infinite loop may also happen if you specify a value that always changes in the dependency array. You can tell which one by removing them one by one. However, removing a dependency you use (or blindly specifying []) is usually the wrong fix. Instead, fix the problem at its source. For example, functions can cause this problem, and putting them inside effects, hoisting them out, or wrapping them with useCallback helps. To avoid recreating objects, useMemo can serve a similar purpose.
Full article here: https://overreacted.io/a-complete-guide-to-useeffect/
I'm building a 'Hacker News' clone, Live Example using React/Redux and can't get this final piece of functionality to work. I have my entire App.js wrapped in BrowserRouter, and I have withRouter imported into my components using window.history. I'm pushing my state into window.history.pushState(getState(), null, `/${getState().searchResponse.params}`) in my API call action creator. console.log(window.history.state) shows my entire application state in the console, so it's pushing in just fine. I guess. In my main component that renders the posts, I have
componentDidMount() {
window.onpopstate = function(event) {
window.history.go(event.state);
};
}
....I also tried window.history.back() and that didn't work
what happens when I press the back button is, the URL bar updates with the correct previous URL, but after a second, the page reloads to the main index URL(homepage). Anyone know how to fix this? I can't find any real documentation(or any other questions that are general and not specific to the OP's particular problem) that makes any sense for React/Redux and where to put the onpopstate or what to do insde of the onpopstate to get this to work correctly.
EDIT: Added more code below
Action Creator:
export const searchQuery = () => async (dispatch, getState) => {
(...)
if (noquery && sort === "date") {
// DATE WITH NO QUERY
const response = await algoliaSearch.get(
`/search_by_date?tags=story&numericFilters=created_at_i>${filter}&page=${page}`
);
dispatch({ type: "FETCH_POSTS", payload: response.data });
}
(...)
window.history.pushState(
getState(),
null,
`/${getState().searchResponse.params}`
);
console.log(window.history.state);
};
^^^ This logs all of my Redux state correctly to the console through window.history.state so I assume I'm implementing window.history.pushState() correctly.
PostList Component:
class PostList extends React.Component {
componentDidMount() {
window.onpopstate = () => {
window.history.back();
};
}
(...)
}
I tried changing window.history.back() to this.props.history.goBack() and didn't work. Does my code make sense? Am I fundamentally misunderstanding the History API?
withRouter HOC gives you history as a prop inside your component, so you don't use the one provided by the window.
You should be able to access the window.history even without using withRouter.
so it should be something like:
const { history } = this.props;
history.push() or history.goBack()