Updating state in async function - javascript

I'm very new to react so apologies if this question has already been answered or should be phrased differently. I have a functional component that fetches a .json file from my public folder in an async function (loadData()). Using the developer tools in my chrome window, I can see that the function gets me exactly what I want, yet the state doesn't seem to want to update when I use setData.
Edit:
So I think I know what the problem is, which is that the first time the component renders, the variable source needs that JSON object which won't be there until the component re-renders. If that's the case, should all the code starting at let source = pickRandomVerb(data) go somewhere outside useEffect()?
function ComposedTextField() {
const classes = useStyles();
const [data, setData] = React.useState([]);
const [displayVerb, setDisplayVerb] = React.useState('');
const pickRandomVerb = (list) => {
var obj_keys = Object.keys(list);
var ran_key = obj_keys[Math.floor(Math.random() * obj_keys.length)];
return list[ran_key];
}
const loadData = async() => {
const response = await fetch('verbs.json');
const json = await response.json();
setData(json);
console.log(json); //json is exactly what I want here
console.log(data); //data is just '[]' here
}
useEffect(() => {
loadData();
console.log(data) //data is also '[]' here
let source = pickRandomVerb(data)
let verbSource = source.infinitive;
let showVerb = verbSource.toString().replaceAll("\"", "");
setDisplayVerb(showVerb)
}, [])
return(
<div>
<Typography className = {classes.form}>
<p>{displayVerb}</p>
</Typography>
</div>
)
}
Can anyone let me know what I'm missing? Thanks in advance.

useState hook is also asynchronous, and will not be reflected immediately and this question already addressed link-1, link-2
changes required (you have to listen the changes of the data in useEffect),
useEffect(() => {
loadData();
console.log(data) //data is also '[]' here
}, []);
useEffect(() => {
let source = pickRandomVerb(data)
let verbSource = source.infinitive;
let showVerb = verbSource.toString().replaceAll("\"", "");
setDisplayVerb(showVerb)
}, [data]);

Maybe you can try something like this
function ComposedTextField() {
const classes = useStyles();
const [data, setData] = React.useState([]);
const [displayVerb, setDisplayVerb] = React.useState('');
const pickRandomVerb = (list) => {
var obj_keys = Object.keys(list);
var ran_key = obj_keys[Math.floor(Math.random() * obj_keys.length)];
return list[ran_key];
}
const loadData = async() => {
const response = await fetch('verbs.json');
const json = await response.json();
setData(json);
console.log(json); //json is exactly what I want here
console.log(data); //data is just '[]' here
}
useEffect(() => {
loadData();
console.log(data) //data is also '[]' here
}, [])
// Move that code outside useEffect and inside condition
if(data!==[]){
let source = pickRandomVerb(data)
let verbSource = source.infinitive;
let showVerb = verbSource.toString().replaceAll("\"", "");
setDisplayVerb(showVerb)
}
return(
<div>
<Typography className = {classes.form}>
<p>{displayVerb}</p>
</Typography>
</div>
)
}
In this code setDisplayVerb() will be called when the data has some value. The flow of the control would be something like this.
Component is mounted with data=[]
usEffect is called which in turns calls loadData()
loadData will set the value of data using setData(json)
When data is set it will cause re-render, since data has value this time, the if
condition is satisfied and the statements inside it will be executed.
setDisplayVerb() will cause re-render of the application.
Since the dependency array of useEffect is empty it will not be called again.
This way you should be able to see the data on your screen.

so to expand on my comment, try this:
const loadData = async() => {
const response = await fetch('verbs.json');
const json = await response.json();
return json; /// <<< note: here you return data
}
useEffect(async () => {
const data = await loadData(); // <<<< note: here you wait for data
console.log(data);
let source = pickRandomVerb(data)
let verbSource = source.infinitive;
let showVerb = verbSource.toString().replaceAll("\"", "");
setDisplayVerb(showVerb)
}, [])
this will only get you the data on initial load.
Just as a side note: to improve a bit on this component, you can move your loadData function outside of the component.

setState is async also then you can't see the data changed in
const loadData = async() => {
const response = await fetch('verbs.json');
const json = await response.json();
setData(json);
console.log(json); //json is exactly what I want here
console.log(data); //data is just '[]' here
}
Your useEffect doesn't define the dependency then it just run once. If you want to see your data changed just add dependency for data to track when data changed.
/// Just load at the first time you render you component
useEffect(()=>{
loadData();
},[])
useEffect(() => {
console.log(data)
let source = pickRandomVerb(data)
let verbSource = source.infinitive;
let showVerb = verbSource.toString().replaceAll("\"", "");
setDisplayVerb(showVerb)
}, [data]) //Just add dependency here for useEffect

Related

useEffect hook runs infinitely when used in a custom hook

Below is my custom hook, I'm trying to handle everything from the hook. I have seen similar questions but none seems to work for my case and I have been made to believe there's a solution for this approach, jus can't figure it out.
const useResource = (baseUrl) => {
const [resources, setResources] = useState([]);
const create = async (resource) => {
const response = await axios.post(baseUrl, resource)
setResources(resources.concat(response.data));
console.log(resources)
return response.data
}
const get = async () => {
const response = await axios.get(baseUrl);
setResources(response.data)
return response.data
}
const service = {
create,
get
}
return [
resources, service
]
}
Here is my approach to use the custom hook, but request keeps looping nonstop, please how do I stop it running after every render?
const App = () => {
const content = useField('text');
const name = useField('text');
const number = useField('text');
const [notes, noteService] = useResource('http://localhost:3005/notes');
const [persons, personService] = useResource('http://localhost:3005/persons');
useEffect(() => {
noteService.get();
}, [noteService])
useEffect(() => {
personService.get();
}, [personService])
const handleNoteSubmit = (event) => {
event.preventDefault();
noteService.create({ content: content.value });
}
const handlePersonSubmit = (event) => {
event.preventDefault();
personService.create({ name: name.value, number: number.value});
}
Edit: I just had to disable ESLint for the dependency line, because I just need it to run once after every render. Works well!
useEffect(() => {
noteService.get();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
personService.get();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
As correctly pointed out in comments, each time the component renders and calls your useResource hook, a new service object is created. If this service object is used as a dependency for any other hooks this will trigger their callbacks.
The solution is to memoize the service object so it's being provided as a stable reference. This can be accomplished via the useMemo hook. Because service will be memoized, the create callback will also be memoized and contain stale resources state. To address this update create to use a functional state update when appending new response data to the existing state.
Example:
import { useEffect, useMemo, useState } from 'react';
const useResource = (baseUrl) => {
const [resources, setResources] = useState([]);
const create = async (resource) => {
const response = await axios.post(baseUrl, resource);
// Functional update to update from previous state
setResources(resources => resources.concat(response.data));
return response.data;
}
const get = async () => {
const response = await axios.get(baseUrl);
setResources(response.data);
return response.data;
}
const service = useMemo(() => ({
create,
get
}), []);
return [resources, service];
};

How to fetch up-to-date data inside async?

I have a function which fetches data.
const fetchData = async (filter) => {
if (loading) return
loading = true
const data = await api(filter)
setData(data)
loading = false
}
I also have a filter component, when I change filters it calls my fetchData() function with the new filter variable.
This all works, however there is an issue
This issue occurs when I switch my filter but my fetching function is in a loading state. This causes the if check to fail and I now see outdated data because a re-fetch never happens.
My initial idea was to create a const q = [] variable, and inside if (loading) i would push my filters, and somehow at the end I would re-fetch with the last element inside my q array and then clear that array.
I dont really know how to do that re-fetch logic. A setInterval(() => checkQ(), 1000)? Doesn't seem right
What would be a better approach to take?
You should use an AbortController - that's part of the fetch, as my experience tells me that it's not hard to initiate a new fetch request, but what to do with the first request, when you send out a second?
Here's a snippet that will do the thing you asked - but also deals with the unnecessary requests:
const { useState, useEffect } = React
const useFetchData = () => {
const [users, setUsers] = useState([])
let controller = null
const fetchData = () => {
console.log('fetch initiated')
if (controller) controller.abort()
controller = new AbortController();
const { signal } = controller;
fetch('https://jsonplaceholder.typicode.com/users', { signal })
.then(response => {
console.log('request response')
return response.json()
})
.then(json => {
console.log('retrieved list:', json)
setUsers(() => json || [])
})
.catch(err => {
if(err.name === "AbortError") {
console.warn('Abort error', err)
}
})
}
return { fetchData }
}
const FetchData = () => {
const { fetchData } = useFetchData()
return (
<div>
<button onClick={fetchData}>FETCH DATA</button><br />
</div>
)
}
const FetchAbortFetchData = () => {
const { fetchData } = useFetchData()
return (
<div>
<button onClick={() => {
fetchData()
fetchData()
}}>FETCH-ABORT-FETCH DATA</button><br />
</div>
)
}
const App = () => {
return (
<div>
<FetchData /><br />
<FetchAbortFetchData />
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Easiest way is to use your filter criteria as the lock.
Advantages
Always fetching data immediately
Only calling setData with the results from the most recent filter criteria
Simple
Disadvantages
May have multiple simultaneous requests
Toggling back/forth between filters may lead to a race condition.
let latestFilter = null;
const fetchData = async (filter) => {
// Check to see if its there's the same request already in progress
// Note: may need to deep equal to check the filter
if (filter === latestFilter) return;
// Set the current request as most up to date
latestFilter = filter
// Fetch Data (async)
const data = await api(filter)
// If the filter is still the most up to date, use it. Otherwise discard.
// Note: may need to deep equal to check the filter
if (filter === latestFilter) {
setData(data)
latestFilter = null
}
}
In order to solve disadvantage 2, you can include a counter. This will ensure that only the most recent request will run setData.
let latestFilter = null;
let latestRequestNumber = 0
const fetchData = async (filter) => {
if (filter === latestFilter) return;
latestFilter = filter
// Identify the current request
const requestNumber = latestRequestNumber + 1;
// Store this number
latestRequestNumber = requestNumber
const data = await api(filter)
// Update data only if we are the most recent request.
if (callCount = latestCallCount) {
setData(data)
latestFilter = null
}
}

Trying to filter by dynamically added property/value in object array and save to state

I was wondering if anybody could help, I have a function that when it runs it will do a fetch and save the object array it gets to a variable, it will then loop through that object array and do another fetch and based on some data in that fetch it will add a new property/value to each object inside the array, I have that working fine, however I then want to filter the array by that new properties value and set the updated object array to state, the problem is the filter doesn't seem to be working, all I get back is the unfiltered data.
Code below, any help would be amazing.
import { useState, useEffect } from 'react'
import ResultsPage from './components/ResultsPage'
const App = () => {
const [results, setResults] = useState([])
const searchedRef = useRef()
useEffect(() => {
console.log(results)
}, [results])
const handleSearch = async () => {
let searched = searchedRef.current.value
let res = await fetch(`someAPI/${searched}`)
let data = await res.json()
searchResults = data
res = null
data = null
searchResults.map(async (element) => {
res = await fetch(`someAPI/${element}`)
data = await res.json()
element.name = data.name;
});
searchResults = searchResults.filter(element => element.name !== null);
setResults(searchResults)
}
return (
<>
<input type="text" ref={searchedRef} onKeyDown={handleSearch}/>
<ResultsPage results={results} />
</>
)
}
export default App
The map will not wait for each fetch to be done.
Async functions evaluate to Promises, so the mapping will evaluate to an array of Promises.
You need to use Promise.all to wait on an array of Promises.
await Promise.all(searchResults.map(/* ... */))
Example here.

React Native - let return() wait until variable is defined

I am getting data in an asynchronous way and consequently showing it inside the render() function using {data}.
Now my question is, how do I let the render() function wait until the variable is defined? Currently either the placeholder variable doesn't change or it never arrives.
This is my code,
let [data] = useState();
let storeData = async (value) => {
try {
await AsyncStorage.setItem('locatie', value)
} catch (e) {
console.log(e)
}
}
let getInfo = async () => {
data = await AsyncStorage.getItem('locatie');
return data;
}
useEffect(() => {
getInfo().then(r => console.log('getInfo called, data var = ' + data))
})
console.log('return() initiate, data var = ' + data)
And showing it here:
<TextInput value={data !== undefined ? data : 'placeholder'} onChangeText={(value) => storeData(value)}/>
Evidently, since it's asynchronous, render() happens before the function.
Any help would be appreciated. Please no flame I use React Native first time :)
Also, please note that it's inside a function, I am not using classes and will not use them.
You should learn more about using react hooks. Anyway, you can refer to below for your issue.
const [data, setData] = useState('');
const storeData = async (value) => {
try {
setData(value);
await AsyncStorage.setItem('locatie', value);
} catch (e) {
console.log(e);
}
};
const getInfo = async () => {
const res = await AsyncStorage.getItem('locatie');
setData(res);
};
useEffect(() => {
getInfo();
}, []);
Instead of let [data] = useState();
write const [data, setData] = useState();
and update the state inside the useEffect hook inside a then()
in your case, after the promise returns some data.

How come my state isn't being filled with response?

console.log(data) gives me an object with the correct data but, I set rates to that data but when console logging rates I get an empty object {}. Thanks in advance.
const [rates, setRates] = useState({});
useEffect(() => {
search();
}, []);
const search = async () => {
const response = await axios.get('https://api.exchangeratesapi.io/latest');
const data = response.data.rates;
console.log(data);
setRates(data);
console.log(rates);
};
As someone said in the comment, state updates will be reflected in the next render. Also, there are some problems with your code I'll address.
const [rates, setRates] = useState({});
useEffect(() => {
// move this in here unless you plan to call it elsewhere
const search = async () => {
const response = await axios.get('https://api.exchangeratesapi.io/latest');
const data = response.data.rates;
setRates(data);
};
search();
}, [/* no dependencies means it runs once */]);
If you do plan on calling search elsewhere, wrap it in a useCallback hook so you can set it as a dependency of your useEffect (you'll get a lint warning without). useCallback with an empty dependency array will always return the same function reference, so your useEffect will only ever run the one time like it does now.
If you leave search as a normal function in the component, the reference changes each render, so your effect would run every render if you included it as a dependency.
const [rates, setRates] = useState({});
const searchCallback = useCallback(async () => {
const response = await axios.get('https://api.exchangeratesapi.io/latest');
const data = response.data.rates;
setRates(data);
}, [])
useEffect(() => {
// move this in here unless you plan to call it elsewhere
search();
}, [searchCallback]);

Categories

Resources