Serialisation of react hooks for fetching data - reactjs

I've just stated experimenting with react hooks and I haven't been able to find a way to answer the following. Assume I have two custom hooks for fetching data. For simplicity, assume the first one takes no argument and the second takes one argument as follows:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// fetching takes 3 secs
function useFetch1() {
const [text, setText] = useState("")
useEffect(() => {
const test = async () => {
await sleep(3000)
setText("text1")
}
test()
}, [])
return text
}
// fetching takes 1 sec
function useFetch2(input) {
const [text, setText] = useState("")
useEffect(() => {
const test = async () => {
await sleep(1000)
setText(input + "text2")
}
test()
}, [input])
return text
}
This allows me to use each hook independently of one another. So far, so good. What happens though when the argument to the second fetch depends on the output of the first fetch. In particular, assume that my App component is like this:
function App() {
const text1 = useFetch1()
const text2 = useFetch2(text1) // input is dependent on first fetch
// renders text2 and then text1text2
return (
<div>
{text2}
</div>
)
}
In this case, the renderer will first display "text2" and then (2 seconds later), it will display "text1text2" (which is when the first fetch finished). Is there a way to use these hooks in a way that ensures that useFetch2 will only be called after useFetch1 is finished?

You can achieve the behavior you're looking for, however:
Hook functions, by design, have to be called on every render - You are not allowed to conditionally call hooks, React can't handle that.
Instead of conditionally calling hooks, you can implement the logic within hooks, for example, useFetch1 can return the state of the request, which can be used in another hook to conditionally do something based on it.
Example:
function useFetch1() {
const [text, setText] = useState("")
// a bool to represent whether the fetch completed
const isFinished = useState(false)
useEffect(() => {
const test = async () => {
await sleep(3000)
setText("text1")
}
test()
}, [])
return {text, isFinished} // Return the isFinished boolean
}
function useFetch2(input, isFetch1Finished) {
const [text, setText] = useState("")
useEffect(() => {
if (!isFetch1Finished) {
return; // fetch1 request not finished, don't fetch
}
const test = async () => {
await sleep(1000)
setText(input + "text2")
}
test()
}, [input])
return text
}
function App() {
const {text: text1, isFinished} = useFetch1()
const text2 = useFetch2(text1, isFinished) // input is dependent on first fetch, but is aware whether the first fetch is finished or not.
return (
<div>
{text2}
</div>
)
}
Now you can use the isFinished boolean flag from the useFetch1 in your second fetch, to determine whether to do something or not.
A better approach
It all depends on your use-case, but you might not want to couple the logic of 2 hooks together. I suggest it would be better to have useFetch2 in a separate component, which is conditionally rendered based on the status useFetch1.
return (
{isFetch1Finished && <SecondComponent text={text1}>}
)
Now, you simplified your hooks logic by conditionally rendering the second component once the first request is finished. This is a better design compared to the first approach.
Do you even need 2 hooks?
Ask yourself if you even need 2 hooks to begin with, it would be trivial to implement the synchronous behavior in a single hook with the use of promises.
Example:
useFetch(() => {
const [state, setState] = useState("");
useEffect(() => {
fetch('http://..').then(res1 => {
fetch('http://..').then(res2 => {
// In this scope you have both `res1` and `res2`.
setState(res1 + res2);
})
})
}, [])
return state;
})

Related

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.

Debounce or throttle with react hook

I need to make an api request when a search query param from an input fields changes, but only if the field is not empty.
I am testing with several answers found on this site, but can't get them working
Firstly this one with a custom hook
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
now in my component I do this
const debouncedTest = useDebounce(() => {console.log("something")}, 1000);
but this seems to gets called every rerender regardless of any parameter, and I need to be able to call it inside a useEffect, like this
useEffect(() => {
if (query) {
useDebounce(() => {console.log("something")}, 1000);
} else {
//...
}
}, [query]);
which of course does not work
Another approach using lodash
const throttledTest = useRef(throttle(() => {
console.log("test");
}, 1000, {leading: false}))
But how would i trigger this from the useEffect above? I don't understand how to make this work
Thank you
Your hook's signature is not the same as when you call it.
Perhaps you should do something along these lines:
const [state, setState] = useState(''); // name it whatever makes sense
const debouncedState = useDebounce(state, 1000);
useEffect(() => {
if (debouncedState) functionCall(debouncedState);
}, [debouncedState])
I can quickly point out a thing or two here.
useEffect(() => {
if (query) {
useDebounce(() => {console.log("something")}, 1000);
} else {
//...
}
}, [query]);
technically you can't do the above, useEffect can't be nested.
Normally debounce isn't having anything to do with a hook. Because it's a plain function. So you should first look for a solid debounce, create one or use lodash.debounce. And then structure your code to call debounce(fn). Fn is the original function that you want to defer with.
Also debounce is going to work with cases that changes often, that's why you want to apply debounce to reduce the frequency. Therefore it'll be relatively uncommon to see it inside a useEffect.
const debounced = debounce(fn, ...)
const App = () => {
const onClick = () => { debounced() }
return <button onClick={onClick} />
}
There's another common problem, people might take debounce function inside App. That's not correct either, since the App is triggered every time it renders.
I can provide a relatively more detailed solution later. It'll help if you can explain what you'd like to do as well.

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 };
};

Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks (dynamic import)

const usePage = ({ page }) => {
const prevPage = usePrevious(page)
const [p, setPage] = useState()
const loadData = async param => {
const data = await import(`${param}`)
setPage(data.default)
}
useEffect(() => {
if (prevPage === page) return
loadData(page)
}, [page, prevPage])
return {
p
}
}
const PageRoute = memo(({page}) => {
const { p } = usePage({ page })
const Page = p
return (
<Page/>
)
}
)
I don't really understand how do this issue related to my code . I don't call any hook inside useEffect. How can I fix it ? I want to call dynamic import in use effect in case when page parameter are not equal previous one.
The problem is that since you're probably exporting a React component as default from these dynamically imported modules, data.default is a function which gets passed to the setter returned by useState.
However, since the useState setter can also take in a function which does the state update, what's happening is that the setter calls the passed function which is a React component (data.default) which fires the hooks used in that component. So your call is actually equivalent to setPage(prev => data.default(prev)).
This can be fixed by explicitly passing your own state updater which just returns the data.default function:
const loadData = async param => {
const data = await import(`${param}`)
setPage(() => data.default) // <-------
}

How can I re-fetch an API using react hooks

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;
};

Resources