React hooks - not updating consistently - reactjs

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

Related

React State not updating using function updater

I'm having trouble figuring out why my React component won't update when I set its state. I'm using the Context API in a Provider pattern, but this happens when I use the useState hook as well.
I've tried useState, I've tried creating a new object to ensure it's not catching a reference, , and I've stepped through the debugger to ensure the function returns the expected value. But the state just won't update
Here's my code. I'm attaching the state updater to an event listener which fires when a user deletes a component from a wysiwyg editor. The second setLiveComponents works just fine. The first one doesn't.
Would the fact that it's called in a callback change anything? I don't think it would...
function App() {
const [liveComponents, setLiveComponents] = useLiveComponents();
editor.on("component:add", (component, block) => {
component.removed = () => {
setLiveComponents((current) => {
//This accomplishes the same thing as "delete obj[key]" but creates a new object
var keepKeys = Object.keys(current).filter(
(entry) => entry != component.getId()
);
var newObj = keepKeys.reduce((obj, key) => {
obj[key] = current[key];
return obj;
}, {});
return newObj;
});
};
var newObj = {};
let customComponent = { settings: {}, gjsComponent: component };
newObj[component.getId()] = customComponent;
setLiveComponents((liveState) => {
return { ...liveState, ...newObj };
});
Any ideas would be so helpful. Thanks!
For anyone wondering the problem here was the setter was getting passed into another library's event handler, which was running in a different execution context. I had assumed it would pass the setter method properly and execute properly, but no.
The solution was to turn this into a class component and bind the setter to this (this.setter = setter.bind(this))
I'm still not fully clear on the issue with the functional approach. Would love any insight!

How can I set React component state based on previous state using Immer?

I'm trying to figure out how to set my React component's state based on the previous state, while also using the Immer library to allow for easier immutable state changes.
Normally, without Immer, I'd safely access the component's previous state using the updater function version of setState():
this.setState(prevState => {
prevState.counter = prevState.counter + 1;
});
However, when using the Immer library to allow for easier immutable state changes, you no longer have access to the updater function, or the prevState variable. This is fine for most operations, as the Immer draft proxy can be used for simple changes - but it doesn't work for more complex changes such as comparisons, as the proxy doesn't reflect the original state object:
this.setState(produce(draft => {
const index = draft.list.indexOf(item); // doesn't work - always returns -1
if (index > -1) {
draftState.list.splice(index, 1);
}
});
The problem is that since the draft state is a proxy, comparisons such as indexOf always fail, since the two objects are inherently different. But I don't want to just use this.state in my produce function, as the React docs are very clear that you shouldn't rely on its value when calculating the new state, and I don't want to do the state update without Immer, as complex state object changes are significantly simpler when working with a mutable proxy object.
Is there any way to access an up to date version of the component's previous state, while still using the Immer produce function in my setState calls?
Edit:
Updating to add a (simplified) example of my actual code:
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
fooBar: {
selectedOptions: []
}
};
}
handleClick(clickedOption) {
/*
if clickedOption is already selected
remove option from selectedOptions
else
add option to selectedOptions
*/
this.setState(produce(draft => {
const index = draft.fooBar.selectedOptions.indexOf(clickedOption); // The problem is here
if (index > -1) {
draft.fooBar.selectedOptions.splice(index, 1);
}
else {
draft.fooBar.selectedOptions.push(clickedOption);
}
}));
}
The problem is at the const index = ... line. If I use draft, then the indexOf function always returns -1, and if I use this.state, then I'm not guaranteed an up-to-date version of selectedOptions to compare against...
You could implement a usePrevious() hook yourself to store any previous variable:
function usePrevious(value) {
const ref = useRef();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}

How to implement derived state in react hooks

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

Where is the best place to make calculations outside the render method in React?

I have a render method in my container component like this:
render() {
const { validationErrors } = this.state
const { errorsText, errorsFields} = validationErrors.reduce(
(acc, error) => {
acc.errorsText.push(error.text)
acc.errorsFields[error.field.toLowerCase()] = true
return acc
},
{
errorsText: [],
errorsFields: {},
},
)
return (
<MyViewComponent
errorsText={errorsText}
errorsFields={errorsFields}
/>
)
}
As you can see every render there are some computations happens (returned array and object with the new values), then I pass it into my child component as a props. I have a feeling that this is a wrong pattern. We should keep render function 'pure'. Isn't it? The question is: Where is the best place for making such computations outside the render?
If this were a functional component (which I highly recommend you use in the future, by the way), you'd be able to use the 'hook' useEffect to recalculate errorsText and errorsField whenever this.state.validationErrors changes, and only when it changes.
For your Class Component, however, I assume at some point you set this.state.validationErrors. What you should do is create a method that runs your reducer and stores errorsText and errorsField to state, then place a call to this method after each point you set this.state.validationErrors. Then, remove the logic in the render method and replace errorsText and errorsField with this.state.errorsText and this.state.errorsField respectively.
Doing this will ensure you only ever run your reducer when necessary (i.e. when this.state.validationErrors changes).
Your component would end up looking something like this:
class MyComponent extends Component {
...
someCallback() {
const validationErrors = someFunctionThatReturnsErrors();
// We do the logic here, because we know that validationErrors
// could have changed value
const { errorsText, errorsFields } = validationErrors.reduce(
(acc, error) => {
acc.errorsText.push(error.text);
acc.errorsFields[error.field.toLowerCase()] = true;
return acc;
}, {
errorsText: [],
errorsFields: {},
},
);
// Put everything in the state
this.setState({
validationErrors, // you may not even need to set this if it's not used elsewhere`
errorsText,
errorsFields
});
}
...
render() {
const {
errorsText,
errorsFields
} = this.state;
return (
<MyViewComponent
errorsText={errorsText}
errorsFields={errorsFields}
/>
);
}
}
It is pure, as it has no side effects.
As long as this does not create performance issues I see no problem with this. If it does create performance issues, you should look into memoizing the reduce. If you were using hooks you could use the built-in React.useMemo for this. While using class version you could look into something like https://www.npmjs.com/package/memoize-one

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