await useFetch and then passing data to another react hook - reactjs

I have a simple useFetch() react hook that makes an api call to fetch data, I then want to pass that data to a custom usePagination() hook that processes the data and returns some of it to display.
something like this
const [data] = useFetch(`http://localhost:whatever`);
const {postDisplay} = usePagination(data);
The problem is that data is undefined until it finishes fetching, and usePagination crashes.
and being that it is a hook you can't call conditionally.
(I guess I can make useFetch async and await it, but it doesn't feel like that's the solution here)
Is there any easy ways around this?

You can handle the condition inside the usePagination hook itself. If it receives undefined as the argument, it should still call all the other hooks it could have (like useMemo or useState), but bail out of all of them. Eg.:
function usePagination(data) {
const postDisplay = useMemo(() => {
if (!data) return null
// business as usual here
}, [data])
return {postDisplay}
}
If your hook doesn't call any other hooks, you can just return early:
function usePagination(data) {
if (!data) return {postDisplay: null}
// business as usual here
return {postDisplay}
}
And inside your component, you handle the case when postDisplay is null as well.

Related

RTK-query: how to have the fixedCacheKey approach also for useLazyQuery?

reading the documentation I saw that mutations can share their result using the fixedCacheKey option:
RTK Query provides an option to share results across mutation hook instances using the fixedCacheKey option. Any useMutation hooks with the same fixedCacheKey string will share results between each other when any of the trigger functions are called. This should be a unique string shared between each mutation hook instance you wish to share results.
I have a GET api call that I need to invoke using the trigger method of the useLazyQuery hook, but I need to exploit its booleans (isSuccess, isError, etc...) in many places. Is it possible to have the same behaviour for the useLazyQuery hooks ?
In RTK-query, queries instances follow this logic in cache (from documentation):
When you perform a query, RTK Query automatically serializes the request parameters and creates an internal queryCacheKey for the request. Any future request that produces the same queryCacheKey will be de-duped against the original, and will share updates if a refetch is trigged on the query from any subscribed component.
Given that the only way, in different components, to get the same cache entry is to pass the same query parameters. If you have many useLazy in many components, those hooks don't point any cache entry until you fire them (with some parameters).
This is what I can understand from the official docs
I ended up implementing a custom hook to handle this scenario
// This is the custom hook that wrap the useLazy logic
const useCustomHook = () => {
const [trigger] = useLazyGetItemQuery();
const result = apiSlice.endpoints.getItem.useQueryState(/* argument if any */);
const handleOnClick = async () => {
// The second argument for the trigger is the preferCacheValue boolean
const { data, error } = await trigger(/* argument if any */, true);
// Here where I will implement the success/error logic ...
}
return { onClick: handleOnClick, result}
}
Setting the preferCacheValue at true forced the RTQK to return the cached value if present for the lazy I'm using. That allow me both to avoid triggering many API calls.
Once implemented the hook above I can do the following:
// Component that will make the API call
function Header() {
const { onClick } = useCustomHook();
return (<Button onClick={onClick}>Click me</Button>);
}
// Component that will use the booleans
function Body() {
const { result: { isFetching } } = useCustomHook()
return (<Spin spinning={isFetching}>
<MyJSX />
</Spin>);
}
With this approach I was able both to centralize the trigger logic (e.g. which parameters I have to pass to my trigger function) and exploiting the booleans of that RTKQ hooks in many components.

Use React props as a function argument

I want to check if this is good practice, and if there's a better approach to it. I have a function that is called when the component loads and when clicking on a couple of buttons, so I need to use it inside useEffect, and pass it to other components. I could use useCallback, but I don't see how this approach is not enough, since the components that can call getWeatherHere also need isCelsius, thus being able to use it as argument (the argument and the state are the same).
export default function weatherInfo() {
const [weatherInfo, setWeatherInfo]: [IWeather, React.Dispatch<IWeather>] = useState();
const [isCelsius, setIsCelsius] = useState(true);
function getWeatherHere(isCelsius: boolean) {
navigator.geolocation.getCurrentPosition(async ({coords: {latitude, longitude}}) => {
const data = await Weather.getWeather(latitude, longitude, !isCelsius ? 'imperial' : undefined);
setWeatherInfo(data);
});
}
useEffect(() => {
getWeatherHere(isCelsius);
}, [isCelsius]);
return (
<OtherComponent isCelsius={isCelsius} getWeatherHere={getWeatherHere}/>
);
}
This is how you do it, but there are tools available to make this easier.
Consider using a construction like useAsync from react-use. It allows you to express a Promise as a state. This avoids all sort of pitfalls and issues of using Promises in functional Components.
So you get something like:
const state = useAsync(async () => {
const { coords: { latitude, longitude } } = await navigator.geolocation.getCurrentPosition();
return {
isCelsius: isCelsius,
data: await Weather.getWeather(latitude, longitude, !isCelsius ? 'imperial' : undefined),
};
}, [ isCelsius ]);
return <>
{state.loading === false && <OtherComponent
isCelsius={state.value.isCelsius}
weatherHere={state.value.data}
/>
</>;
Some remarks:
I added isCelsius to the value of the async state. If you don't do that, and pass isCelsius directly, you're going to have a desynch between the value of isCelsius and the weather data. I would actually expect that the temperature unit is part of the result of the getWeather request, but if it's not, this works.
There is one problem when using promises with setState in React, and that has to do with cleanups. If your Component is unmounted before the promise is completed, it doesn't magically cancel the code. It will finish the request and call setWeatherInfo. That will trigger a re-render on an unmounted component, which React will give you a warning about. It's not a huge problem in this case, but it can become a problem in more complex situations. useAsync takes care of this by checking if the component is still mounted at the end of the fetcher function. You can also do that yourself by using usePromise, but I would try to avoid using Promises in this way all together.
Your code can suffer from race conditions. If you change isCelsius quickly a couple of times, it's going to be a coinflip which result is going to end up being used. useAsync also takes care of this.
If, instead of passing the weather, you want to pass a function that fetches the weather, use useAsyncFn instead. The state is the same, but it also returns a function that allows you to call the fetcher function. This is in addition to the value of isCelsius changing.
As your post is about best practices, I'll let you my 2 cents.
Some things that I would change using only pure react to refactor it.
export default function weatherInfo() {
// You don't need to type both returns, only the hook.
const [weatherInfo, setWeatherInfo] = useState<IWeather | undefined>(undefined);
// Why do you need this state if it's not changing?
// May you can put this in a const and save one state.
const isCelsius = true;
// I may change this "Here" in the function, because it don't look wrong, by it's subjective
// Use "ByUserLocation" it's a better description
// With the hook useCallback you can rerender the function only based on the right values
const getWeatherByUserLocation = useCallback(() => {
// Extracting the function improves the maintenance by splitting each step of the process and each callback function.
const callbackFn = async ({coords: {latitude, longitude}}) => {
// Turn your validations into variables with a well-descriptive name.
// Avoid making validation starting by negating the value;
const temperatureUnit = isCelsius ? 'celsius' : 'imperial';
// Avoid using "data" as the name of a variable even when it looks clear what the data is. In the future, you will not remember if this logic should change.
const weatherInfoData = await Weather.getWeather(
latitude,
longitude,
temperatureUnit
);
setWeatherInfo(weatherInfoData);
};
navigator.geolocation.getCurrentPosition(callbackFn);
}, [isCelsius]);
useEffect(() => {
getWeatherByUserLocation();
// removing the prop from the function, you avoid calling it many times based on the state change
// A useEffect without dependencies on the dependency array, it will only be called on the component mount and unmount
}, []);
return (
<OtherComponent isCelsius={isCelsius} getWeatherByUserLocation={getWeatherByUserLocation}/>
);
}

Should localstorage be set from within react callbacks?

In a (typescript) react app, I have some hooks for reading and writing to local storage, like this:
import { useEffect, useState } from "react";
...
export const useLocalStorage = (key, defaultValue) => {
const [value, setValue] = useState(() => {
return getStorageValue(key, defaultValue);
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
Elsewhere in the app, I have a UX element that needs to store some data in local storage, as part of an onClick() callback:
myValue, setMyValue = useLocalStorage("MY_KEY", 0);
...
onClick() => {
setMyValue("some data");
}
However, this means calling useEffect() from within a callback, which violates the hook rules.
Is it conventional here to just call localstorage.setItem() directly from withing the callback, or is there a more idiomatic way to refactor this code?
I think you're a little confused about what a hook is. Consider this snipet.
function Button() {
const [wasClicked, setWasClicked] = useState(false);
function handleClick() {
setWasClicked(true) // completely legal.
// this is not "calling a hook"
const [clickedTime, setClickedTime] = useState(Date.now()) // illegal
// this is "calling a hook"
}
return <button disabled={wasClicked} onclick={handleClick}>click!</button>
}
Calling a hook like useState(false) is as you say not permitted within callbacks. This is because the order in which hooks are called is actually super important to React. So, you can't conditionally call hooks, and you cant call them from a callback, you have to call them at the top level of your component.
That being said, setWasClicked is not a hook, it's just a regular function that happens to be returned from a hook. You can call this function from anywhere, because as stated it is not a hook.
In your case, useLocalStorage is a hook, you have to follow the rules of hooks. However, it returns setValue which is not a hook, just a regular function returned by the useState call. That triggers the useEffect callback to run, but it doesn't re-run useEffect. useEffect was called only when you called useLocalStorage.
TLDR:
To answer your question, I would put local storage stuff in a hook. You do want to use the useEffect hook because you don't want to access localStorage on every render, only when dependencies change.

Why or when should I use state within a custom Hook in React?

I am learning about custom Hooks in React. I have taken the following typical example (useFetch) as you can see in https://www.w3schools.com/react/react_customhooks.asp.
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState(null); /* <--- Why use this? */
useEffect(() => {
fetch(url).then((res) => res.json()).then((data) => setData(data));
}, [url]);
return [data];
};
export default useFetch;
I'm confused why state should be used inside that Hook, or in general in custom Hooks. I always relate state management to components, not to a hook. Hence perhaps my confusion.
Couldn't it have been done simply by returning a data variable?
Unlike normal functions, custom hooks encapsulates React state. This means that the hook can utilize react's own hooks and perform custom logic. It's quite abstract in that sense.
For your case, you want to return the state of the data, not just the data by itself because the state is tied to a useEffect. That means that fetch will only run and by extension only update data when its dependencies ([url]) are changed.
If it was a normal function just returning the data from the fetch, you would send a request every time the component using the hook re-renders. That's why you use useState coupled with useEffect to make sure it only updates when it should.

Modifying state in react-query's query function

I have written a wrapper around fetch that lets me handle error states from my API like a 401. When it gets an error, the custom fetch function sets some state using react hooks.
Because I use hooks in the function, I cannot just import it normally. Therefore, I pass this wrapped function around using context.
When I use react-query, I would like to just use this wrapped function in the following way:
function myQuery(key, apiFetch) {
return apiFetch(...)
}
function MyComponent() {
useQuery('key', myQuery)
}
In this example, apiFetch is an available argument in the query function.
One option I do have here is to just inline the function like so:
function myQuery(key, apiFetch) {
return apiFetch(...)
}
function MyComponent() {
const apiFetch = useApiFetch();
useQuery('key', (key) => apiFetch(...))
}
However I find this a little messy and would like to keep the query/mutation functions separate if possible.
Does anyone know of an approach I can take to have my apiFetch function be available to me in react-query's functions?
If you don't want to repeat calling useApiFetch and useQuery hooks in multiple components, you can extract both hooks into another custom hook that integrates react-query with your fetch hook:
function useApiQuery(param) {
const apiFetch = useApiFetch();
return useQuery(['key', param], (key) => apiFetch(...))
}
Then use it in your component:
function MyComponent({ param }) {
const { data, error } = useApiQuery(param);
return (...);
}

Resources