React State not updating using function updater - reactjs

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!

Related

React useEffect runs but does not call function

I am trying to write a useEffect hook which will update the DOM once when the page loads. For some reason that I cannot fathom, the useEffect runs but the function does not get called. The console.log() on the other hand does get called.
There is no problem with the function itself, it works when I run it from the console or trigger it in some other way. I have also tried using different syntax for writing and/or calling the function, placing inside or outside the useEffect. The result is always the sameā€”the DOM does not update but the console.log() logs.
Please help me, what am I doing wrong/ misunderstanding? Thank you in advance.
// Add ids to h2 elements inside markdown
useEffect(() => {
function setID() {
const H2List = document.getElementsByTagName("h2");
const H2Array = [...H2List];
H2Array.map((a) => (
a.setAttribute('id',
`${H2Array[H2Array.indexOf(a)].childNodes[0].nodeValue
.split(" ").join("-").toLowerCase()}`)
))
}
console.log("useeffect ran once");
setID();
}, [])
After some more Googling, this works:
window.onload = function() {
setID()
}
function setID() {
const H2List = document.getElementsByTagName("h2");
const H2Array = [...H2List];
H2Array.map((a) => (
a.setAttribute('id',
`${H2Array[H2Array.indexOf(a)].childNodes[0].nodeValue
.split(" ").join("-").toLowerCase()}`)
))
}
Thank you to this source.
if you want to update the dom you should cause a rerender in your app
define a state and setState your new data

Use custom hook in callback function

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.

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

React: Passing props to parent returns <empy string>

First let me put my code here and I'll explain what's happening.
Parent.js
callback = (id) => {
this.setState({des: id});
console.log(this.state.des);
}
//on my render i have this when i call my child component
<Child callback={this.callback}/>
Child.js
handleChange = (event) => {
let des = event.target.value;
console.log(des);
this.props.callback(des);
};
When i console.logon my Child component, it returns the data that i want to pass, but when I do it in calbackon my Parent component, it returns <empty string> and i don't know why that's happening.
The reason this is happening is because setState is an async function. When you are trying to log this.state.des, the state would not have been set yet. If you want to console log your state to see if it has been set as expected, what you want to do is log it in the callback of this.setState (so it logs once we know state is set). Try something like the following in your parent.js :
callback = (id) => {
this.setState({des: id}, () => {
console.log(this.state.des);
});
}
see the React Docs for setState for more details
The call to setState is asynchronous and therefore you might not read the updated state if you are accessing it directly after calling setState. Because of this setState(updater[, callback]) actually exposes a callback which can be used for operations which depend on the state update being done. (This is explained in the react docs for setState.)
In your case, adjusting the callback function like this
callback = (id) => {
this.setState({des: id}, () => {
console.log(this.state.des);
});
}
should do the trick.
If you want to know more about the reasoning behind setState being asynchronous (even if it might be a bit confusing in the beginning, like in your case) you should check out this github issue and especially this comment.

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

Resources