How can I re-fetch an API using react hooks - reactjs

devs,
I have decided to finally learn react hooks with what I thought would be a simple project. I can't quite figure out how I re-fetch an API using react hooks. Here is the code I have so far.
import React, { useState, useEffect } from "react"
import useFetch from "./utils/getKanya"
const kanye = "https://api.kanye.rest"
const Index = () => {
let [kanyaQuote, setKanyeQuote] = useState(null)
let data = useFetch(kanye)
const getMore = () => {
setKanyeQuote(useFetch(kanye))
}
return (
<>
<h1>Welcome to Next.js!</h1>
<p>Here is a random Kanye West quote:</p>
{!data ? <div>Loading...</div> : <p>{!kanyaQuote ? data : kanyaQuote}</p>}
<button onClick={getMore}>Get new quote</button>
</>
)
}
export default Index
I get the kanyeQuote state value to null
I fetch the initial data
I either show "Loading..." or the initial quote
I am trying to set up a button to re-fetch the API and store the data in kanyeQuote via getKanyeQuote (setState)
This is the error I get Error: Invalid hook call...
I would greatly appreciate any guidance you can provide on this.

The issue here is, that you can only use hooks directly inside the root of your component.
It's the number 1 'rule of hooks'. You can read more about that here
const getMore = () => {
setKanyeQuote(useFetch(kanye) /* This cannot work! */)
}
There are a few ways you could work around that. Without knowing the internal logic in your useFetch-hook I can only assume you are able to change it.
Change hook to handle its state internally
One way to work around that would be to change the logic of your custom useFetch hook to provide some form of function that fetches the data and updates the state internally. It could then look something like this:
const { data, doFetch } = useFetch(kanye);
useEffect(() => {
doFetch(); // initialFetch
}, []);
const getMore = () => {
doFetch();
};
// ...
You would then need to change the internal logic of your useFetch-hook to use useState internally and expose the getter of it. It would look something like this:
export const useFetch = (url) => {
const [data, setData] = useState(null);
const doFetch = () => {
// Do your fetch-Logic
setData(result);
};
return { data, doFetch };
};
Change hook not to handle any state at all.
If you only want to manage the state of the loaded data in the parent component, you could just provide the wrapped fetch function through the hook; Something like that:
const doFetch = useFetch(kanye);
const [data, setData] = useState(null);
useEffect(() => {
setData(doFetch()); // initialFetch
}, []);
const getMore = () => {
setData(doFetch())
};
// ...
You would then need to change the internal logic of your useFetch-hook to not have any internal state and just expose the wrapped fetch. It would look something like this:
export const useFetch = (url) => {
const doFetch = () => {
// Do your fetch-Logic
return result;
};
return doFetch;
};

Related

React's new use() hook falls into infinite loop

I'm trying to load data for a client component. I can't use await/async as specified by React's RFC about use() hook. Thus I need to use use() hook.
However, it goes into infinite loop.
Here's my code:
import { use } from 'react'
const Component = () => {
const response = use(fetch('some_url'))
const data = use(response.json())
return <div>{data}</div>
}
Based on my intuition, I tried to use a callback inside the use() hook, as we do for useEffect():
const response = use(() => fetch('use_url'), [])
But it complained that an invalid parameter is passed into the use() hook.
I can't find anyting online. What should I do? What have I done wrong?
You need to "stabilize" the return of fetch. You could memoize it
const Component = () => {
const fetchPromise = React.useMemo(() => fetch('some_url').then(r => r.json()), [])
const data = use(fetchPromise);
return <div>{data}</div>;
}
I guess you just wanna memorized use result.
const response = useMemo(() => use(use(fetch('some_url')).json()), ['some_deps'])

How can I make separate components to loading in order?

For example, I write this in the sandbox for demonstration only, I don't quite remember the structure of my old project, but I remember that I tried something like this, this is written in 1 component, but I think it's not very good practice, because later on, I remember that I split my components into many more for easier management, but at that point, I can't make it load in order, so whenever the Home components are called, it just fetches everything instead of in order, which makes my page load very long due to it try to fetch everything from every component that exists in the Home component.
import {React, useState, useEffect} from "react"
import axios from "axios"
function Home() {
const [loadingSlider, setLoadingSlider] = useState(true)
const [loadingCategory, setLoadingCategory] = useState(true)
const [loadingStuff, setLoadingStuff] = useState(true)
const [sliderData, setSliderData] = useState()
const [categoryData, setCategoryData] = useState()
const [stuffData, setStuffData] = useState()
useEffect(() => {
const fetchSlider = async () => {
const response = await axios.get("sliderUrl")
setSliderData(response.data)
setLoadingSlider(false)
}
const fetchCategory = async () => {
const response = await axios.get("categoryUrl")
setCategoryData(response.data)
setLoadingCategory(false)
}
const fetchStuff = async () => {
const response = await axios.get("stuffUrl")
setStuffData(response.data)
setLoadingStuff(false)
}
fetchSlider()
fetchCategory()
fetchStuff()
} , [])
return (
<>
{
loadingSlider ? "Loading slider" : {sliderData}
}
{
loadingCategory ? "Loading category" : {categoryData}
}
{
loadingStuff ? "Loading stuff" : {stuffData}
}
</>
)
}
So with this practice, if I split 3 sliderData, categoryData, stuffData into 3 different components, and I just want to call it in Home component for easier management, how can I let it load in order?
Each component will fetch inside of it separately.
For example:
function Home() {
return (
<>
<SliderComponent/>
<CategoryComponent/>
<StuffComponent/>
{/* <More components might be added in the future/> */}
</>
)
}
What do I need to do so it can load in order, and not load all of the components at the same time? I want it done fetching the slider first, then it'll proceed to fetch the category, and then so on.
You could do like this:
useEffect(() => {
fetchSlider();
} , [])
const fetchSlider = async () => {
const response = await axios.get("sliderUrl")
if(response.status === 200){
// Call another method you want to load
setSliderData(response.data)
setLoadingSlider(false)
fetchCategory(); // Once Slide data is set call category method
}
}
const fetchCategory = async () => {
const response = await axios.get("categoryUrl")
if(response.status === 200){
// Call another method you want to load
setCategoryData(response.data)
setLoadingCategory(false)
fetchStuff(); // Once Category data is set call stuff method
}
}
const fetchStuff = async () => {
const response = await axios.get("stuffUrl")
setStuffData(response.data)
setLoadingStuff(false)
}
In Short Once you fetch data from method with status 200 then only call another method. And Its your option also even if one method might failed whether you cant to call next method or you want to show error. Hope It might help with your problem.

Avoid blocking UI rendering while api fetching data, React

I am working on React app which fetches large data (Thousands of records) and then render it. Until api is fetching data, UI keep blocked and does not display anything. I am creating chunks in size of 10 for fetching api using Promise.allSettled and combining them all.
useEffect(() => {
fetchBatchedData()
},[])
fetchBatchedData is an async function and sets data in redux store only, I don`t need that data in UI at loading time. Until I get all the data, UI display nothing. How can I fetch data in background without blocking component rendering?
You could use a useState like this:
const [data, setData] = useState();
useEffect(() => {
const loadData = () => {
const tempData = fetchBatchedData();
setData(tempData)
}
},[])
if(!data){
return(
<h1>Loading...</h1>
)
} else {
return(
<h1>Got the data!</h1>
)
}
Maybe we can defer rendering the data? There is something called requestIdleCallback here.
Something like this maybe?
import { useState, useEffect } from 'react';
function RenderDeferred({ children, timeout }) {
const [render, setRender] = useState(false);
useEffect(() => {
if (render) setRender(false);
const id = requestIdleCallback(() => setRender(true), { timeout: idleTimeout });
return () => cancelIdleCallback(id);
}, [idleTimeout]);
if (!render) return null;
return children;
}
And then use it like this:
<RenderDeferred timeout={3000}>
<YourComponent />
</RenderDeferred>

Best practice for marking hooks as not to be reused in multiple places

It seems a lot of my custom React Hooks don't work well, or seem to cause a big performance overhead if they are reused in multiple places. For example:
A hook that is only called in the context provider and sets up some context state/setters for the rest of the app to use
A hook that should only be called in a root component of a Route to setup some default state for the page
A hook that checks if a resource is cached and if not, retrieves it from the backend
Is there any way to ensure that a hook is only referenced once in a stack? Eg. I would like to trigger a warning or error when I call this hook in multiple components in the same cycle.
Alternatively, is there a pattern that I should use that simply prevents it being a problem to reuse such hooks?
Example of hook that should not be reused (third example). If I would use this hook in multiple places, I would most likely end up making unnecessary API calls.
export function useFetchIfNotCached({id}) {
const {apiResources} = useContext(AppContext);
useEffect(() => {
if (!apiResources[id]) {
fetchApiResource(id); // sets result into apiResources
}
}, [apiResources]);
return apiResources[id];
}
Example of what I want to prevent (please don't point out that this is a contrived example, I know, it's just to illustrate the problem):
export function Parent({id}) {
const resource = useFetchIfNotCached({id});
return <Child id={id}>{resource.Name}</Child>
}
export function Child({id}) {
const resource = useFetchIfNotCached({id}); // <--- should not be allowed
return <div>Child: {resource.Name}</div>
}
You need to transform your custom hooks into singleton stores, and subscribe to them directly from any component.
See reusable library implementation.
const Comp1 = () => {
const something = useCounter(); // is a singleton
}
const Comp2 = () => {
const something = useCounter(); // same something, no reset
}
To ensure that a hook called only once, you only need to add a state for it.
const useCustomHook = () => {
const [isCalled, setIsCalled] = useState(false);
// Your hook logic
const [state, setState] = useState(null);
const onSetState = (value) => {
setIsCalled(true);
setState(value);
};
return { state, setState: onSetState, isCalled };
};
Edit:
If you introduce a global variable in your custom hook you will get the expected result. Thats because global variables are not tied to component's lifecycle
let isCalledOnce = false;
const useCustomHook = () => {
// Your hook logic
const [state, setState] = useState(null);
const onSetState = (value) => {
if (!isCalledOnce) {
isCalledOnce = true;
setState(false);
}
};
return { state, setState: onSetState, isCalled };
};

How to wait for multiple state updates in multiple hooks?

Example
In my scenario I have a sidebar with filters.. each filter is created by a hook:
const filters = {
customerNoFilter: useFilterForMultiCreatable(),
dateOfOrderFilter: useFilterForDate(),
requestedDevliveryDateFilter: useFilterForDate(),
deliveryCountryFilter: useFilterForCodeStable()
//.... these custom hooks are reused for like 10 more filters
}
Among other things the custom hooks return currently selected values, a reset() and handlers like onChange, onRemove. (So it's not just a simple useState hidden behind the custom hooks, just keep that in mind)
Basically the reset() functions looks like this:
I also implemented a function to clear all filters which is calling the reset() function for each filter:
const clearFilters = () => {
const filterValues = Object.values(filters);
for (const filter of filterValues) {
filter.reset();
}
};
The reset() function is triggering a state update (which is of course async) in each filter to reset all the selected filters.
// setSelected is the setter comming from the return value of a useState statement
const reset = () => setSelected(initialSelected);
Right after the resetting I want to do stuff with the reseted/updated values and NOT with the values before the state update, e.g. calling API with reseted filters:
clearFilters();
callAPI();
In this case the API is called with the old values (before the update in the reset())
So how can i wait for all filters to finish there state updated? Is my code just badly structured? Am i overseeing something?
For single state updates I could simply use useEffect but this would be really cumbersome when waiting for multiple state updates..
Please don't take the example to serious as I face this issue quite often in quite different scenarios..
So I came up with a solution by implementing a custom hook named useStateWithPromise:
import { SetStateAction, useEffect, useRef, useState } from "react";
export const useStateWithPromise = <T>(initialState: T):
[T, (stateAction: SetStateAction<T>) => Promise<T>] => {
const [state, setState] = useState(initialState);
const readyPromiseResolverRef = useRef<((currentState: T) => void) | null>(
null
);
useEffect(() => {
if (readyPromiseResolverRef.current) {
readyPromiseResolverRef.current(state);
readyPromiseResolverRef.current = null;
}
/**
* The ref dependency here is mandatory! Why?
* Because the useEffect would never be called if the new state value
* would be the same as the current one, thus the promise would never be resolved
*/
}, [readyPromiseResolverRef.current, state]);
const handleSetState = (stateAction: SetStateAction<T>) => {
setState(stateAction);
return new Promise(resolve => {
readyPromiseResolverRef.current = resolve;
}) as Promise<T>;
};
return [state, handleSetState];
};
This hook will allow to await state updates:
const [selected, setSelected] = useStateWithPromise<MyFilterType>();
// setSelected will now return a promise
const reset = () => setSelected(undefined);
const clearFilters = () => {
const promises = Object.values(filters).map(
filter => filter.reset()
);
return Promise.all(promises);
};
await clearFilters();
callAPI();
Yey, I can wait on state updates! Unfortunatly that's not all if callAPI() is relying on updated state values ..
const [filtersToApply, setFiltersToApply] = useState(/* ... */);
//...
const callAPI = () => {
// filtersToApply will still contain old state here, although clearFilters() was "awaited"
endpoint.getItems(filtersToApply);
}
This happens because the executed callAPI function after await clearFilters(); is is not rerendered thus it points to old state. But there is a trick which requires an additional useRef to force rerender after filters were cleared:
useEffect(() => {
if (filtersCleared) {
callAPI();
setFiltersCleared(false);
}
// eslint-disable-next-line
}, [filtersCleared]);
//...
const handleClearFiltersClick = async () => {
await orderFiltersContext.clearFilters();
setFiltersCleared(true);
};
This will ensure that callAPI was rerendered before it is executed.
That's it! IMHO a bit messy but it works.
If you want to read a bit more about this topic, feel free to checkout my blog post.

Resources