Use custom hook in callback function - reactjs

I have a customHook, and I need to call it in two places. One is in the top level of the component. The other place is in a onclick function of a button, this button is a refresh button which calls the customHook to fetch new data like below. I am thinking of two approaches:
create a state for the data, call hook and set the data state in the component and in the onclick function, call hook and set the data state. However, the hook cannot be called inside another function i.e onclick in this case.
create a boolean state called trigger, everytime onclick of the button, toggle the trigger state and pass the trigger state into the myCallback in the dependent list so that myCallback function gets recreated, and the hook gets called. However, I don't really need to use this trigger state inside the callback function, and the hook gives me error of removing unnecessary dependency. I really like this idea, but is there a way to overcome this issue?
Or is there any other approaches to achieve the goal?
const MyComponent = () => {
const myCallback = React.useCallback(() => { /*some post processing of the data*/ }, []);
const data = customHook(myCallback);
return <SomeComponent data={data}>
<button onclick={/*???*/}></button>
</SomeComponent>;
};

It is possible to make your second example work with some tweaking. Instead of passing in a dependency to update the effect function, just make the effect function a stand-alone function that you pass into useEffect, but can also call in other places (e.g. you can return the effect function from your hook so your hook users can use it too)
For example:
const App = () => {
const { resource, refreshResource } = useResource()
return (
<div>
<button onClick={refreshResource}>Refresh</button>
{resource || 'Loading...'}
</div>
)
}
const useResource = () => {
const [resource, setResource] = useState(null)
const refreshResource = async () => {
setResource(null)
setResource(await fetchResource())
}
useEffect(refreshResource, [])
return { resource, refreshResource }
}
const fetchResource = async () => {
await new Promise(resolve => setTimeout(resolve, 500))
return Math.random()
}
Edit
I hadn't realized that the hook couldn't be edited. I honestly can't think of any good solutions to your problem - maybe one doesn't exist. Ideally, the API providing this custom hook would also provide some lower-level bindings that you could use to get around this issue.
If worst comes to worst and you have to proceed with some hackish solution, your solution #2 of updating the callback should work (assuming the custom hook refetches the resource whenever the parameter changes). You just have to get around the linting rule, which, I'm pretty sure you can do with an /* eslint-disable-line */ comment on the line causing the issue, if eslint is being used. Worst comes to worst, you can make a noop function () => {} that you call with your trigger parameter - that should put the linter at bay.

Related

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

React make an async call on data after retrieving it from the state

I need to make an async call after I get some data from a custom hook. My problem is that when I do it causes an infinite loop.
export function useFarmInfo(): {
[chainId in ChainId]: StakingBasic[];
} {
return {
[ChainId.MATIC]: Object.values(useDefaultFarmList()[ChainId.MATIC]),
[ChainId.MUMBAI]: [],
};
}
// hook to grab state from the state
const lpFarms = useFarmInfo();
const dualFarms = useDualFarmInfo();
//Memoize the pairs
const pairLists = useMemo(() => {
const stakingPairLists = lpFarms[chainIdOrDefault].map((item) => item.pair);
const dualPairLists = dualFarms[chainIdOrDefault].map((item) => item.pair);
return stakingPairLists.concat(dualPairLists);
}, [chainIdOrDefault, lpFarms, dualFarms]);
//Grab the bulk data results from the web
useEffect(() => {
getBulkPairData(pairLists).then((data) => setBulkPairs(data));
}, [pairLists]);
I think whats happening is that when I set the state it re-renders which causes hook to grab the farms from the state to be reset, and it creates an infinite loop.
I tried to move the getBulkPairData into the memoized function, but that's not meant to handle promises.
How do I properly make an async call after retrieving data from my hooks?
I am not sure if I can give you a solution to your problem, but I can give you some hints on how to find out the cause:
First you can find out if the useEffect hook gets triggered too often because its dependency changes too often, or if the components that contains your code gets re-mounted over and over again:
Remove the dependency of your useEffect hook and see if it still gets triggered too often. If so, your problem lies outside of your component.
If not, find out if the dependencies of your useMemo hook change unexpectedly:
useEffect(()=>console.log("chainIdOrDefault changed"), [chainIdOrDefault]);
useEffect(()=>console.log("lpFarms changed"), [lpFarms]);
useEffect(()=>console.log("dualFarms changed"), [dualFarms]);
I assume, this is the most likely reason - maybe useFarmInfo or useDualFarmInfo create new objects on each render (even if these objects contain the same data on each render, they might not be identical). If so, either change these hooks and add some memoization (if you have access to your code) or narrow down the dependencies of your pairLists:
const pairLists = useMemo(() => {
const stakingPairLists = lpFarms[chainIdOrDefault].map((item) => item.pair);
const dualPairLists = dualFarms[chainIdOrDefault].map((item) => item.pair);
return stakingPairLists.concat(dualPairLists);
}, [lpFarms[chainIdOrDefault], dualFarms[chainIdOrDefault]]);

New React hook for callbacks

I've been using React for a couple years, with functional components and hooks. The most frustrating thing I've found has to do with passing callbacks. I have two concerns:
If an element receives a callback from a parent it can't really use that function in a event listener or onEvent field or in a setTimeout/setInterval, because the passed function may be redefined any time the parent rerenders.
A function that's generated at each render (like an arrow function) and passed to a child element breaks React.memo().
I've seen some limited solutions to this, such as useEventListener() but not one that addresses all the situations that concern me.
So I've written my own hook that I've called useHandler. Unlike useCallback, which generates a new function whenever the inputs change, useHandler always returns the same function. This way the function case be passed to an element using React.memo() or to a setInterval or whatever.
It call be used by the parent element (needed for the React.memo case) or by the child element (if you're worried that a passed-in function might change). Here's an example of it used in the parent:
const onClick = useHandler(event => {
//Do whatever you want. Reference useState variables or other values that might change,
})
<MyElement onClick={onClick} />
MyElement now always gets the same function, but calling that function will execute the most recent code in the parent element.
Here's an example in the child element:
const reportBack = useHandler(props.reportBack)
useEffect(() => {
setInterval(reportBack, 1000)
}, [reportBack])
The interval callback will always call the current reportBack passed from the parent.
The only thing missing for me is that I haven't modified the React eslint config to recognize that that results of useHandler cannot change and thus shouldn't generate a warning if omitted from the dependency list in useEffect or similar.
And finally, here's the source for my onHandler hook:
export function useHandler(callback) {
const callbackRef = useRef()
callbackRef.current = callback
return useRef((...args) => callbackRef.current(...args)).current
}
My questions are: Are there any flaws in this solution? Is it a good way to address my issues with React callbacks? I haven't found anything more elegant but I'm plenty willing to believe that I've missed something.
Thanks.
I can't comment on the second point as I've not used React.memo but the first point is why we have cleanup functions.
Let's say you have a function that is passed as a prop down to your component and you want to use it as an event listener.
If you are attaching this listener inside a useEffect like: window.addEventListener('someEvent', (event) => someFunctionPassedAsProp(event))
Then the useEffect can look something like:
useEffect(() => {
const handler = (event) => someFunctionPassedAsProp(event)
window.addEventListener('someEvent', handler)
return () => window.removeEventListener('someEvent', handler)
}, [someFunctionPassedAsProp])
Similarly for intervals you can do something like:
useEffect(() => {
const interval = setInterval(() => someFunctionPassedAsProp(), 1000)
return () => clearInterval(interval)
}, [someFunctionPassedAsProp])

Hooks setState always one step behind

I used the useState hook. Was supposed to trigger a set state method (in hooks) everytime the value of a dropdown button changes but the set state always happen one step behind. I've seen solutions with the traditional setState methods of class based components, but how do i fix this using hooks useState?
<Dropdown
placeholder='Select College'
search
fluid
selection
options={collegeSelection}
onChange={selectCollegeHandler}
/>
Method:
const selectedCollegeHandler = (event, data) => {
setSelectedCollege(data.value);
}
State:
const [selectedCollegeState, setSelectedCollege] = useState(' ');
For completion it's worth mentioning that useEffect is designed to help with this asynchrony. In fact, according to the Hooks docs, the hooks solution is to use the Effect Hook, (which essentially is implementing a callback with useState).
const CollegeForm = () => {
const [college, setCollege] = useState(null)
useEffect(() => {
    console.log(college); // add whatever functions use new `college` value here.
}, [college]);
return (
<form onChange={e => setCollege(e.target.value)} />
    )
};
This (side) Effect is called when (and only when) Bob chooses a new college in your form. Whatever logic is based on his every indecisive prevarication over college choice can go inside this useEffect callback. (You can also use Effect without the dependency array, [college], which will cause these (side) effects every time any state changes).
To explain relative to the class based components: with the "traditional" setState methods we could just add a callback as a second argument to setState so the new state was used immediately:
this.setState({ college: data.value }, () => console.log(new value));
The callback prevents it being queued in what the React docs call a pending state transition. But we can no longer do this because hooks do not take callbacks.
As cbdeveloper says, sometimes passing a function to setState is appropriate, for example:
[isOnline, toggleIsOnline] = useState(false);
<Button onClick={() => toggleIsOnline(prevState => !prevState)} /> 
But this isn't always appropriate, especially if you're not actually using the prevState. 
Try this. Using the functional form of setState() you can "trick" React into thinking that your new state depends on your last state, so it does the update right away. This has helped me on a number of ocasions. See if that helps you too.
method:
const selectedCollegeHandler = (event, data) => {
setSelectedCollege((prevState) => {
return data.value
});
}
Functional updates
If the new state is computed using the previous
state, you can pass a function to setState. The function will receive
the previous value, and return an updated value. Here’s an example of
a counter component that uses both forms of setState:
Source: Hooks API
Try async function. It solved the same issue for me.
const selectedCollegeHandler = async(event, data) => {
await setSelectedCollege(data.value);
}

In React Hook useCallback, How are (a,b) used

In the react hooks doc, they give an example of using the useCallback React hook as follows:
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
I have an example that has a callback that gets called with a parameter (not a or b) and it seems to work. Please explain to me what a,b are and how they are meant to be used. Below is my working code using a callback.
const signupCallback = email => {
return console.log(`sign up called with email ${email}`);
};
const memoizedsignupCallback = useCallback(() => {
signupCallback();
}, []);
and the call that uses the callback.
<SignMeUp signupCallback={memoizedsignupCallback} />
This is the array of values that the hook depends on. When these values change, it causes the hook to re-execute. If you don't pass this parameter, the hook will evaluate every time the component renders. If you pass in [], it will only evaluate on the initial render.
Documentation regarding this is available here: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect.
If you do pass this array of parameters, it is very important to include all of the state that can change and is referenced in the hook closure. If you forget to include something, the values in the closure will become stale. There is an eslint rule that checks for this issue (the linked discussion also contains more details): https://github.com/facebook/react/issues/14920.
You are correct in that useCallback is used to memoize a function. You can think of a and b (or anything used in the second argument of useCallback) as the keys to the memoized function. When either a or b change, a new function is created.
This is useful especially when you want something to be called on an onClick that requires some values from your component's props.
Similar to your example instead of creating a new function on every render:
const Signup = ({ email, onSignup }) => {
return <button onClick={() => onSignup(email) } />;
}
you would use useCallback:
const Signup = ({ email, onSignup }) => {
const onClick = useCallback(() => onSignup(email), [email, onSignup]);
return <button onClick={onClick} />;
}
This will ensure that a new function is created and passed to onClick only if email or onSignup change.
The use of parameter a, b depends on whether the function that you are trying to execute takes them from the enclosing scope or not.
When you create a function like
const signupCallback = email => {
return console.log(`sign up called with email ${email}`);
};
const memoizedsignupCallback = useCallback(() => {
signupCallback();
}, []);
In the above case memoizedsignupCallback is created on initial render and it will have access to the values from the enclosing closure when it is created. Not if you want to access a value that lies within its closure but can update due to some interaction, you need to recreate the memoized callback and hence you would pass in the arguments to useCallback.
However in your case the value that memoizedsignupCallback uses is passed on by the caller while executing the method and hence it would work correctly
DEMO

Resources