Can't handle react spinner loading using useState - reactjs

I use a function component, so i have to use UseState to handle component states.
I'm trying to show a spinner when loading data with axios :
import { Spinner } from 'react-bootstrap';
const MandatesPage = props => {
const [mandates, setMandates] = useState([]);
const [loading, setLoading] = useState(false); // to handle spinner hide show
useEffect(() => {
setLoading(true); // here loading is true
console.log(loading)
axios
.get(`${config.api}/mandates`)
.then(response => response.data["hydra:member"],setLoading(false)) // here loading is false
.then(data => setMandates(data))
.catch(error => console.log(error.response));
}, []);
...
if (loading) return
return (
<Spinner animation="border" variant="primary" />
);
}
return (
..... // return the other logic of my app
)
}
my problem is the spinner is not shown and i put console.log(loading) after setLoading(true) but i got false value.

Of course loading is still false, because the setting is async and will only be true on the next render.
For the next render, the loading spinner will be returned, since loading will be true than.
If the axios calls needs short than 16 - 32ms, which is the normal frame for each render in react, the loading spinner will not be shown, because loading will already be set back to false.

The problem is that you're trying a asynchronous operation in a synchronous way. You should be holding until your API response gets back, something more like this:
useEffect(() => {
async function fetchMyAPI() {
let url = 'http://something/';
let config = {};
const response = await myFetch(url);
console.log(response);
}
fetchMyAPI();
}, []);
Applying to your example:
useEffect(() => {
setLoading(true);
async function fetchOnAxios() {
const response = await axios.get(`${config.api}/mandates`)
// Down below inside this function
// you can set the loading based on the response
}
fetchOnAxios()
}, []);
I deeply recommend this article for further reading, it has examples and everything.

Related

React wait for fetch data as part of custom hook

In my React functional component, I have the following code;
const user = useFetch('api/userinfo', {});
Essentially, this is a custom hook call and internally it has a fetch call to the API and sets the data (below is relevant code inside usefetch);
const [data, setData] = useState(initialData);
//....fetch call
setData(json); // once data is fetched
In my main component, since my grid depends on this data, how do I make the code wait to proceed to the Grid jsx till data is fetched? I was planning to use async..await. But not sure if it is possible to do that here with custom hooks?
With below code, seems like the hooks is getting invoked multiple times for some reason;
export default function useFetch(initialUrl, initialData) {
const [url] = useState(initialUrl);
const [loadingData, setLoadingData] = useState(true);
const [data, setData] = useState(initialData);
useEffect(() => {
setLoadingData(true);
fetch(url)
.then(response => {
if (response.status === 200) {
response.json().then(json => {
setData(json);
setLoadingData(false);
});
})
}, [url]);
return [loadingData, data];
}
A couple options for you:
Use another state variable (ie some boolean) and use that to keep track of whether or not the data comes back from the API. Then conditionally render some 'loading' element
Check to see if the data exists and conditionally render based on its existence.
Here's how you can do it with your custom hook:
// defining useFetch hook
const useFetch = (url) => {
// state to keep track of loading
const [loadingData, setLoadingData] = useState(false);
// state for data itself
const [data, setData] = useState(null);
// effect to fetch data
useEffect(() => {
const fetchData = async () => {
try {
// set data to loading
setLoadingData(true);
// request to load data, you can use fetch API too
const { data } = await axios.get(url);
// set data in state and loading to false
setLoadingData(false);
setData(data);
} catch (error) {
console.log("error", error);
}
};
fetchData();
}, [url]);
// return the data and loading state from this hook
return [loadingData, data];
};
Now, you can use this hook in your component like:
const MyComponent = (props) => {
const [isDataLoading, data] = useFetch('/api/some-url');
// now check if data is loading, if loading then return a loader/spinner
if (isDataLoading || !data) return <p>Data is loading...</p>
// otherwise render your actual component
return (
<div>
<h1>This is my component with data</h1>
</div>
);
}

Infinate loop with fetching data in UseEffect

Having a weird issue where by i am fetching some data from my local api, and it is infinately calling it for some strange reason:
import React, { useState, useEffect } from 'react';
const Users = () => {
const [users, setUsers] = useState([])
const fetchUsers = async () => {
try {
await fetch('http://localhost:3001/users')
.then(response => response.json())
.then(data => setUsers(data));
}
catch(ex) {
console.error('ex:', ex);
}
}
useEffect(() => {
fetchUsers();
}, [users])
return <div>xxx</div>
}
export default Users;
If I console.log(data) instead of setUsers(data), then all seems to be fine and the console log only outputs 1 set of information.
I am unsure what I am doing wrong. Any ideas?
useEffect(() => {
fetchUsers();
}, [users])
should be:
useEffect(() => {
fetchUsers();
}, [])
The first will fetch users every time user changes. then it changes the users object with the results which causes the infinite loop.
The fix instead only calls it once on mount.
Your effect:
useEffect(() => {
fetchUsers();
}, [users]);
will be executed whenever someone changes users object. In this case, the first time you will call useEffect, and fetch data from API, when you receive data from the backend you will update users object and trigger an infinite loop.
You can solve problem with:
useEffect(() => { fetchUsers(); }, []);

React set 2 states simulatenously within useeffect

I have a component that I use to display a list of data entries like this (simplified):
// resource is the rest endpoint,
// items is the parents components
// state that holds the data entries and setItems is the corresponding setter
export default function LoadedList({resource,items, setItems,CustomFormat}){
const [loadingOrError,setLoadingOrError] =useState(false)
useEffect(()=>{
axios.get(baseURL+resource)
.then((e)=>{
setItems(e.data)
setLoadingOrError(false)
})
.catch((e)=>{
setItems([{text:"Error"}])
setLoadingOrError(true)
})
setItems([{text:"Loading...-"}])
setLoadingOrError(true)
},[])
return(
<div className="list">
{
items.map((item)=>
loadingOrError?
<DefaultFormat item={item} />
:
<CustomFormat item={item}/>
)
}
</div>
)
}
The basic idea is, that while the component is loading item or if it fails, the default format should be used to display the corresponding message.
Once the items have successfully loaded, the format from the parent should be used to format the entries.
The problem is, that I have found out that setItems and setLoading are not changed simulatneously. The way it appears to work is that it first setItems then rerenders all the entries and only then changes loadingOrError to true. So is there a way to set both of those simulatenously? Or just without rerendering everything inbetween?
Instead of trying to update both simultaneously, why don't you try keeping track of the loading and error state separately, and then do something like this:
// resource is the rest endpoint,
// items is the parents components
// state that holds the data entries and setItems is the corresponding setter
export default function LoadedList({resource, items, setItems, CustomFormat}){
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(()=>{
setLoading(true);
axios.get(baseURL+resource)
.then((e)=>
setItems(e.data)
)
.catch((e)=>
setError("Error")
)
.finally(() => setLoading(false));
},[])
if(loading) {
return "Loading ...";
}
if(error) {
return error;
}
return(
<div className="list">
{items.map((item, index) => <CustomFormat key={index} item={item}/>)}
</div>
)
}
That should display Loading... until all items are loaded.
If you insist on wanting to leave everything as it is, and just achieve what you originally asked about updating both at the same time, you would probably need to define a function that executes the API call one level up, together with the loading state, error state and data state handling, have all those state together under the same state hook, and then pass down the API function to be used in the child's useEffect.
const [dataState, setDataState] = useState({
data: null,
loading: false,
error: ""
});
...
setDataState({data: data, loading: false});
Besides this, I recommend two things:
You should check that the component is still mounted when the request finishes and right before setting the state. Otherwise you will get an error. That's very simple to achieve with a an additional variable to keep track of the mount state.
It might be beneficial to create a custom hook for handling requests, since that's probably something you will do a lot, and it will look very similar in every case. I find the step-by-step guide in this post very clear.
Taken from that post:
useFetch custom hook
import { useState, useEffect } from 'react';
const useFetch = (url = '', options = null) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
let isMounted = true;
setLoading(true);
fetch(url, options)
.then(res => res.json())
.then(data => {
if (isMounted) {
setData(data);
setError(null);
}
})
.catch(error => {
if (isMounted) {
setError(error);
setData(null);
}
})
.finally(() => isMounted && setLoading(false));
return () => (isMounted = false);
}, [url, options]);
return { loading, error, data };
};
export default useFetch;

React Hook useEffect : fetch data using axios with async await .api calling continuous the same api

While I fetch data from API and set the response to a array using useEffect
it call the API repeat continuous.
let [product, setproduct] = useState([]);
async function fetchData() {
let response = await axios(
`api`
);
let user = await response.data;
setproduct(user);
console.log(product);
}
useEffect(() => {
fetchData();
});
From the docs,
Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
You can provide the empty dependency array / [] as second argument to useEffect, it is same as componentDidMount which will executes only once in component life cycle.
useEffect(() => {
fetchData();
}, []); //This will run only once
Pass empty [] as an second argument to useEffect method. It will be called on initial render, like below.
useEffect(() => {
fetchData();
}, []);
i think the blow example will help you through fetch API .
import React , {useEffect} from "react";
import axios from "axios";
const Courses = ()=>{
useEffect(()=>{
getProducts()
})
const getProducts = async() => {
await axios.get('api/get_all_products')
.then(({data}) =>{
console.log("this is data from api ");
console.log('data' , data);
} )
console.log("data ehre ");
}
return(
<div>
<h2>Products data here</h2>
</div>
)
};
export default Courses;
let [product, setProduct] = useState([]);
// This function will be only called once
async function fetchData() {
let response = await axios("api");
let user = response.data;// Don't need await here
setProduct(user);
console.log(product);
}
useEffect(() => {
fetchData();
}, []);

How to send request on click React Hooks way?

How to send http request on button click with react hooks? Or, for that matter, how to do any side effect on button click?
What i see so far is to have something "indirect" like:
export default = () => {
const [sendRequest, setSendRequest] = useState(false);
useEffect(() => {
if(sendRequest){
//send the request
setSendRequest(false);
}
},
[sendRequest]);
return (
<input type="button" disabled={sendRequest} onClick={() => setSendRequest(true)}
);
}
Is that the proper way or is there some other pattern?
export default () => {
const [isSending, setIsSending] = useState(false)
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
this is what it would boil down to when you want to send a request on click and disabling the button while it is sending
update:
#tkd_aj pointed out that this might give a 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."
Effectively, what happens is that the request is still processing, while in the meantime your component unmounts. It then tries to setIsSending (a setState) on an unmounted component.
export default () => {
const [isSending, setIsSending] = useState(false)
const isMounted = useRef(true)
// set isMounted to false when we unmount the component
useEffect(() => {
return () => {
isMounted.current = false
}
}, [])
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
if (isMounted.current) // only update if we are still mounted
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
You don't need an effect to send a request on button click, instead what you need is just a handler method which you can optimise using useCallback method
const App = (props) => {
//define you app state here
const fetchRequest = useCallback(() => {
// Api request here
}, [add dependent variables here]);
return (
<input type="button" disabled={sendRequest} onClick={fetchRequest}
);
}
Tracking request using variable with useEffect is not a correct pattern because you may set state to call api using useEffect, but an additional render due to some other change will cause the request to go in a loop
In functional programming, any async function should be considered as a side effect.
When dealing with side effects you need to separate the logic of starting the side effect and the logic of the result of that side effect (similar to redux saga).
Basically, the button responsibility is only triggering the side effect, and the side effect responsibility is to update the dom.
Also since react is dealing with components you need to make sure your component still mounted before any setState or after every await this depends on your own preferences.
to solve this issue we can create a custom hook useIsMounted this hook will make it easy for us to check if the component is still mounted
/**
* check if the component still mounted
*/
export const useIsMounted = () => {
const mountedRef = useRef(false);
const isMounted = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});
return isMounted;
};
Then your code should look like this
export const MyComponent = ()=> {
const isMounted = useIsMounted();
const [isDoMyAsyncThing, setIsDoMyAsyncThing] = useState(false);
// do my async thing
const doMyAsyncThing = useCallback(async () => {
// do my stuff
},[])
/**
* do my async thing effect
*/
useEffect(() => {
if (isDoMyAsyncThing) {
const effect = async () => {
await doMyAsyncThing();
if (!isMounted()) return;
setIsDoMyAsyncThing(false);
};
effect();
}
}, [isDoMyAsyncThing, isMounted, doMyAsyncThing]);
return (
<div>
<button disabled={isDoMyAsyncThing} onClick={()=> setIsDoMyAsyncThing(true)}>
Do My Thing {isDoMyAsyncThing && "Loading..."}
</button>;
</div>
)
}
Note: It's always better to separate the logic of your side effect from the logic that triggers the effect (the useEffect)
UPDATE:
Instead of all the above complexity just use useAsync and useAsyncFn from the react-use library, It's much cleaner and straightforward.
Example:
import {useAsyncFn} from 'react-use';
const Demo = ({url}) => {
const [state, doFetch] = useAsyncFn(async () => {
const response = await fetch(url);
const result = await response.text();
return result
}, [url]);
return (
<div>
{state.loading
? <div>Loading...</div>
: state.error
? <div>Error: {state.error.message}</div>
: <div>Value: {state.value}</div>
}
<button onClick={() => doFetch()}>Start loading</button>
</div>
);
};
You can fetch data as an effect of some state changing like you have done in your question, but you can also get the data directly in the click handler like you are used to in a class component.
Example
const { useState } = React;
function getData() {
return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}
function App() {
const [data, setData] = useState(0)
function onClick() {
getData().then(setData)
}
return (
<div>
<button onClick={onClick}>Get data</button>
<div>{data}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
You can define the boolean in the state as you did and once you trigger the request set it to true and when you receive the response set it back to false:
const [requestSent, setRequestSent] = useState(false);
const sendRequest = () => {
setRequestSent(true);
fetch().then(() => setRequestSent(false));
};
Working example
You can create a custom hook useApi and return a function execute which when called will invoke the api (typically through some onClick).
useApi hook:
export type ApiMethod = "GET" | "POST";
export type ApiState = "idle" | "loading" | "done";
const fetcher = async (
url: string,
method: ApiMethod,
payload?: string
): Promise<any> => {
const requestHeaders = new Headers();
requestHeaders.set("Content-Type", "application/json");
console.log("fetching data...");
const res = await fetch(url, {
body: payload ? JSON.stringify(payload) : undefined,
headers: requestHeaders,
method,
});
const resobj = await res.json();
return resobj;
};
export function useApi(
url: string,
method: ApiMethod,
payload?: any
): {
apiState: ApiState;
data: unknown;
execute: () => void;
} {
const [apiState, setApiState] = useState<ApiState>("idle");
const [data, setData] = useState<unknown>(null);
const [toCallApi, setApiExecution] = useState(false);
const execute = () => {
console.log("executing now");
setApiExecution(true);
};
const fetchApi = useCallback(() => {
console.log("fetchApi called");
fetcher(url, method, payload)
.then((res) => {
const data = res.data;
setData({ ...data });
return;
})
.catch((e: Error) => {
setData(null);
console.log(e.message);
})
.finally(() => {
setApiState("done");
});
}, [method, payload, url]);
// call api
useEffect(() => {
if (toCallApi && apiState === "idle") {
console.log("calling api");
setApiState("loading");
fetchApi();
}
}, [apiState, fetchApi, toCallApi]);
return {
apiState,
data,
execute,
};
}
using useApi in some component:
const SomeComponent = () =>{
const { apiState, data, execute } = useApi(
"api/url",
"POST",
{
foo: "bar",
}
);
}
if (apiState == "done") {
console.log("execution complete",data);
}
return (
<button
onClick={() => {
execute();
}}>
Click me
</button>
);
For this you can use callback hook in ReactJS and it is the best option for this purpose as useEffect is not a correct pattern because may be you set state to make an api call using useEffect, but an additional render due to some other change will cause the request to go in a loop.
<const Component= (props) => {
//define you app state here
const getRequest = useCallback(() => {
// Api request here
}, [dependency]);
return (
<input type="button" disabled={sendRequest} onClick={getRequest}
);
}
My answer is simple, while using the useState hook the javascript doesn't enable you to pass the value if you set the state as false. It accepts the value when it is set to true. So you have to define a function with if condition if you use false in the usestate

Resources