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}/>
);
}
Related
i have a doubt about React hook, his props and useEffect.
In my component, i receive a list inside props, i must filter the received list and get a value from the list before the component is mounted.
(this code is a simplified example, i apply another filters and conditions more complex)
function MyComponent(props) {
const [value, setValue] = useState(null)
useEffect(()=>{
var found = props.myList.find((x)=> {return x.name=="TEST"});
if(found.length > 0)
setValue("Hello World")
}, []);
return (
<div>{value}</div>
)
}
Is correct to use useEffect for get and set a value respect to props before the component is mounted?
This useEffect is executed after the 1st render, so null would be "rendered" first, and then the useEffect would update the state causing another render.
However, if your value is dependent on the props, and maybe on other states (filters for example), this is a derived value, and it doesn't need to be used as a state. Instead calculate it on each render, and if it's a heavy computation that won't change on every render wrap it with useMemo:
function MyComponent({ myList }) {
const value = useMemo(() => {
const found = myList.some(x => x.name=="TEST");
return found ? 'Hello World' : '';
}, [myList]);
return (
<div>{value}</div>
)
}
There's no need to use useEffect here, you can simply pass a callback to the useState hook. The callback is only ran on the initialization of the hook so this has the same outcome as your useEffect hook (i.e. it doesn't run on every rerender so same performance hit).
function MyComponent({ myList }) {
const [value, setValue] = useState(() => {
const found = myList.find(x => x.name === 'TEST');
return found ? 'Hello World' : null;
});
return (
<div>{value}</div>
);
}
No need for any hook at all, actually, you should just write it like this:
function MyComponent({ myList }) {
const found = myList.some(x => x.name == "TEST");
const value = found ? 'Hello World' : '';
return (
<div>{value}</div>
)
}
Using useEffect or even only useState is conceptually wrong. The useState hook is used when a component needs to have its own independent internal state which isn't the case here, as the value is purely derived from the props. The useEffect hook is an escape hatch typically used when you need to sync with some external imperative API, not at all the case here.
The solution with useMemo is perfectly fine, and more efficient if your filtering condition is very computationally expensive, but it is also a lot more complicated to understand and to maintain, so I would suggest going for the simplest solution until the lack of memoization actually becomes a measurable problem.
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]]);
I'm using useRef to hold the latest value of a prop so that I can access it later in an asynchronously-invoked callback (such as an onClick handler). I'm using a ref instead of putting value in the useCallback dependencies list because I expect the value will change frequently (when this component is re-rendered with a new value), but the onClick handler will be called rarely, so it's not worth assigning a new event listener to an element each time the value changes.
function MyComponent({ value }) {
const valueRef = useRef(value);
valueRef.current = value; // is this ok?
const onClick = useCallback(() => {
console.log("the latest value is", valueRef.current);
}, []);
...
}
The documentation for React Strict Mode leads me to believe that performing side effects in render() is generally unsafe.
Because the above methods [including class component render() and function component bodies] might be called more than once, it’s important that they do not contain side-effects. Ignoring this rule can lead to a variety of problems, including memory leaks and invalid application state.
And in fact I have run into problems in Strict Mode when I was using a ref to access an older value.
My question is: Is there any concern with the "side effect" of assigning valueRef.current = value from the render function? For example, is there any situation where the callback would receive a stale value (or a value from a "future" render that hasn't been committed)?
One alternative I can think of would be a useEffect to ensure the ref is updated after the component renders, but on the surface this looks unnecessary.
function MyComponent({ value }) {
const valueRef = useRef(value);
useEffect(() => {
valueRef.current = value; // is this any safer/different?
}, [value]);
const onClick = useCallback(() => {
console.log("the latest value is", valueRef.current);
}, []);
...
}
For example, is there any situation where the callback would receive a stale value (or a value from a "future" render that hasn't been committed)?
The parenthetical is the primary concern.
There's currently a one-to-one correspondence between render (and functional component) calls and actual DOM updates. (i.e. committing)
But for a long time the React team has been talking about a "Concurrent Mode" where an update might start (render gets called), but then get interrupted by a higher priority update.
In this sort of situation it's possible for the ref to end up out of sync with the actual state of the rendered component, if it's updated in a render that gets cancelled.
This has been hypothetical for a long time, but it's just been announced that some of the Concurrent Mode changes will land in React 18, in an opt-in sort of way, with the startTransition API. (And maybe some others)
Realistically, how much this is a practical concern? It's hard to say. startTransition is opt-in so if you don't use it you're probably safe. And many ref updates are going to be fairly 'safe' anyway.
But it may be best to err on the side of caution, if you can.
UPDATE: Now, the beta docs also say you should not do it:
Do not write or read ref.current during rendering, except for
initialization. This makes your component’s behavior unpredictable.
By initialization above they mean such pattern:
function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
....
To the best of my knowledge, it is safe, but you just need to be aware that changes to the ref-boxed value may occur when React "feels like" rendering your component and not necessarily deterministically.
This looks a lot like react-use's useLatest hook (docs), reproduced here since it's trivial:
import { useRef } from 'react';
const useLatest = <T>(value: T): { readonly current: T } => {
const ref = useRef(value);
ref.current = value;
return ref;
};
export default useLatest;
If it works for react-use, I think it's fine for you too.
function MyComponent({ value }) {
const valueRef = useRef(value);
valueRef.current = value; // is this ok?
const onClick = useCallback(() => {
console.log("the latest value is", valueRef.current);
}, []);
...
}
I don't really see an issue here as valueRef.current = value will occur every render cycle. It's not expensive, but it will happen every render cycle.
If you use an useEffect hook then you at least minify the number of times you set the ref value to only when the prop actually changes.
function MyComponent({ value }) {
const valueRef = useRef(value);
useEffect(() => {
valueRef.current = value;
}, [value]);
const onClick = useCallback(() => {
console.log("the latest value is", valueRef.current);
}, []);
...
}
Because of the way useEffect works with the component lifecycle I'd recommend sticking to using the useEffect hook and keeping normal React patterns. Using the useEffect hook also provides a more deterministic value per real render cycle, i.e. the "commit phase" versus the "render phase" that can be cancelled, aborted, recalled, etc...
Curious though, if you just want the latest value prop value, just reference the value prop directly, it will always be the current latest value. Add it to the useCallback hook's dependency. This is essentially what you are accomplishing with the useEffect to update the ref, but in a clearer manner.
function MyComponent({ value }) {
...
const onClick = useCallback(() => {
console.log("the latest value is", value);
}, [value]);
...
}
If you really just always want the latest mutated value then yeah, skip the useCallback dependencies, and skip the useEffect, and mutate the ref all you want/need and just reference whatever the current ref value is at the time the callback is invoked.
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.
I have a component that's supposed to read a property from the component (which is either string "fill" or string "stroke") and pull the according key from an object to read it's context.
This gets mounted as soon as an object is selected, accesses the active object and pulls out it's color either as a fill color or a stroke color.
useEffect(() => {
setButtonColor(context.objects.getActiveObject()[props.type]);
}, []); //on mount
Mounting it like this:
<ColorPicker type="fill" />
<ColorPicker type="stroke" />
This supposed to run only once on mount. I thought when the dep array is empty, it runs on any case once when it's mounted.
So how do I run something once on mount utilizing props and context?
And why does it need a dependency at all when I want it to ALWAYS run only ONCE on mount, no matter what?
It's best to move away from the thinking that effects run at certain points in the lifecycle of a component. While that is true, a model that might help you better get to grips with hooks is that the dependency array is a list of things that the effect synchronizes with: That is, the effect should be run each time those things change.
When you get a linter error indicating your dependency array is missing props, what the linter is trying to tell you is that your effect (or callback, or memoization function) rely on values that are not stable. It does this because more often than not, this is a mistake. Consider the following:
function C({ onSignedOut }) {
const onSubmit = React.useCallback(() => {
const response = await fetch('/api/session', { method: 'DELETE' })
if (response.ok) {
onSignedOut()
}
}, [])
return <form onSubmit={onSubmit}>
<button type="submit">Sign Out</button>
</form>
}
The linter will issue a warning for the dependency array in onSubmit because onSubmit depends on the value of onSignedOut. If you were to leave this code as-is, then onSubmit will only be created once with the first value of onSignedOut. If the onSignedOut prop changes, onSubmit won't reflect this change, and you'll end up with a stale reference to onSignedOut. This is best demonstrated here:
import { render } from "#testing-library/react"
it("should respond to onSignedOut changes correctly", () => {
const onSignedOut1 = () => console.log("Hello, 1!")
const onSignedOut2 = () => console.log("Hello, 2!")
const { getByText, rerender } = render(<C onSignedOut={onSignedOut1} />)
getByText("Sign Out").click()
// stdout: Hello, 1!
rerender(<C onSignedOut={onSignedOut2} />)
getByText("Sign Out").click()
// stdout: Hello, 1!
})
The console.log() statement does not update. For this specific example that would probably violate your expectations as a consumer of the component.
Let's take a look at your code now.
As you can see, this warning is essentially stating that your code might not be doing what you think it is doing. The easiest way to dismiss the warning if you're sure you know what you're doing is to disable the warning for that specific line.
useEffect(() => {
setButtonColor(context.objects.getActiveObject()[props.type]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); //on mount
The correct way to do this would be to place your dependencies inside of the array.
const { type } = props
useEffect(() => {
setButtonColor(context.objects.getActiveObject()[type]);
}, [context, type]);
This would, however, change the button colour every time type changed. There's something to note here: You're setting state in response to props changing. That's called derived state.
You only want that state to be set on the initial mount. Since you only want to set this on the initial mount, you could simply pass your value to React.useState(initialState), which would accomplish exactly what you want:
function C({ type }) {
const initialColor = context.objects.getActiveObject()[type];
const [color, setButtonColor] = React.useState(initialColor);
...
}
This still leaves the problem that the consumer might be confused as to why the view does update when you change the props. The convention that was common before functional components took off (and one I still use) is to prefix props that are not monitored for changes with the word initial:
function C({ initialType }) {
const initialColor = context.objects.getActiveObject()[initialType];
const [color, setButtonColor] = React.useState(initialColor);
}
You should still be careful here, though: It does mean that, for the lifetime of C, it will only ever read from context or initialType once. What if the value of the context changes? You might end up with stale data inside of <C />. That might be acceptable to you, but it's worth calling out.
React.useRef() is indeed a good solution to stabilize values by only capturing the initial version of it, but it's not necessary for this use-case.
This is my workaround for the issue:
Set the color to a variable and then use that variable to set the button color on mount of the component.
const oldColor = useRef(context.objects.getActiveObject()[props.type]);
useEffect(() => {
setButtonColor(oldColor.current);
}, []); //on mount
useRef returns a mutable ref object whose .current property is
initialized to the passed argument (initialValue). The returned object
will persist for the full lifetime of the component.