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.
Related
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.
I have implemented the following code to fetch data and render a component if everything goes well along with checking loading, error states.
import { useEffect, useState } from "react";
function Posts() {
const [posts, setPosts] = useState([]);
const [loader, setLoader] = useState(false);
const [error, setError] = useState({ status: false, message: "" });
const fetchPosts = () => {
setLoader(true);
setTimeout(async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
);
// throw new Error("Some error occured");
const data = await response.json();
if (data.error) {
setError({ status: true, message: data.error });
} else {
setPosts(data);
}
setLoader(false);
} catch (error) {
console.log("error", error);
setError({ status: true, message: error.message });
setLoader(false);
}
}, 2000);
};
useEffect(() => {
fetchPosts();
}, []);
if (loader) return <h3>Loading...</h3>;
if (error.status) return <h3>Error: {error.message}</h3>;
return (
<div>
<h1>Posts</h1>
{posts.length === 0 && <h3>There are no posts</h3>}
{posts.length > 0 && (
<div>
{posts.map((post) => (
<Post post={post} key={post.id} />
))}
</div>
)}
</div>
);
}
export default Posts;
Is this the right way to handle loading, error and success states when fetching data? or is there a better and more elegant solution than repeating this for every component?
Instead of checking for data.error in the try block, you could check for response.ok; if it is true, call response.json(), otherwise throw an error.
Also move the setLoader call to the finally block to avoid the duplicate calls in try and catch blocks.
try {
const response = await fetch(...);
if (response.ok) {
let data = await response.json();
setPosts(data);
} else {
throw new Error(/* error message */);
}
} catch (error) {
console.log("error", error);
setError({ status: true, message: error.message });
} finally {
setLoader(false);
}
If you want to check for data.error property in a response, you can change the following if condition
if (response.ok) {
to
if (response.ok && !data.error) {
is there a better and more elegant solution than repeating this for
every component?
Make a custom hook to make the fetch request and use that in every component that needs to fetch data from the backend.
const useFetch = (apiUrl, initialValue) => {
const [data, setData] = useState(initialValue);
const [loader, setLoader] = useState(false);
const [error, setError] = useState({ status: false, message: "" });
useEffect(() => {
async function fetchData = (url) => {
setLoader(true);
try {
const response = await fetch(url);
if (response.ok) {
let responseData = await response.json();
setData(responseData);
} else {
throw new Error(/* error message */);
}
} catch (error) {
console.log("error", error);
setError({ status: true, message: error.message });
} finally {
setLoader(false);
}
}
fetchData(apiUrl);
}, [apiUrl]);
return [data, error, loader];
};
Your solution should be good enough to do, but, to me, I would prefer not to set timeout for getting data, and I will use .then and .catch for better readable and look cleaner to me
import { useEffect, useState } from "react";
function Posts() {
const [posts, setPosts] = useState([]);
const [loader, setLoader] = useState(false);
const [error, setError] = useState({ status: false, message: "" });
const fetchPosts = () => {
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(data => {
setPosts(data);
setLoader(false);
})
.catch(error =>{
console.log("error", error);
setError({ status: true, message: error.message });
setLoader(false);
});
};
useEffect(() => {
setLoader(true);
fetchPosts();
}, []);
if (loader) return <h3>Loading...</h3>;
if (error.status) return <h3>Error: {error.message}</h3>;
return (
<div>
<h1>Posts</h1>
{posts.length === 0 && <h3>There are no posts</h3>}
{posts.length > 0 && (
<div>
{posts.map((post) => (
<Post post={post} key={post.id} />
))}
</div>
)}
</div>
);
}
export default Posts;
I've created a custom hook to fetch data with events handlers, when I using it on click event the hook makes the request on the second click
useFetch.js
import { useState, useEffect } from 'react';
import { makeRequest } from '../utils';
const useFetch = (query = {}) => {
const [request, setRequest] = useState({ ...query });
const [data, setData] = useState({
response: null,
isError: null,
isLoading: request.isLoading,
});
const fetchData = async () => {
if (!request.url) {
return;
}
try {
const res = await makeRequest(
request.url,
request.method || 'get',
request.body || null,
);
setData({
response: res,
isLoading: false,
error: null,
});
} catch (error) {
setData({
response: null,
error,
isLoading: false,
});
}
};
const onEvent = (req) => {
if (req) {
setRequest({ ...req });
}
};
useEffect(() => fetchData(), [request]);
return { ...data, onEvent };
};
export default useFetch;
Component File
const { isLoading, isError, response, onEvent } = useFetch();
const ClickMe = () => {
onEvent({
url: 'v1/profile/login',
method: 'post',
body: {
username: 'eee#ddd.com',
password: '2342332',
},
});
console.log('response', response);
};
return (
<>
<button onClick={() => ClickMe()} type="button">
Click Me
</button>
)
the log inside the ClickMe function is null in the first click but in the second click it returns the value
Because fetchData is asynchronous function you cannot know when resposne will be set, that's why you cannot access it like normal sync code
in your app code you could observe response change to console it like
useEffect(() => { console.log(response) }, [ response ]);
At the time of console.log, the response is not fetched. Since when ever response changes, the component re-renders, you can try like below to see the updated values of isLoading and response.
return (
<>
{isLoading && <div> Loading... </div>}
{`Resonse is ${JSON.stringify(response)}`}
<button onClick={() => ClickMe()} type="button">
Click Me
</button>
</>
);
As the others said, it's an asynchronous operation. If you want to use the response as soon as you called onEvent, you can do something along these lines using a promise :
import { useState, useEffect } from 'react';
import { makeRequest } from '../utils';
const useFetch = (query = {}) => {
useEffect(() => {
if (query) {
fetchData(query)
}
}, []) // if query is provided, run query
const [data, setData] = useState({
response: null,
isError: null,
isLoading: true
});
const fetchData = async (query) => {
return new Promise((resolve, reject) => {
if (!query.url) {
reject('url needed')
}
makeRequest(query).then(res => {
setData({
response: res,
isLoading: false,
error: null
})
resolve(res)
).catch(error => {
setData({
response: null,
error,
isLoading: false
});
reject(error)
});
})
})
};
// provide fetchData directly for lazy calls
return { ...data, fetchData };
};
export default useFetch;
And then call it like so :
const { response, fetchData } = useFetch()
fetchData({
url: 'v1/profile/login',
method: 'post',
body: {
username: 'eee#ddd.com',
password: '2342332',
},
}).then(res => ...);
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])
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])