I've been playing around with the new hook system in React 16.7-alpha and get stuck in an infinite loop in useEffect when the state I'm handling is an object or array.
First, I use useState and initiate it with an empty object like this:
const [obj, setObj] = useState({});
Then, in useEffect, I use setObj to set it to an empty object again. As a second argument I'm passing [obj], hoping that it wont update if the content of the object hasn't changed. But it keeps updating. I guess because no matter the content, these are always different objects making React thinking it keep changing?
useEffect(() => {
setIngredients({});
}, [ingredients]);
The same is true with arrays, but as a primitive it wont get stuck in a loop, as expected.
Using these new hooks, how should I handle objects and array when checking weather the content has changed or not?
Passing an empty array as the second argument to useEffect makes it only run on mount and unmount, thus stopping any infinite loops.
useEffect(() => {
setIngredients({});
}, []);
This was clarified to me in the blog post on React hooks at https://www.robinwieruch.de/react-hooks/
Had the same problem. I don't know why they not mention this in docs. Just want to add a little to Tobias Haugen answer.
To run in every component/parent rerender you need to use:
useEffect(() => {
// don't know where it can be used :/
})
To run anything only one time after component mount(will be rendered once) you need to use:
useEffect(() => {
// do anything only one time if you pass empty array []
// keep in mind, that component will be rendered one time (with default values) before we get here
}, [] )
To run anything one time on component mount and on data/data2 change:
const [data, setData] = useState(false)
const [data2, setData2] = useState('default value for first render')
useEffect(() => {
// if you pass some variable, than component will rerender after component mount one time and second time if this(in my case data or data2) is changed
// if your data is object and you want to trigger this when property of object changed, clone object like this let clone = JSON.parse(JSON.stringify(data)), change it clone.prop = 2 and setData(clone).
// if you do like this 'data.prop=2' without cloning useEffect will not be triggered, because link to data object in momory doesn't changed, even if object changed (as i understand this)
}, [data, data2] )
How i use it most of the time:
export default function Book({id}) {
const [book, bookSet] = useState(false)
const loadBookFromServer = useCallback(async () => {
let response = await fetch('api/book/' + id)
response = await response.json()
bookSet(response)
}, [id]) // every time id changed, new book will be loaded
useEffect(() => {
loadBookFromServer()
}, [loadBookFromServer]) // useEffect will run once and when id changes
if (!book) return false //first render, when useEffect did't triggered yet we will return false
return <div>{JSON.stringify(book)}</div>
}
I ran into the same problem too once and I fixed it by making sure I pass primitive values in the second argument [].
If you pass an object, React will store only the reference to the object and run the effect when the reference changes, which is usually every singe time (I don't now how though).
The solution is to pass the values in the object. You can try,
const obj = { keyA: 'a', keyB: 'b' }
useEffect(() => {
// do something
}, [Object.values(obj)]);
or
const obj = { keyA: 'a', keyB: 'b' }
useEffect(() => {
// do something
}, [obj.keyA, obj.keyB]);
If you are building a custom hook, you can sometimes cause an infinite loop with default as follows
function useMyBadHook(values = {}) {
useEffect(()=> {
/* This runs every render, if values is undefined */
},
[values]
)
}
The fix is to use the same object instead of creating a new one on every function call:
const defaultValues = {};
function useMyBadHook(values = defaultValues) {
useEffect(()=> {
/* This runs on first call and when values change */
},
[values]
)
}
If you are encountering this in your component code the loop may get fixed if you use defaultProps instead of ES6 default values
function MyComponent({values}) {
useEffect(()=> {
/* do stuff*/
},[values]
)
return null; /* stuff */
}
MyComponent.defaultProps = {
values = {}
}
Your infinite loop is due to circularity
useEffect(() => {
setIngredients({});
}, [ingredients]);
setIngredients({}); will change the value of ingredients(will return a new reference each time), which will run setIngredients({}). To solve this you can use either approach:
Pass a different second argument to useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
setIngredients({});
}, [timeToChangeIngrediants ]);
setIngrediants will run when timeToChangeIngrediants has changed.
I'm not sure what use case justifies change ingrediants once it has been changed. But if it is the case, you pass Object.values(ingrediants) as a second argument to useEffect.
useEffect(() => {
setIngredients({});
}, Object.values(ingrediants));
As said in the documentation (https://reactjs.org/docs/hooks-effect.html), the useEffect hook is meant to be used when you want some code to be executed after every render. From the docs:
Does useEffect run after every render? Yes!
If you want to customize this, you can follow the instructions that appear later in the same page (https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects). Basically, the useEffect method accepts a second argument, that React will examine to determine if the effect has to be triggered again or not.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
You can pass any object as the second argument. If this object remains unchanged, your effect will only be triggered after the first mount. If the object changes, the effect will be triggered again.
I'm not sure if this will work for you but you could try adding .length like this:
useEffect(() => {
// fetch from server and set as obj
}, [obj.length]);
In my case (I was fetching an array!) it fetched data on mount, then again only on change and it didn't go into a loop.
If you include empty array at the end of useEffect:
useEffect(()=>{
setText(text);
},[])
It would run once.
If you include also parameter on array:
useEffect(()=>{
setText(text);
},[text])
It would run whenever text parameter change.
I often run into an infinite re-render when having a complex object as state and updating it from useRef:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients({
...ingredients,
newIngedient: { ... }
});
}, [ingredients]);
In this case eslint(react-hooks/exhaustive-deps) forces me (correctly) to add ingredients to the dependency array. However, this results in an infinite re-render. Unlike what some say in this thread, this is correct, and you can't get away with putting ingredients.someKey or ingredients.length into the dependency array.
The solution is that setters provide the old value that you can refer to. You should use this, rather than referring to ingredients directly:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients(oldIngedients => {
return {
...oldIngedients,
newIngedient: { ... }
}
});
}, []);
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.
I believe they are trying to express the possibility that one could be using stale data, and to be aware of this. It doesn't matter the type of values we send in the array for the second argument as long as we know that if any of those values change it will execute the effect. If we are using ingredients as part of the computation within the effect, we should include it in the array.
const [ingredients, setIngredients] = useState({});
// This will be an infinite loop, because by shallow comparison ingredients !== {}
useEffect(() => {
setIngredients({});
}, [ingredients]);
// If we need to update ingredients then we need to manually confirm
// that it is actually different by deep comparison.
useEffect(() => {
if (is(<similar_object>, ingredients) {
return;
}
setIngredients(<similar_object>);
}, [ingredients]);
The main problem is that useEffect compares the incoming value with the current value shallowly. This means that these two values compared using '===' comparison which only checks for object references and although array and object values are the same it treats them to be two different objects. I recommend you to check out my article about useEffect as a lifecycle methods.
The best way is to compare previous value with current value by using usePrevious() and _.isEqual() from Lodash.
Import isEqual and useRef. Compare your previous value with current value inside the useEffect(). If they are same do nothing else update. usePrevious(value) is a custom hook which create a ref with useRef().
Below is snippet of my code. I was facing problem of infinite loop with updating data using firebase hook
import React, { useState, useEffect, useRef } from 'react'
import 'firebase/database'
import { Redirect } from 'react-router-dom'
import { isEqual } from 'lodash'
import {
useUserStatistics
} from '../../hooks/firebase-hooks'
export function TMDPage({ match, history, location }) {
const usePrevious = value => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
const userId = match.params ? match.params.id : ''
const teamId = location.state ? location.state.teamId : ''
const [userStatistics] = useUserStatistics(userId, teamId)
const previousUserStatistics = usePrevious(userStatistics)
useEffect(() => {
if (
!isEqual(userStatistics, previousUserStatistics)
) {
doSomething()
}
})
In case you DO need to compare the object and when it is updated here is a deepCompare hook for comparison. The accepted answer surely does not address that. Having an [] array is suitable if you need the effect to run only once when mounted.
Also, other voted answers only address a check for primitive types by doing obj.value or something similar to first get to the level where it is not nested. This may not be the best case for deeply nested objects.
So here is one that will work in all cases.
import { DependencyList } from "react";
const useDeepCompare = (
value: DependencyList | undefined
): DependencyList | undefined => {
const ref = useRef<DependencyList | undefined>();
if (!isEqual(ref.current, value)) {
ref.current = value;
}
return ref.current;
};
You can use the same in useEffect hook
React.useEffect(() => {
setState(state);
}, useDeepCompare([state]));
You could also destructure the object in the dependency array, meaning the state would only update when certain parts of the object updated.
For the sake of this example, let's say the ingredients contained carrots, we could pass that to the dependency, and only if carrots changed, would the state update.
You could then take this further and only update the number of carrots at certain points, thus controlling when the state would update and avoiding an infinite loop.
useEffect(() => {
setIngredients({});
}, [ingredients.carrots]);
An example of when something like this could be used is when a user logs into a website. When they log in, we could destructure the user object to extract their cookie and permission role, and update the state of the app accordingly.
my Case was special on encountering an infinite loop, the senario was like this:
I had an Object, lets say objX that comes from props and i was destructuring it in props like:
const { something: { somePropery } } = ObjX
and i used the somePropery as a dependency to my useEffect like:
useEffect(() => {
// ...
}, [somePropery])
and it caused me an infinite loop, i tried to handle this by passing the whole something as a dependency and it worked properly.
Another worked solution that I used for arrays state is:
useEffect(() => {
setIngredients(ingredients.length ? ingredients : null);
}, [ingredients]);
Related
Is there any other way to use an async function in a react component outside of useEffect?
For example, I defined an async function outside of the react component:
//In src/utils/getFactoryPair.ts
export const getFactoryPair = async (factoryContract: SwapV2Factory, pairIndex: number): Promise<string> => {
const pair = await factoryContract.allPairs(pairIndex);
return pair;
}
And in my react component, why cant I do this?
import { getFactoryPair } from "src/utils/getFactoryPair.ts";
const App: React.FC = () => {
...
const pair = getFactoryPair(myContractInstance, 1)
...
}
And instead I have to call it within useEffect or define another async function within the react component to call getFactoryPair.
Is there another way or is this just how it works?
From What is the point of useEffect() if you don't specify a dependancy:
Effects that are executed at the top-level inside functional component
execute in a different way as compared to the effect inside the
useEffect hook.
When effects are inside the useEffect:
They are executed after browser has painted the screen, i.e. after
React has applied changes to the DOM.
Before running the effect again, useEffect can run the clean-up function,
if clean-up function is provided.
Specifying the dependency array will allow you to skip
running the effect after every re-render of the component.
Because of the above mentioned points, you always want the
side-effects to be executed inside the useEffect hook.
Hope this helps!
And in my react component, why cant I do this?
You can call it, but you won't want to, because you won't be able to use its result. The render call (the call to your component function) is synchronous, and your async function reports its completion asynchronously.
Separately, React will call your component's function to render whenever it needs to, so depending on what the function does, if you did call it directly during render, you might be calling it more often than you should. This is why you use useEffect to control when you call it:
With no dependency array: Called after every render (relatively uncommon).
With an empty dependency array: Called after the first render only, and never again for the same element.
With a non-empty dependency array: Called after the first render and then called again after any render where any of the dependency values changed.
Is there another way or is this just how it works?
This is just now it works, render calls are synchronous.
It's not clear to me what getFactoryPair does. If it's just accessing information, it would be fine to call it during render (if it were synchronous), but if it's creating or modifying information, it wouldn't be, because rendering should be pure other than state and such handled via hooks.
In a comment you've clarified what you use the function for:
...getFactoryPair basically retrieves the contract address for another contract based on the given index. Simply retrieving information in an async way. I would eventually use the results to display it on a table component.
Assuming you get myContractInstance as a prop (though I may be wrong there, you didn't show it as one), you'd probably use state to track whether you had received the pair, and useEffect to update the pair if myContractInstance changed:
const App: React.FC<PropsTypeHere> = ({ myContractInstance }) => {
const [pair, setPair] = useState<string | null>(null);
const [pairError, setPairError] = useState<Error | null>(null);
useEffect(() => {
getFactoryPair(myContractInstance, 1)
.then((pair) => {
setPair(pair);
setPairError(null);
})
.catch((error) => {
setPair(null);
setPairError(error);
});
}, [myContractInstance]);
if (pairError !== null) {
// Render an "error" state
return /*...*/;
}
if (pair === null) {
// Render a "loading" state
return /*...*/;
}
// Render the table element using `pair`
return /*...*/;
};
This is a fairly classic use of useEffect. You can wrap it up in a custom, reusable hook if you like:
type UsePairResult =
// The pair is loaded
| ["loaded", string, null]
// The pair is loading
| ["loading", null, null]
// There was an error loading
| ["error", null, string];
function usePair(instance: TheInstanceType, index: number): UsePairResult {
const [pair, setPair] = useState<string | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
getFactoryPair(instance, index)
.then((pair) => {
setPair(pair);
setError(null);
})
.catch((error) => {
setPair(null);
setError(error);
});
}, [instance, index]);
const state = pair ? "loaded" : error ? "error" : "loading";
return [state, pair, error];
}
Then any place you need it:
const App = ({ myContractInstance }) => {
const [pairState, pair, pairError] = usePair(myContractInstance, 1);
switch (pairState) {
case "loaded":
// Render the table element using `pair`
return /*...*/;
case "loading":
// Render a "loading" state
return /*...*/;
case "error":
// Render an "error" state
return /*...*/;
}
};
Graphql provides useQuery hook to fetch data. It will get called whenever the component re-renders.
//mocking useQuery hook of graphql, which updates the data variable
const data = useQuery(false);
I am using useEffect hook to control how many times should "useQuery" be called.
What I want to do is whenever I receive the data from useQuery, I want to perform some operation on the data and set it to another state variable "stateOfValue" which is a nested object data. So this has to be done inside the useEffect hook.
Hence I need to add my stateOfValue and "data" (this has my API data) variable as a dependencies to the useEffect hook.
const [stateOfValue, setStateOfValue] = useState({
name: "jack",
options: []
});
const someOperation = (currentState) => {
return {
...currentState,
options: [1, 2, 3]
};
}
useEffect(() => {
if (data) {
let newValue = someOperation(stateOfValue);
setStateOfValue(newValue);
}
}, [data, stateOfValue]);
Basically I am adding all the variables which are being used inside my useEffect as a dependency because that is the right way to do according to Dan Abramov.
Now, according to react, state updates must be done without mutations to I am creating a new object every time I need to update the state. But with setting a new state variable object, my component gets re-rendered, causing an infinite renders.
How to go about implementing it in such a manner that I pass in all the variables to my dependency array of useEffect, and having it execute useEffect only once.
Please note: it works if I don't add stateOfValue variable to dependencies, but that would be lying to react.
Here is the reproduced link.
I think you misunderstood
what you want to be in dependencies array is [data, setStateOfValue] not [data, stateOfValue]. because you use setStateOfValue not stateOfValue inside useEffect
The proper one is:
const [stateOfValue, setStateOfValue] = useState({
name: "jack",
options: []
});
const someOperation = useCallback((prevValue) => {
return {
...prevValue,
options: [1, 2, 3]
};
},[])
useEffect(() => {
if (data) {
setStateOfValue(prevValue => {
let newValue = someOperation(prevValue);
return newValue
});
}
}, [data, setStateOfValue,someOperation]);
If you want to set state in an effect you can do the following:
const data = useQuery(query);
const [stateOfValue, setStateOfValue] = useState({});
const someOperation = useCallback(
() =>
setStateOfValue((current) => ({ ...current, data })),
[data]
);
useEffect(() => someOperation(), [someOperation]);
Every time data changes the function SomeOperation is re created and causes the effect to run. At some point data is loaded or there is an error and data is not re created again causing someOperation not to be created again and the effect not to run again.
First I'd question if you need to store stateOfValue as state. If not (eg it won't be edited by anything else) you could potentially use the useMemo hook instead
const myComputedValue = useMemo(() => someOperation(data), [data]);
Now myComputedValue will be the result of someOperation, but it will only re-run when data changes
If it's necessary to store it as state you might be able to use the onCompleted option in useQuery
const data = useQuery(query, {
onCompleted: response => {
let newValue = someOperation();
setStateOfValue(newValue);
}
)
In my state I use a set to keep track of a selection. The set can grow to quite large size and as such I wish to prevent copying the set constantly.
I am using a hook for state as below, the code is working. However instead of returning a new set, I prefer to return the "old set", update the set in place.
When I do so, however, react doesn't notice the change and redraw events and other effects are not occurring.
const [selected, setSelected] = React.useState<Set<number>>(new Set());
function onSelect(ev: SyntheticEvent<>, checked: boolean, event_id: number) {
setSelected((selected) => {
if (checked) {
if (!selected.has(event_id)) {
selected.add(event_id);
}
} else {
if (selected.has(event_id)) {
selected.delete(event_id);
}
}
return new Set(selected);
})
}
How do I tell react "hey I've updating xyz state variable"?
You can use useCallback like so
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
From the docs: useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
Although you can use useRef like so
function useEffectOnce(cb) {
const didRun = useRef(false);
useEffect(() => {
if(!didRun.current) {
cb();
didRun.current = true
}
})
}
object ref does not notify react about changes to the current ref value.
I'm trying to access state from useState hook but it is giving me the initial state even after I have modified it.
const quotesURL = "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";
function QuoteGenerator() {
const [quotes, setQuotes] = useState([]);
const [currentQuote, setCurrentQuote] = useState({ quote: "", author: "" });
useEffect(() => {
axios(quotesURL)
.then(result => {
console.log(result);
setQuotes(result.data);
})
.then(() => {
console.log(quotes);
});
}, []);
console.log(quotes) is returning empty array instead of array of objects
Here's how you should do it:
const quotesURL = "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";
function QuoteGenerator() {
const [quotes, setQuotes] = useState([]);
const [currentQuote, setCurrentQuote] = useState({ quote: "", author: "" });
useEffect(() => { // THIS WILL RUN ONLY AFTER YOUR 1ST RENDER
axios(quotesURL)
.then(result => {
console.log(result);
setQuotes(result.data); // HERE YOU SET quotes AND IT WILL TRIGGER A NEW RENDER
})
}, []); // BECAUSE YOU'VE SET IT WITH '[]'
useEffect(() => { // THIS WILL RUN WHEN THERE'S A CHANGE IN 'quotes'
if (quotes.length) {
setSomeOtherState(); // YOU CAN USE IT TO SET SOME OTHER STATE
}
},[quotes]);
}
How this code works:
1st render: You just the the initial states. useEffects are not run yet.
After 1st render: Both effects will run (in that order). The first one will fire the axios request. The second one will do nothing, because quotes has no length yet.
Axios request completes: the thenclause will run and setQuotes will be called to set the new quotes value. This will trigger a re-render.
2nd render: Now the quotes state has beens updated with the new value.
After 2nd render: Only the second useEffect will run, because it's "listening" for changes in the quotes variable that just changes. Then you can use it to set some state like you said.
This is expected. Here's how your code works:
quotes and setQuotes are returned from the useState function.
useEffect runs for the first time once your component is mounted. quotes (empty array) and setQuotes are available within this function.
When your axios request completes, you setQuotes. However, two things: 1 - this doesn't immediately update the value of the state. 2 - within the context of useEffect, quotes is still an empty array - when you do setQuotes(result.data) you're creating a new array, and that will not be directly accessible within this context.
As such, console.log(quotes); will give an empty array.
Depends on what you're trying to use quotes for. Why not just directly work with result.data?
Update: I'm thinking of maybe something like this:
function QuoteGenerator() {
const [quotes, setQuotes] = useState([]);
const [currentQuote, setCurrentQuote] = useState({ quote: "", author: "" });
useEffect(() => {
axios(quotesURL).then(result => {
console.log(result);
setQuotes(result.data);
setSomeOtherState(); // why not do it here?
});
}, []);
}
This way you maintain closer control of the data, without giving it over to lifecycle methods.
Another way you could refactor your code to work:
const quotesURL = "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";
function QuoteGenerator = ({ quote }) => {
const [quotes, setQuotes] = useState([]);
const [currentQuote, setCurrentQuote] = useState({ quote: "", author: "" });
const fetchQuote = async quote => {
const result = await axios.get(quotesURL);
setQuotes(result.data);
};
useEffect(() => {
fetchQuote(quote);
}, [quote]);
};
So now you have a function inside of your QuoteGenerator functional component called fetchQuote. The useEffect hook allows us to use something like lifecycle methods, kind of like combining the componentDidMount and componentDidUpdate lifecycle methods. In this case I called useEffect with a function to be ran everytime this component initially gets rendered to the screen and any time the component update as well.
You see in the other answers, that a second argument is passed as an empty array. I put quote as the first element inside of that empty array as it was passed as a prop in my example, but in others' example it was not, therefore they have an empty array.
If you want to understand why we use an empty array as the second argument, I think the best way to explain it is to quote the Hooks API:
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.
If you pass an empty array ([]), the props and state as inside the effect will always have their initial values. While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model...
In place of setState we call setQuotes and this is used to update the list of quotes and I passed in the new array of quotes which is result.data.
So I passed in fetchQuote and then passed it the prop that was provided to the component of quote.
That second argument of the empty array in useEffect is pretty powerful and not easy to explain and/or understand for everybody right away. For example, if you do something like this useEffect(() => {}) with no empty array as a second argument, that useEffect function will be making non-stop requests to the JSON server endpoint or whatever.
If you use useEffect(() => {}, []) with an empty array, it will only be invoked one time which is identical to using a componentDidMount in a class-based component.
In the example, I gave above, I am instituting a check to limit how often useEffect gets called, I passed in the value of the props.
The reason I did not put the async function inside of useEffect is because it's my understanding that we cannot use useEffect if we are passing an async function or a function that returns a Promise, at least according to the errors I have seen in the past.
With that said, there is a workaround to that limitation like so:
useEffect(
() => {
(async quote => {
const result = await axios.get(quotesURL);
setQuotes(result.data);
})(quote);
},
[quote]
);
This is a more confusing syntax but it supposedly works because we are defining a function and immediately invoking it. Similar to something like this:
(() => console.log('howdy'))()
I created a project with create-react-app,
and I am trying React hooks,
in below example,
the sentence console.log(articles) runs endlessly:
import React, {useState, useEffect} from "react"
import {InfiniteScroller} from "react-iscroller";
import axios from "axios";
import './index.less';
function ArticleList() {
const [articles, setArticles] = useState([]);
useEffect(() => {
getArticleList().then(res => {
setArticles(res.data.article_list);
console.log(articles);
});
},[articles]);
const getArticleList = params => {
return axios.get('/api/articles', params).then(res => {
return res.data
}, err => {
return Promise.reject(err);
}).catch((error) => {
return Promise.reject(error);
});
};
let renderCell = (item, index) => {
return (
<li key={index} style={{listStyle: "none"}}>
<div>
<span style={{color: "red"}}>{index}</span>
{item.content}
</div>
{item.image ? <img src={item.image}/> : null}
</li>
);
};
let onEnd = () => {
//...
};
return (
<InfiniteScroller
itemAverageHeight={66}
containerHeight={window.innerHeight}
items={articles}
itemKey="id"
onRenderCell={renderCell}
onEnd={onEnd}
/>
);
}
export default ArticleList;
Why is it?How to handle it?
React useEffect compares the second argument with it previous value, articles in your case. But result of comparing objects in javascript is always false, try to compare [] === [] in your browser console you will get false. So the solution is to compare not the whole object, but articles.lenght
const [articles, setArticles] = useState([]);
useEffect(() => {
getArticleList().then(res => {
setArticles(res.data.article_list);
console.log(articles);
});
},[articles.length]);
It is simply because you cannot compare two array (which are simply) objects in JavaScript using ===. Here what you are basically doing is comparing the previous value of articleName to current value of articleName, which will always return false. Since
That comparison by reference basically checks to see if the objects given refer to the same location in memory.
Here its not, so each time re-rendering occurs.
The solution is to pass a constant like array length or if you want a tight check then create a hash from all element in the array and compare them on each render.
This is a gotcha with the React's useEffect hook.
This kind of similar to how Redux reducers work. Every time an object is passed in as the second argument inside of a useEffect array, it is considered a different object in memory.
For example if you call
useEffect(() > {}, [{articleName: 'Hello World'}])
and then a re-render happens, it will be called again every time. So passing in the length of the articles is a way to bypass this. If you never want the function to be called a second time, you can pass in an empty array as an argument.
I ran into this with an array objects. I found just passing [array.length] as the second argument did not work, so I had to create a second bit of state to keep up with array.length and call setArrayLength when manipulating the array.
const = [array, setArray] = useState([]);
const = [arrayLength, setArrayLength] = useState(array.length)
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(// whatever you need);
setArray(result.data)
};
fetchData();
}, [arrayLength]);
const handleArray () => {
// Do something to array
setArrayLength(array.length);
}
There is a deep comparison utilizing useRef that you can see the code here. There is an accompanying video on egghead, but it's behind a paywall.
How to make it stop running infinitely:
Replace your useEffect hook with:
// ...
useEffect(() => {
getArticleList().then(res => {
setArticles(res.data.article_list);
console.log(articles);
});
},[JSON.stringify(articles)]); // <<< addition
// ...
Explanation:
Before we understand why it runs infinitely or why we did that addition to stop it, we must understand 3 things:
Arrays are JavaScript objects.
JavaScript compares objects based on reference, not value. Meaning:
const arr1 = [];
const arr2 = [];
console.log(arr1 === arr2) // false
arr1 and arr2 are not equal because their references are different (they're 2 different instances). The same would be true if each was equal to {}, because those are also objects.
React re-runs hooks when a value in the dependency array changes. It does a comparison of the old value and the newly set value.
Why the hook is running infinitely:
[remember] React re-runs the hook when an item in the dependency array changes. A new instance of articles, which is a part of the dependency array, seems to get created (in res.data.article_list) and set to articles (through setArticles). This new instance is not equal to the older one because [remember] JavaScript compares objects based on reference not value. This causes the hook to re-run infinitely.
Why using JSON.stringify() stops it from running infinitely:
Using JSON.stringify() converts to a string, which is a primitive type, and is compared based on value (not reference). If the content of res.data.article_list is the same, then React compares the old stringified version of it with the new one, sees that they're the same, and doesn't re-run the hook.
Extra: Why using JSON.stringify(arr) in the dependency array is better than arr.length:
Doing arr.length returns a number which is a primitive value. JS doesn't compare primitive values by reference, it compares them by value. I.e. 0 === 0 // true, so React won't re-run it if arr.length doesn't change, , even if the array content did change.
So if we have articles = ['earth is melting'] and then we set a new value to articles[0] like articles = ['earth is healing']. It will think it's the same and will not re-run the hook, because both articles.lengths evaluate to 1.
['earth is melting'].length === ['earth is healing'].length // true
Using JSON.stringify(arr), on the other hand, converts the array to a string (e.g. '[]').
"['earth is melting']" === "['earth is healing']" // false
So JSON.stringify(arr) will re-run useEffect every time the actual content OR length of the array changes. Meaning that it's more specific.