How to implement derived state in react hooks - reactjs

function ScrollView(props) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
useEffect(() => {
setIsScrollingDown(props.row)
}, [props.row])
// this is a fake function to show its own logic to change the state,
// and this function could be called in somewhere to make the isScrollingDown
// state to be a derived state
const handleClick = () => {
setIsScrollingDown(false)
}
return `Scrolling down: ${isScrollingDown}`;
}
In this case, i use the useEffect hook to update the derived state in response to a change in props. Can this be a common solution to resolve the derived state in some individual cases. Or if there is any problems in this case?

Generally, there are no issues with your implementation, as the isScrollingDown is merely reacting to the changes of the row props value.
However, I do feel that there is no need to use derived state in your scenario, as the logic is rather simple.
It will be much more intuitive to remove the need to maintain the component state and remove any other sources of unnecessary re-rendering.
function ScrollView({ row }) {
return `Scrolling down: ${row}`;
}
Edit: given that there is actually a need for an internal state, some other optimisations I would suggest would be to carry out a check within the useEffect hook such that you are only updating the state when the props and state values differ:
useEffect(() => {
if (props.row === isScrollingDown {
return;
}
setIsScrollingDown(props.row);
}, [props.row]);

Related

Functional component - calling useEffect on state change, but not when props changed

Working with a functional component which has the following two useEffects:
//update the state when props changed
useEffect(() => {
const newState = mapPropsToState(props);
if (!_.isEqual(newState, state)) {
setState(newState);
}
}, [props]);
//make an API call
useEffect(() => {
preferencesChanged();
}, [state]);
State is derived from props, so the purpose of the first useEffect is to respond to a change in props and update the state.
The purpose of the second useEffect is to make an API call when state has changed. However, this API call can result in the props of this component changing (since preferencesChanged() updates the state of a parent component).
What I really want is for the setState in the first useEffect to be done "quietly" and not to trigger the 2nd useEffect.
Is this possible? Or am I thinking about this design in completely the wrong way?
State is derived from props.
You can just do your logic inside of component (in render phase), instead of calling it in an effect:
//...
const newState = mapPropsToState(props);
// ...
and you can use useMemo if that is an expensive calculation:
const newState = useMemo(() => mapPropsToState(props), [props])
And to answer your question,
What I really want is for the setState in the first useEffect to be done "quietly" and not to trigger the 2nd useEffect.
You can store the relevant info (dependencies of 2nd useEffect) in a seperate state varable, and then use that.
Refs:
You might not need an effect (React docs)
Summing up some of nuances of useEffect

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]]);

React hooks - not updating consistently

So I'm semi-new to hooks. I want to run some basic validation. Running into a strange issue: when I run two hooks back-to-back, only the second of two hooks works.
const [validationTracking, setValidationTracking] = useState({});
const setValidation = (idx, field, value) => {
const validationCopy = cloneDeep(validationTracking);
if (!validationCopy[idx]) {
validationCopy[idx] = {};
}
validationCopy[idx][field] = value;
setValidationTracking(validationCopy);
};
const validateInputs = () => {
partnerInfo.forEach((object, idx) => {
if (!object['title']) {
setValidation(idx, 'title', true);
}
if (!object['body']) {
setValidation(idx, 'body', true);
}
});
};
In the above code partnerInfo=[{title: '', body: ''}]
The validation only gets triggered for body when I run validateInputs
If the array has more than one item, only the very last field will get its validation set to true [{body: true}]
The input above SHOULD set validationTracking to [{title: true, body: true}]but it seems to skip or override earlier items
I know this.setState() in class-based components is async. I'm wondering if something similar is happening here..?
There are a few things to be aware of with the useState hook:
the state change will not be immediately visible to your component logic until the next re-render of your component (and reevaluation of any closures)
if multiple calls are made to a state hook's "setter" in a single render cycle, only the last state update will be collected and applied in the subsequent render cycle
The second point is more relevant to your question, given the forEach iteration in your code makes multiple calls to the setValiadation setter of your state hook. Because these are made in a single render cycle, only the last call to setValiadation will have an observable effect.
The usual way to address this is to gather all state changes into a single object, and apply those with a single call to your setter. You could take the following approach to achieve that:
const [validationTracking, setValidationTracking] = useState({});
// Revised function
const updateValidation = (object, idx, field, value) => {
const validationCopy = cloneDeep(object);
if (!validationCopy[idx]) {
validationCopy[idx] = {};
}
validationCopy[idx][field] = value;
return validationCopy
};
const validateInputs = () => {
// Call setter via a callback that transforms current state
// into a new state object for the component
setValidationTracking(state => {
// Reduce partnerInfo array to a new state object
return partnerInfo.reduce((acc, infoObject, idx) => {
if (!infoObject['title']) {
acc = updateValidation(acc, idx, 'title', true);
}
if (!infoObject['body']) {
acc = updateValidation(acc, idx, 'body', true);
}
return acc;
}, state);
});
};
Hope that helps!
You need to understand that useState hook works in a functional way. When you call it, it triggers a re-render of the component, passing the new state value to it. State values are immutable, they are not references to values that can change. This is why we say that a React function components acts as pure functions with respect to their props.
So when you call setValidationTracking(validationCopy) twice during a single update, you send two state updates that are computed using the current state for this iteration.
I.e: when the second loop calls cloneDeep(validationTracking), validationTracking has not changed because the re-render triggered by the first loop has not happened and the state value is immutable any way.
To fix the problem you can instead pass a state updater function:
setValidationTracking(currentValidationTracking => ({
...currentValidationTracking,
[idx]: {
...(currentValidationTracking[idx] || {}),
[field]: value
}
}));

useState vs useReducer

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
(quote from https://reactjs.org/docs/hooks-reference.html#usereducer)
I'm interested in the bold part, which states that useReducer should be used instead of useState when being used in contexts.
I tried both variants, but they don't appear to differ.
The way I compared both approaches was as follows:
const [state, updateState] = useState();
const [reducerState, dispatch] = useReducer(myReducerFunction);
I passed each of them once to a context object, which was being consumed in a deeper child (I just ran separate tests, replacing the value by the function that I wanted to test).
<ContextObject.Provider value={updateState // dispatch}>
The child contained these functions
const updateFunction = useContext(ContextObject);
useEffect(
() => {
console.log('effect triggered');
console.log(updateFunction);
},
[updateFunction]
);
In both cases, when the parent rerendered (because of another local state change), the effect never ran, indicating that the update function isn't changed between renders.
Am I reading the bold sentence in the quote wrong? Or is there something I'm overlooking?
useReducer also lets you optimize performance for components that
trigger deep updates because you can pass dispatch down instead of
callbacks.
The above statement is not trying to indicate that the setter returned by useState is being created newly on each update or render. What it means is that when you have a complex logic to update state you simply won't use the setter directly to update state, instead you will write a complex function which in turn would call the setter with updated state something like
const handleStateChange = () => {
// lots of logic to derive updated state
updateState(newState);
}
ContextObject.Provider value={{state, handleStateChange}}>
Now in the above case everytime the parent is re-rendered a new instance of handleStateChange is created causing the Context Consumer to also re-render.
A solution to the above case is to use useCallback and memoize the state updater method and use it. However for this you would need to take care of closure issues associated with using the values within the method.
Hence it is recommended to use useReducer which returns a dispatch method that doesn't change between re-renders and you can have the manipulation logic in the reducers.
Practical observation on useReducer and useState -
UseState:
In my React Native project I've 1 screen containing 25+ different states created using useState.
I'm calling an api in useEffect (componentDidMount) and on getting the response based on some conditions, I'm setting up these 25 states, calling 25 state setter function for each function.
I've put a re-rendering counter and checked my screen is re-rendered 14 times.
re-rendering count likewise :
let count = 0;
export default function Home(props) {
count++;
console.log({count});
//...
// Rest of the code
}
UseReducer :
Then I've moved these 25 states in useReducer states, And used only single action to update these states on API response.
I've observed there is only 2 re-render.
//API calling method:
fetchData()
{
const response = await AuthAxios.getHomeData();
dispatch({type: 'SET_HOME_DATA', data: response.data});
}
//useReducer Code:
const initialStaes = {
state1: null,
state2: null,
//.....More States
state27: null,
state28: null
}
const HomeReducer = (state, action) => {
switch (action.type) {
case 'SET_HOME_DATA': {
return {
...state,
state1: (Data based on conditions),
state2: !(some Conditions ),
//....More states
state27: false
}
}
}
}
Advantage of useReducer in this case :
Using useReducer I've reduced number of re-renders on the screen, hence better performance and smoothness of the App.
Number of lines is reduced in my screen itself. It improved code readablity.
When you need to care about it
If you create a callback on render and pass it to a child component, the props of that child will change. However, when the parent renders, a regular component will rerender (to the virtual dom), even props remain the same. The exception is a classComponent that implements shouldComponentUpdate, and compares props (such as a PureComponent).
This is an optimization, and you should only care about it if rerendering the child component requires significant computation (If you render it to the same screen multiple times, or if it will require a deep or significant rerender).
If this is the case, you should make sure:
Your child is a class component that extends PureComponent
Avoid passing a newly created function as a prop. Instead, pass
dispatch, the setter returned from React.useState or a memoized
customized setter.
Using a memoized customized setter
While I would not recommend building a unique memoized setter for a specific component (there are a few things you need to look out for), you could use a general hook that takes care of implementation for you.
Here is an example of a useObjState hook, which provides an easy API, and which will not cause additional rerenders.
const useObjState = initialObj => {
const [obj, setObj] = React.useState(initialObj);
const memoizedSetObj = React.useMemo(() => {
const helper = {};
Object.keys(initialObj).forEach(key => {
helper[key] = newVal =>
setObj(prevObj => ({ ...prevObj, [key]: newVal }));
});
return helper;
}, []);
return [obj, memoizedSetObj];
};
function App() {
const [user, memoizedSetUser] = useObjState({
id: 1,
name: "ed",
age: null,
});
return (
<NameComp
setter={memoizedSetUser.name}
name={user.name}
/>
);
}
const NameComp = ({name, setter}) => (
<div>
<h1>{name}</h1>
<input
value={name}
onChange={e => setter(e.target.value)}
/>
</div>
)
Demo

Resources