useState hook has stale data - javascript

I'm using useState hook to set a value from a dropdown to a variable. In my particular instance inside onDeviceIdChange, I'm setting setDeviceId to value which is emitted from an event in the component library I'm using. value holds the correct value I'm expecting, however it's not update to deviceId immediately.
I've read the various posts related to this issue and the most common solution seems to be passing the variable to the second argument for useEffect
So something like this
useEffect(() => {
fetchDeviceIds();
console.log("call back for device id selection", deviceId);
}, [deviceId]);
But that seems to just fire the fetchDeviceIds function repeatedly for me and never actually call the change handler. I've tried a few variations with useEffect and haven't been able to get the updated value inside deviceId
Below was my initial implementation without putting deviceId into the second argument of useEffect
import React, { useState, useEffect } from "react";
import axios from "axios";
import { Dropdown, Form, Input } from "semantic-ui-react";
const Example = props => {
const [deviceId, setDeviceId] = useState("");
const [hardware, setHardware] = useState([]);
const [error, setError] = useState("");
const [versionNum, setVersionNum] = useState("");
const [isLoading, setIsLoading] = useState(true);
async function fetchDeviceIds() {
const url = "/api/hardware";
try {
const response = await fetch(url, {
method: "GET"
});
if (!response.ok) {
setError(response.statusText);
}
const data = await response.json();
console.log(data);
setHardware(data.hardware);
} catch (error) {
console.log("error catch", error);
console.log("Handle networking errors", error.message);
}
}
const versionUrl = "/api/versionUrl";
const onDeviceIdChange = async (e, { value }) => {
console.log("device id value --> ", value);
setDeviceId(value);
console.log(deviceId); //this won't update immediately
if (!deviceId) {
handleClear();
return;
}
try {
console.log("calling versionUrl");
const response = fetch(versionUrl, {
method: "POST",
body: JSON.stringify({
deviceId: deviceId
}),
headers: {
"Content-Type": "application/json"
}
});
if (!response.ok) {
setError(response.statusText);
}
const data = response.json();
console.log(data);
setVersionNum(data.version_num);
setIsLoading(false);
} catch (error) {
console.log("error catch", error);
console.log("Handle networking errors", error.message);
}
}
};
useEffect(() => {
fetchDeviceIds();
}, []);
const handleClear = () => {
setDeviceId("");
setVersionNum("");
setIsLoading(true);
};
return (
<>
<Form.Field required>
<label style={{ display: "block" }}>device Id</label>
<Dropdown
id="deviceId"
value={deviceId}
onChange={onDeviceIdChange}
selection
fluid
placeholder="Please select an device ID"
clearable
options={hardware.map(device => {
return {
text: device.id,
value: device.id,
key: device.id
};
})}
style={{ marginTop: ".33", marginBottom: "2em" }}
/>
</Form.Field>
<Form.Field>
<label style={{ display: "block", marginTop: "2em" }}>Version Number</label>
{isLoading ? (
<Input
loading
iconPosition="left"
fluid
placeholder="Choose an device ID..."
value={versionNum}
/>
) : (
<Input value={versionNum} fluid readOnly />
)}
</Form.Field>
</>
);
};
export default Example;

onDeviceIdChange should look like this:
const onDeviceIdChange = async (e, { value }) => {
// code for validating value etc
setDeviceId(value);
}
and the rest of the code should go into useEffect hook that is dependant on deviceId:
useEffect(fetchId, [deviceId]);

setDeviceId(value);
console.log(deviceId); //this won't update immediately because setDeviceId is asynchronous!
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value. There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.
React does not update the state and when you console.log you get the previous value
In your case, you can use the value for all synchronous actions since it is the new value you are setting OR you can move all the code below setState to inside a useEffect
Update
From the code below and the docs
onChange={onDeviceIdChange}
The second argument would be an object of the shape {data: value}
so your argument value will always be undefined!
const onDeviceIdChange = async (e, {value}) => {
console.log("device id value --> ", value);
console.log("device value from event --> ", e.target.value);
setDeviceId(value);
};
useEffect(() => {
if (!deviceId) {
handleClear();
return;
}
try {
console.log("calling versionUrl");
const response = fetch(versionUrl, {
method: "POST",
body: JSON.stringify({
deviceId: deviceId
}),
headers: {
"Content-Type": "application/json"
}
});
if (!response.ok) {
setError(response.statusText);
}
const data = response.json();
console.log(data);
setVersionNum(data.version_num);
setIsLoading(false);
} catch (error) {
console.log("error catch", error);
console.log("Handle networking errors", error.message);
}
}
},[deviceId])

Related

React custom hook does not give result of POST

The custome hook post method working fine at the same time the response adding state taking time.
console.log(jsonResult)
shows the response of POST method at the same time responseData shows null
usePostQuery
import { useCallback, useState } from "react";
interface bodyData {
message: string,
author: string
}
const usePostQuery = (url: string, data?: bodyData )=> {
const [responseData, setResponseData] = useState();
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState('');
const callPost = useCallback( async (data: bodyData) => {
try {
setLoading(true);
const response = await fetch(url, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: data.message,
userId: 15
})
});
const jsonResult = await response.json();
console.log('--------jsonResult---------');
console.log(jsonResult)
setResponseData(jsonResult);
} catch (error: any) {
setError(error.message);
} finally {
setLoading(false);
}
},
[url]
);
return { responseData, loading, error, callPost };
};
export default usePostQuery;
const { responseData, loading, error, callPost } = usePostQuery('https://jsonplaceholder.typicode.com/posts')
The responseData is not giving post call response
useEffect(() => {
if (draftMessage && myMessage) {
// submitNewMessage()
console.log("post my message to server");
callPost({
message: myMessage,
author: "Mo"
});
if (loading === false) {
setMyMessage("");
setdraftMessage(false);
console.log("after ", responseData);
}
console.log("responseData ", responseData);
}
}, [draftMessage, myMessage]);
The fetch is successful because the console in side fetch response shows the API response.
There's nothing wrong with your custom hook. The issue is in your effect hook.
It only triggers when its dependencies change, ie draftMessage and myMessage. It does not re-evaluate loading or responseData so will only ever see their states at the time it is triggered.
It's really unclear what you're using the draftMessage state for. Instead, I would simply trigger the callPost in your submit handler...
export default function App() {
const [myMessage, setMyMessage] = useState("");
const { responseData, loading, callPost } = usePostQuery(
"https://jsonplaceholder.typicode.com/posts"
);
const handleMyMessage = (val) => {
setMyMessage(val);
};
const handleSubmit = async (event) => {
event.preventDefault();
await callPost({ message: myMessage, author: "Mo" });
setMyMessage("");
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{loading ? (
<p>Loading...</p>
) : (
<ChatForm
onChange={handleMyMessage}
myMessage={myMessage}
handleSubmit={handleSubmit}
/>
)}
<pre>responseData = {JSON.stringify(responseData, null, 2)}</pre>
</div>
);
}
Your hook controls the loading and responseData states so there's really very little for your components to do.

My useFetch hook is giving me too many re-renders

I'm trying to abstract my fetch out to a hook in my expo react native app. The fetch must be able to do POST method. I started out by trying to use and then modify the useHook() effect found at https://usehooks-ts.com/react-hook/use-fetch. It caused too many re-renders. So in searching Stack Overflow I found this article that recommended that I do it as a function. Since, I anticipate almost every fetch request is going to be a POST and done with a submit, that looked like a good idea. But it is still giving me too many re-renders. Then I found this article that said to do it with a useCallBack. Still no success...
Here is the hook I'm trying to use:
import { useCallback, useState } from "react";
import { FetchQuery } from "../interfaces/FetchQuery";
interface State<T> {
data?: T;
error: Error | string | null;
fetchData: any;
}
const useFetch2 = <T = unknown,>(): State<T> => {
const [data, setData] = useState<T>();
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async (query: FetchQuery) => {
const { url, options } = query;
if (!url) return;
const response = await fetch(url, options);
const data = await response.json();
if (data.error) {
setData(undefined);
setError(data.error);
} else {
setData(data);
setError(null);
}
}, []);
return { fetchData, data, error };
};
export default useFetch2;
This is the component calling the code (right now it assigns all of the data to the error field so I can validate the data coming back):
export default function AdminLogin() {
// screen controls
const [err, setErr] = useState<Error | string | undefined>();
// form controls
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
// fetch information
// const [fetchQuery, setFetchQuery] = useState<FetchQuery>({});
const { fetchData, data, error } = useFetch2<User>();
if (error) {
setErr(error);
}
if (data?.user) {
setErr(JSON.stringify(data.user));
}
const handleSignIn = async () => {
const options = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"Cache-control": "no-cache",
},
body: JSON.stringify({ email, password }),
};
const url = BASE_API + "users/login";
// setFetchQuery({ url, options }); <-- attempted to use this with useEffect
fetchData({ url, options });
};
// useEffect(() => {
// fetchData();
// }, [fetchQuery]);
return (
<View style={styles.container}>
<Text style={styles.msg}>Maps can only be edited by Administers.</Text>
<View style={styles.controlGroup}>
<Text style={styles.UnPw}>Email</Text>
<TextInput
style={styles.txInput}
keyboardType="email-address"
placeholder="Email"
value={email}
onChangeText={(value) => setEmail(value)}
/>
</View>
<View style={styles.controlGroup}>
<Text style={styles.UnPw}>Password</Text>
<TextInput
style={styles.txInput}
maxLength={18}
placeholder="Password"
value={password}
onChangeText={(value) => setPassword(value)}
secureTextEntry
/>
</View>
<TouchableOpacity onPress={handleSignIn}>
<Text>Sign in</Text>
</TouchableOpacity>
{/* {status === "loading" && <Text>Loading...</Text>} */}
{err && (
<>
<Text style={styles.errText}>Error:</Text>
<Text style={styles.errText}>{err}</Text>
</>
)}
</View>
);
}
const styles = StyleSheet.create({});
Adding some console.log commands does show that states are continually updating after the fetch command, but I don't understand why nor how to fix it.
You should not update state directly in the component. This may cause the state to be updated on every render, which forces re-render and creates an infinite render loop.
Instead, state should be updated only in callbacks or useEffect.
Changes you have to make are convert following
if (error) {
setErr(error);
}
if (data?.user) {
setErr(JSON.stringify(data.user));
}
to
useEffect(() => {
if (error) {
setErr(error);
}
if (data?.user) {
setErr(JSON.stringify(data.user));
}
}, [error, data?.user])
Or, even better would be to remove err state and convert it to const variable like
// remove following
// const [err, setErr] = useState<Error | string | undefined>();
// ..some code
const { fetchData, data, error } = useFetch2<User>();
// err is not required to be a state
const err = error || JSON.strigify(data?.user);
You should remove this code which causes infinite re-renders:
if (error) {
setErr(error);
}
if (data?.user) {
setErr(JSON.stringify(data.user));
}
You should put it to useEffect, something like this:
useEffect(() => {
setErr(error);
}, [error])
useEffect(() => {
setErr(JSON.stringify(data.user));
}, [data?.user])
Because that code updates state on every render.

Correct way to cancel async axios request in a React functional component

What is the correct approach to cancel async requests within a React functional component?
I have a script that requests data from an API on load (or under certain user actions), but if this is in the process of being executed & the user navigates away, it results in the following warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Most of what I have read solves this with the AbortController within the componentDidUnmount method of a class-based component. Whereas, I have a functional component in my React app which uses Axois to make an asynchronous request to an API for data.
The function resides within a useEffect hook in the functional component to ensure that the function is run when the component renders:
useEffect(() => {
loadFields();
}, [loadFields]);
This is the function it calls:
const loadFields = useCallback(async () => {
setIsLoading(true);
try {
await fetchFields(
fieldsDispatch,
user.client.id,
user.token,
user.client.directory
);
setVisibility(settingsDispatch, user.client.id, user.settings);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
}
}, [
fieldsDispatch,
user.client.id,
user.token,
user.client.directory,
settingsDispatch,
user.settings,
]);
And this is the axios request that is triggered:
async function fetchFields(dispatch, clientId, token, folder) {
try {
const response = await api.get(clientId + "/fields", {
headers: { Authorization: "Bearer " + token },
});
// do something with the response
} catch (e) {
handleRequestError(e, "Failed fetching fields: ");
}
}
Note: the api variable is a reference to an axios.create object.
To Cancel a fetch operation with axios:
Cancel the request with the given source token
Ensure, you don't change component state, after it has been unmounted
Ad 1.)
axios brings its own cancel API:
const source = axios.CancelToken.source();
axios.get('/user/12345', { cancelToken: source.token })
source.cancel(); // invoke to cancel request
You can use it to optimize performance by stopping an async request, that is not needed anymore. With native browser fetch API, AbortController would be used instead.
Ad 2.)
This will stop the warning "Warning: Can't perform a React state update on an unmounted component.". E.g. you cannot call setState on an already unmounted component. Here is an example Hook enforcing and encapsulating mentioned constraint.
Example: useAxiosFetch
We can incorporate both steps in a custom Hook:
function useAxiosFetch(url, { onFetched, onError, onCanceled }) {
React.useEffect(() => {
const source = axios.CancelToken.source();
let isMounted = true;
axios
.get(url, { cancelToken: source.token })
.then(res => { if (isMounted) onFetched(res); })
.catch(err => {
if (!isMounted) return; // comp already unmounted, nothing to do
if (axios.isCancel(err)) onCanceled(err);
else onError(err);
});
return () => {
isMounted = false;
source.cancel();
};
}, [url, onFetched, onError, onCanceled]);
}
import React from "react";
import axios from "axios";
export default function App() {
const [mounted, setMounted] = React.useState(true);
return (
<div>
{mounted && <Comp />}
<button onClick={() => setMounted(p => !p)}>
{mounted ? "Unmount" : "Mount"}
</button>
</div>
);
}
const Comp = () => {
const [state, setState] = React.useState("Loading...");
const url = `https://jsonplaceholder.typicode.com/users/1?_delay=3000&timestamp=${new Date().getTime()}`;
const handlers = React.useMemo(
() => ({
onFetched: res => setState(`Fetched user: ${res.data.name}`),
onCanceled: err => setState("Request canceled"),
onError: err => setState("Other error:", err.message)
}),
[]
);
const cancel = useAxiosFetch(url, handlers);
return (
<div>
<p>{state}</p>
{state === "Loading..." && (
<button onClick={cancel}>Cancel request</button>
)}
</div>
);
};
// you can extend this hook with custom config arg for futher axios options
function useAxiosFetch(url, { onFetched, onError, onCanceled }) {
const cancelRef = React.useRef();
const cancel = () => cancelRef.current && cancelRef.current.cancel();
React.useEffect(() => {
cancelRef.current = axios.CancelToken.source();
let isMounted = true;
axios
.get(url, { cancelToken: cancelRef.current.token })
.then(res => {
if (isMounted) onFetched(res);
})
.catch(err => {
if (!isMounted) return; // comp already unmounted, nothing to do
if (axios.isCancel(err)) onCanceled(err);
else onError(err);
});
return () => {
isMounted = false;
cancel();
};
}, [url, onFetched, onError, onCanceled]);
return cancel;
}
useEffect has a return option which you can use. It behaves (almost) the same as the componentDidUnmount.
useEffect(() => {
// Your axios call
return () => {
// Your abortController
}
}, []);
You can use lodash.debounce and try steps below
Stap 1:
inside constructor:
this.state{
cancelToken: axios.CancelToken,
cancel: undefined,
}
this.doDebouncedTableScroll = debounce(this.onScroll, 100);
Step 2:
inside function that use axios add:
if (this.state.cancel !== undefined) {
cancel();
}
Step 3:
onScroll = ()=>{
axiosInstance()
.post(`xxxxxxx`)
, {data}, {
cancelToken: new cancelToken(function executor(c) {
this.setState({ cancel: c });
})
})
.then((response) => {
}

Fetch data with a custom React hook

I'm newbie in React but I'm developing an app which loads some data from the server when user open the app. App.js render this AllEvents.js component:
const AllEvents = function ({ id, go, fetchedUser }) {
const [popout, setPopout] = useState(<ScreenSpinner className="preloader" size="large" />)
const [events, setEvents] = useState([])
const [searchQuery, setSearchQuery] = useState('')
const [pageNumber, setPageNumber] = useState(1)
useEvents(setEvents, setPopout) // get events on the main page
useSearchedEvents(setEvents, setPopout, searchQuery, pageNumber)
// for ajax pagination
const handleSearch = (searchQuery) => {
setSearchQuery(searchQuery)
setPageNumber(1)
}
return(
<Panel id={id}>
<PanelHeader>Events around you</PanelHeader>
<FixedLayout vertical="top">
<Search onChange={handleSearch} />
</FixedLayout>
{popout}
{
<List id="event-list">
{
events.length > 0
?
events.map((event, i) => <EventListItem key={event.id} id={event.id} title={event.title} />)
:
<InfoMessages type="no-events" />
}
</List>
}
</Panel>
)
}
export default AllEvents
useEvents() is a custom hook in EventServerHooks.js file. EventServerHooks is designed for incapsulating different ajax requests. (Like a helper file to make AllEvents.js cleaner) Here it is:
function useEvents(setEvents, setPopout) {
useEffect(() => {
axios.get("https://server.ru/events")
.then(
(response) => {
console.log(response)
console.log(new Date())
setEvents(response.data.data)
setPopout(null)
},
(error) => {
console.log('Error while getting events: ' + error)
}
)
}, [])
return null
}
function useSearchedEvents(setEvents, setPopout, searchQuery, pageNumber) {
useEffect(() => {
setPopout(<ScreenSpinner className="preloader" size="large" />)
let cancel
axios({
method: 'GET',
url: "https://server.ru/events",
params: {q: searchQuery, page: pageNumber},
cancelToken: new axios.CancelToken(c => cancel = c)
}).then(
(response) => {
setEvents(response.data)
setPopout(null)
},
(error) => {
console.log('Error while getting events: ' + error)
}
).catch(
e => {
if (axios.isCancel(e)) return
}
)
return () => cancel()
}, [searchQuery, pageNumber])
return null
}
export { useEvents, useSearchedEvents }
And here is the small component InfoMessages from the first code listing, which display message "No results" if events array is empty:
const InfoMessages = props => {
switch (props.type) {
case 'no-events':
{console.log(new Date())}
return <Div className="no-events">No results :(</Div>
default:
return ''
}
}
export default InfoMessages
So my problem is that events periodically loads and periodically don't after app opened. As you can see in the code I put console log in useEvents() and in InfoMessages so when it's displayed it looks like this:
logs if events are displayed, and the app itself
And if it's not displayed it looks like this: logs if events are not displayed, and the app itself
I must note that data from the server is loaded perfectly in both cases, so I have totally no idea why it behaves differently with the same code. What am I missing?
Do not pass a hook to a custom hook: custom hooks are supposed to be decoupled from a specific component and possibly reused. In addition, your custom hooks return always null and that's wrong. But your code is pretty easy to fix.
In your main component you can fetch data with a custom hook and also get the loading state like this, for example:
function Events () {
const [events, loadingEvents] = useEvents([])
return loadingEvents ? <EventsSpinner /> : <div>{events.map(e => <Event key={e.id} title={e.title} />}</div>
}
In your custom hook you should return the internal state. For example:
function useEvents(initialState) {
const [events, setEvents] = useState(initialState)
const [loading, setLoading] = useState(true)
useEffect(function() {
axios.get("https://server.ru/events")
.then(
(res) => {
setEvents(res.data)
setLoading(false)
}
)
}, [])
return [events, loading]
}
In this example, the custom hook returns an array because we need two values, but you could also return an object with two key/value pairs. Or a simple variable (for example only the events array, if you didn't want the loading state), then use it like this:
const events = useEvents([])
This is another example that you can use, creating a custom hook that performs the task of fetching the information
export const useFetch = (_url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(true);
useEffect(function() {
setLoading('procesando...');
setData(null);
setError(null);
const source = axios.CancelToken.source();
setTimeout( () => {
axios.get( _url,{cancelToken: source.token})
.then(
(res) => {
setLoading(false);
console.log(res.data);
//setData(res);
res.data && setData(res.data);
// res.content && setData(res.content);
})
.catch(err =>{
setLoading(false);
setError('si un error ocurre...');
})
},1000)
return ()=>{
source.cancel();
}
}, [_url])

Confused about useEffect

I'm building my first Custom React Hook and am confused about what I think is a simple aspect of the code:
export const useFetch = (url, options) => {
const [data, setData] = useState();
const [loading, setLoading] = useState(true);
const { app } = useContext(AppContext);
console.log('** Inside useFetch: options = ', options);
useEffect(() => {
console.log('**** Inside useEffect: options = ', options);
const fetchData = async function() {
try {
setLoading(true);
const response = await axios.get(url, options);
if (response.status === 200) {
setData(response.data);
}
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
fetchData();
}, []);
return { loading, data };
};
I pass to useFetch two parameters: A url and a headers object that contains an AWS Cognito authorization key that looks like this: Authorization: eyJraWQiOiJVNW... (shortened for brevity)
When I do this the options object on does exist near within useFetch but within the useEffect construct it is empty. YET the url string is correctly populated in BOTH cases.
This makes no sense to me. Might anyone have an idea why this is occurring?
Below an implementation of your code showing that it works as expected.
The async/await has been converted to a Promise but should have the same behavior.
"Inside use fetch" is outputed 3 times:
on mount (useEffect(()=>..., [])
after first state change (setLoading(true))
after second state change (setLoading(false))
and "Inside use effect" is outputed 1 time on mount (useEffect(()=>..., [])
Since it doesn't work for you this way it could mean that when the component mounts, options is not available yet.
You confirm it when saying that when you put options as a dependency, useEffect is called two times with the first fetch failing (most likely because of options missing).
I'm pretty sure you will find the problem with options in the parents of the component using your custom hook.
const axios = {
get: (url, options) => {
return new Promise(resolve => setTimeout(() => resolve({ status: 200, data: 'Hello World' }), 2000));
}
};
const AppContext = React.createContext({ app: null });
const useFetch = (url, options) => {
const [data, setData] = React.useState();
const [loading, setLoading] = React.useState(true);
const { app } = React.useContext(AppContext);
console.log('** Inside useFetch: options = ', JSON.stringify(options));
React.useEffect(() => {
console.log('**** Inside useEffect: options = ', JSON.stringify(options));
const fetchData = function () {
setLoading(true);
const response = axios.get(url, options)
.then(response => {
if (response.status === 200) {
setData(response.data);
}
setLoading(false);
})
.catch(error => {
setLoading(false);
throw error;
});
};
fetchData();
}, []);
return { loading, data };
};
const App = ({url, options}) => {
const { loading, data } = useFetch(url, options);
return (
<div
style={{
display: 'flex', background: 'red',
fontSize: '20px', fontWeight: 'bold',
justifyContent: 'center', alignItems: 'center',
width: 300, height: 60, margin: 5
}}
>
{loading ? 'Loading...' : data}
</div>
);
};
ReactDOM.render(
<App
url="https://www.dummy-url.com"
options={{ headers: { Authorization: 'eyJraWQiOiJVNW...' } }}
/>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root" />

Categories