useEffect with async function being called in a loop - javascript

My situation is this:
export default function Component({ navigation }) {
const [ item, setItem ] = useState([]);
useEffect(() => {
AsyncStorage.getItem('someItem')
.then(data => JSON.parse(data))
.then(jsonData => {
setItem(jsonData);
})
.catch(error => {});
}, [item]);
The problem is that useEffect seems to be called in a loop, even when "item" doesn't change. If I remove item from the dependencies array, it gets called once only and when item changes the component does not re-render. Is there a solution?
Solved like this:
export default function Component({ navigation }) {
const [ item, setItem ] = useState([]);
const [ update, setUpdate ] = useState(false);
const handleUpdate = () => {
setUpdate(!update);
}
useEffect(() => {
AsyncStorage.getItem('someItem')
.then(data => JSON.parse(data))
.then(jsonData => {
setItem(jsonData);
})
.catch(error => {});
}, [update]);
And then calling handleUpdate (or passing it to a child component and letting the child call it) when I want to update item's state.

You have an infinite loop:
The second argument you send to useEffect() is an array of dependencies. Every time one of these dependencies change - the first argument to useEffect() which is a callback will be invoked. So here you do:
Every time item changes - run code that changes item (because the callback sets value with setItem)
notice: useEffect will also invoke the callback sent to it once at first.

The issue is the same described in this post. When you use an object as a dependency, useEffect thinks it's different on every render. Since you use an array, which is an object, you get the infinite loop. Let's say your state variable was instead a primitive type like a string or a number, then it would not keep rendering as useEffect would be able to figure out that the value has not changed.
So, a possible solution in your specific case, because there is a JSON string being returned, could be using that JSON string as a parallel state variable to check for changes.
Consider something like this:
const simulateCall = () => Promise.resolve('["ds","1234sd","dsad","das"]')
export default function App() {
const [state, setArrayState] = React.useState([])
const [stringState, setStringState] = React.useState('')
React.useEffect(() => {
simulateCall()
.then(data => {
setStringState(data);
setArrayState(JSON.parse(data));
})
.catch(error => console.log(error));
}, [stringState]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{state.map((item, i) => <div key={i}>{item}</div>)}
</div>
);
}

Related

React useEffect executing last function

I need to have 2 different functions that update 2 different components only once in the beginning. Hence, I'm using useEffect. The code is as follows
const loadCategories = () => {
getCategories().then((c) => setValues({ ...values, categories: c.data }));
}
const loadStores = () => {
getStores().then((c) => setValues({ ...values, stores: c.data }));
}
useEffect(() => {
loadStores();
loadCategories();
}, []);
Both the functions are setting the values of the dropdown elements
The problem is though both functions are exectued, only loadCategories() function logic is reflected in the UI. How to make both functions logic reflect in the UI?
first better practice to add those function in useEffect or to wrap them in useCallback hook.
second both or your function are promises so each may not resolve at same time and when you trying to update state values will keep it initial value that why your first function is not reflecting in the ui instead use setState callback to get the previous state like this :
useEffect(() => {
const loadCategories = () => {
getCategories().then((c) => setValues(prevState=>({ ...prevState, categories: c.data })));
}
const loadStores = () => {
getStores().then((c) => setValues(prevState=>({ ...prevState, stores: c.data })));
}
loadStores();
loadCategories();
}, []);
Promise and useEffect can be challenging as the component might dismount before you promise is full-filled.
Here is a solution which works quite well:
useEffect(() => {
let isRunning = true;
Promise.all([
getCategories(),
getStores()
]).then(([stores, categories]) => {
// Stop if component was unmounted:
if (!isRunning) { return }
// Do anything you like with your lazy load values:
console.log(stores, categories)
});
return () => {
isRunning = false;
}
}, []);
Wait for both promises to resolve and then use the data from both to update your state, combined in whatever way you see fit.
useEffect(() => {
Promise.all([getCategories(), getStores()]).then(([categories, stores]) => {
setValues({categories, stores})
});
}, []);
The problem with what you had before (as you experienced) is that values is always the value at the point at which useState was run on this render.
If you really want to do the updates separately, than you can look into useReducer: https://reactjs.org/docs/hooks-reference.html#usereducer
You are lying to React about dependencies. Both your functions depend on the values state variable. That's also why it does not work: When the hooks get run, values gets closured, then when setValues runs values changes (gets set to a new object), however inside the closure it is still referencing the old values.
You can easily resolve that by passing a callback to setValues, this way you do not have a dependency to the outside and the values update is atomic:
setValues(values => ({ ...values, stores: c.data }));

Default value of state is not updated between renders

Consider following example. Im calling a fetching function in parent from child.
The request finishes, Im passing the data from async request to my Child as items prop. Im setting it as a default value for useState - React.useState(items);.
Expected behavior: request finishes, items is updated, Child gets new props, its re-rendered and the default value in useState is updated. So the a variable hold the proper object.
Actual behavior: the default value in useState is not updated between renders. Why?
const Child = ({ fn, items }) => {
const [a, b] = React.useState(items);
console.log(a, items); // a is null but items is an object
React.useEffect(() => {
fn();
}, []);
return JSON.stringify(a);
}
const App = () => {
const [stateOne, setStateOne] = React.useState(null);
const fn = () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => {
setStateOne(json);
});
}
console.log(stateOne)
return <Child fn={fn} items={stateOne} />;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
The default value is intentionally only used when the component mounts. That's what's meant by "default". After that, the only way to change the state is by calling the state-setter function (b in your code).
It's rare that you need to copy a prop into state, so the likely fix is to just delete the state entirely, and use the prop.
const Child = ({ fn, items }) => {
React.useEffect(() => {
fn();
}, []);
return JSON.stringify(items);
}
If you do need to have state for some reason, first consider if you can move that state up to the parent and thus eliminate this issue. If for some reason you can't do that either, then you'll need to have the child implement logic which calls the state setter b when you want it to be called. For example:
const Child = ({ fn, items }) => {
const [a, b] = React.useState(items);
React.useEffect(() => {
b(items);
}, [items]);
React.useEffect(() => {
fn();
}, []);
return JSON.stringify(a);
}
You misunderstood what it means to have a "default value" in the case of React hooks. It's not so much a "default value" as an "initial value".
Consider the following code:
let n = Math.random();
const [myN, setMyN] = useState(n);
console.log(n) // This number will always be the same between renders
In your case, the initial value is null. It will never change unless if you call the setter for it.
If you're looking to pass down a prop, don't mix it into state.

React: Why can't I pass data from one functional component to another functional component?

I am a bit lost as to why I can't pass fetched data between two functional components. I tried to use Hooks and it returned an error message suggesting to use array for object[Promise]. Any idea what I missed here? I'm pretty new to React. Any help would be appreciated!
// Component A
function App() {
const apiURL = "https://services9.arcgis.com/M4Su6cnNT6vqupbh/arcgis/rest/services/COVID19_Data/FeatureServer/0/query?where=1%3D1&outFields=*&outSR=4326&f=json";
const apiData = fetch(apiURL)
.catch(err=>console.log(err))
.then(data=>console.log(data));
return (
<div>
<h1>Hello</h1>
<DataVisualize data={apiData}/>
</div>
);
}
// Component B
function DataVisualize(props){
return <div>{props.data}</div>;
}
Your apiData does not contain the actual data you want - it's only a Promise. Its catch is also in the wrong place (put the .catch after the .thens), and you need to call .json or .text on the Response (returned by the resolve value of fetch) in order to get a Promise that resolves to the actual data you need.
Set some state when the final data is retrieved instead:
// Component A
function App() {
const apiURL = "https://services9.arcgis.com/M4Su6cnNT6vqupbh/arcgis/rest/services/COVID19_Data/FeatureServer/0/query?where=1%3D1&outFields=*&outSR=4326&f=json";
const [data, setData] = useState();
useEffect(
() => {
fetch(apiURL)
.then(res => res.json())
.then(setData)
.catch(console.error); // handle errors
},
[] // run this only once, on mount
);
return (
<div>
<h1>Hello</h1>
{ data && <DataVisualize data={data}/> }
</div>
);
}
// Component B
function DataVisualize(props){
return <div>{props.objectIdFieldName}</div>;
}
Also note that the result doesn't appear to have any .data property (though it does have a objectIdFieldName property, so I put that in as an example).

How to set a state value to an array in react native?

I have a functional component, ListKeys. When it loads I want to set an empty array of keys equal to a list of all keys extracted from storage. Here is what I've got at the moment:
const ListKeys = props => {
const [keys, setKeys] = useState([]);
const [areKeysLoaded, setAreKeysLoaded] = useState(false);
useEffect(() => {
if(!areKeysLoaded){
loadSavedKeys();
setAreKeysLoaded(true);
console.log(keys)
}
});
async function loadSavedKeys(){
try {
var allKeys = await AsyncStorage.getAllKeys();
console.log(allKeys);
setKeys(allKeys);
}
catch {
console.log("Error: Cannot access saved data.");
}
}
return (
<View></View>
);
};
export default ListKeys;
This code correctly gets the list of keys and outputs it to the console. This is done on line 16: console.log(allKeys);
However, when I then setKeys(allKeys);, this doesn't work. I know this because line 9: console.log(keys) outputs an empty array.
I'm guessing I can't just set a state value array to another array but I'm not experienced enough with JS or React Native to know why.
Can someone tell me how to properly set the keys array to the allKeys array?
You need to wait for the loadSavedKeys to resolve first before trying to set since it is async:
const ListKeys = props => {
const [keys, setKeys] = useState([]);
const [areKeysLoaded, setAreKeysLoaded] = useState(false);
useEffect(() => {
if (areKeysLoaded) return
AsyncStorage
.getAllKeys()
.then((keys) => {
setAreKeysLoaded(true)
setKeys(keys)
})
.catch(e => console.error(e));
}, []);
return (
<View></View>
);
};
export default ListKeys;
Note I simplified the code a bit since it seemed a little more verbose then necessary. They key take away is you need to wait for getAllKeys to resolve before you'll get the keys since it is asynchronous.
Also, you prob don't need areKeysLoaded if you just want this to run once, but I guess you could use it instead for a loading indicator?

With a React Hooks' setter, how can I set data before the component renders?

I export a JS object called Products to this file, just to replace a real API call initially while I am building/testing. I want to set the function's state to the object, but mapped. I have the component looking like this:
function App() {
const [rooms, setRooms] = useState([]);
const [days, setDays] = useState([]);
const roomsMapped = products.data.map(room => ({
id: room.id,
title: room.title
}))
useEffect(() => {
setRooms(roomsMapped);
})
return ( etc )
This returns the following error: Error: Maximum update depth exceeded.
I feel like I'm missing something really obvious here, but am pretty new to React and Hooks. How can I set this data before the component renders?
Just declare it as initial value of rooms
const Component = () =>{
const [rooms, setRooms] = useState(products.data.map(room => ({
id: room.id,
title: room.title
})))
}
You can also use lazy initial state to avoid reprocessing the initial value on each render
const Component = () =>{
const [rooms, setRooms] = useState(() => products.data.map(room => ({
id: room.id,
title: room.title
})))
}
Change useEffect to this
useEffect(() => {
setRooms(roomsMapped);
},[])
With Lazy initialisation with function as a parameter of useState
import React, { useState } from "react";
function App() {
const [rooms, setRooms] = useState(() => {
// May be a long computation initialization
const data = products.data || [];
return data.map(({ id, title }) => ({ id, title }));
});
return (
// JSX stuffs
)
}
You can use default props for this.set initial value with empty list .
You are getting 'Error: Maximum update depth exceeded', because your useEffect function doesn't have dependency array. Best way to fix this is to pass empty array as the second argument to useEffect like this:
useEffect(() => {
setRooms(roomsMapped);
},[]) <= pass empty array here
this will prevent component to re render, it you want your component to re render on props change you can pass the props in the array like this:
useEffect(() => {
setRooms(roomsMapped);
},[props.props1,props.props2])
here you can pass as many props as you want...

Categories