I have a function that's invoked on click to update the state, but only updates when it's clicked twice. For example, if I click once, it duplicates the prior update, and only when I click once more, the state gets updated.
const select = day => {
let markedDay = day.dateString
setSelected([...selected, markedDay])
let obj = selected.reduce((c, v) => Object.assign(c, {[v]: { selected: true, disableTouchEvent: true }}), {})
setBooking(obj)
}
The purpose of the function is to 1) gather the data in an array with the setSelected hook and 2) convert it to an object with newly assigned properties using Object.assign and reduce.
When the function is invoked once, the state selected from the hook setSelected shows either empty if invoked for the very first time or the previous state. The same pattern goes for obj. I want the function to update the state upon the first invocation, and not have to be invoked twice.
Update
I've simplified the function to following, but still the same problem:
const select = day => {
let markedDay = day.dateString
setSelected({...selected, [markedDay]: { selected: true, disableTouchEvent: true }})
}
The function is for react-native-calendars and an example of selected would be:
"2019-11-18": Object {
"selected": true,
"disableTouchEvent": true,
}
Each time I select a date, I want to see the above object updated on the state, but only when I click it for the second time, it gets updated.
Setting state in React is async, so the updates won't be reflected to the state until next render. The issue with your code is that you're using the new value of state right after it's set, however the updated value will be available only on the next component render. To fix this issue you can reuse the updated value of selected before setting it to state:
const select = day => {
let markedDay = day.dateString
const newSelected = [...selected, markedDay];
let obj = newSelected.reduce((c, v) => Object.assign(c, {
[v]: {
selected: true,
disableTouchEvent: true
}
}), {})
setSelected(newSelected)
setBooking(obj);
}
Alternatively, if you want to use the updated state value in the setBooking, you'd need to make that call inside the useEffect while tracking selected variable.
const select = day => {
let markedDay = day.dateString
setSelected([...selected, markedDay]);
}
useEffect(() => {
let obj = selected.reduce((c, v) => Object.assign(c, {
[v]: {
selected: true,
disableTouchEvent: true
}
}), {})
setBooking(obj);
}, [selected])
That might be a preferable approach when you have more complex state.
I can only hazard a guess on what's happening, but I think that what we're looking at here is a race condition. I'm guessing that either setBooking or setSelected is supposed to take values from your state and adjust the rendering of the final product, yes?
The problem is that setting state in react is asynchronous. If I say
console.log(this.state.foo) // 'bar'
this.setState({foo:'fizz'})
console.log(this.state.foo) // still 'bar'
react is still setting the state in the background; it hasn't finished yet, so I get the old value.
There are three methods to fixing this error.
Method 1: Make your render statements depend on the state/call functions that depend on state. That way, whenever the state updates, what you're rendering updates.
Method 2: Pass the parameters that you wanted to store in state as arguments to your setBooking or setSelected function(s). No need to worry about race conditions if they're just receiving the values.
Method 3: You can write force setState to be synchronous.
this.setState({foo:'bar'},()=>{
//do something
})
The "do something" portion of this code will not execute until after state has been set. It has a little bit of code smell (You're not taking advantage of asynch, after all) but sometimes it's necessary if you want to re-use a function in componentDidMount as well as in the life cycle of your component.
Related
I have an issue trying to watch a prop in a child component. Here's a playground link with a a little code to reproduce the issue:
Vue SFC Playground
The child component (in orange) inherits the data from the parent (just an array). The child component copies the data from the parent whenever it changes, and you can then modify (append) to the child data, and when you click save, the data should be emitted into the parent (and is then subsequently passed into the child as a prop).
The problem is, as soon as you click save in the child component, the child is no longer able to watch for changes to the prop. It never sees the future changes from the parent. Vue devtools correctly sees the data though. What am i missing here?
The watch composable accepts either a 'ref', 'reactive' object, or a getter function.
But as stated in the documentation, it doesn't accept inner property of a reactive object:
Do note that you can't watch a property of a reactive object like this:
const obj = reactive({ count: 0 })
// this won't work because we are passing a number to watch()
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
So to listen to props.list changes, you need to use a getter function:
watch(() => props.list, () => {
// ...
})
See this working playground
Issue is not in a computed property, Issue is in your onAdd method. As this is an array, It should be update by reference not a value.
const onAdd = (val) => {
// data.value.list = val; ❌
val.forEach((d, index) => {
data.value.list[index] = d ✅
})
}
Working Vue SFC Playground
Try to set your watcher like:
watch(
() => props.list,
(newValue, oldValue) => {
console.log('updated!');
internalCopy.value = JSON.parse(JSON.stringify(props.list));
}, {
immediate: true,
deep: true,
}
It is also working well with deep watch on props object, like so:
watch(props, () => {
console.log('updated!');
internalCopy.value = props.list;
}, {
immediate: true,
deep: true,
})
Since the ref (reference) to the props object keeps unchanged.
This is a very interesting question to deeply understand the Vue reactivity. That's why I wanted to clarify it completely .
The previous answers are correct, but they don't explain the problem fully enough for me.
Since the props.list is a ref (reference) to the the array, when you define your watcher with it, then the watcher is bound to this ref (a Proxy object) and it will stay bounded to this ref even if you replace the props.list with a new ref to an new array. This is what you do with your add event.
To check it I have added the refBackup with the original ref to see it the watcher still updates, it we update the original list array through refBackup. Like this:
const refBackup = ref(props.list);
const addToBack = () => {
refBackup.value.push('addToBack');
}
Notice that updating the refBackup triggers the watcher(), even after the save was clicked and the props.list points now to another ref with a new Array.
Here is the link to my playground
In case when you define your watcher() with the function () => props.list,
this function always returns the actual ref to the last updated array. The watcher is bound to the function and not to the ref of props.list array.
My list of items is not being updated when I do a delete of a row even though the console.log(data) results in one less item after deleting the row, but the console.log('list of items after: ', listOfitems) gives me all full items even those that are deleted.
const [listOfitems, setListOfitems] = useState([]);
useEffect(() => {
getListOfitems();
}, [])
Please let me know what could be possibly wrong here:
editable={{
onRowDelete: (oldData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
console.log('list of items before: ', listOfitems)
console.log('oldData.item_key;: ', oldData.item_key)
const indexToDelete = oldData.item_key;
console.log(indexToDelete)
const data =
listOfitems.filter(
(item) => ((item.item_key !== oldData.item_key))
)
setListOfitems(data);
console.log(data)
console.log(setListOfitems(data))
deleteitem(oldData);
setListOfitems(data);
console.log('list of items after: ', listOfitems)
}
resolve();
}, 1000);
}),
}}
The second argument in the useEffect hook is an array of values that the hook depends on. In your example you passed an empty array, which is great for making the hook work like ComponentDidMount(). However, since your hook now depends on no values, it will never fire again. In the below example, passing listOfItems into that array will cause the hook to fire whenever that piece of state changes. Not seeing the rest of your code, but based on your description of the problem, this is my best guess as to a solution for you.
Hope this helps.
const [listOfitems, setListOfitems] = useState([]);
useEffect(() => {
console.log('useEffect')
getListOfitems().then((listOfitems) => {
setListOfitems(listOfitems);
});
getDocuments();
}, [listOfItems]);
Every time the component is rendered a new const state is assigned to useState hook. In you case every time your component is rerendred this line runs assigning a new value to the listOfitems.
const [listOfitems, setListOfitems] = useState([]);
This means that console logging out the state before the component rerenders would return the old state.
Console.log(data) results in the correct state because you simply filter and return a new array.
As dvfleet413 mentioned you do not include the listOfItems in the argument array of the useEffect meaning that your component only renders once.
The set* methods for state are asynchronous, so if you try to access listOfitems immediately after calling setListOfitems, you could be using the old value and not the newly updated one.
You could use a separate useEffect method that is dependent on the value being set in the first one (use the second param like [listOfitems]). This other method should get triggered after the update.
In short - the data IS being updated, but your console.log is displaying the old value before the update is complete.
edit: Code example:
useEffect(()=>{
getListOfitems()
// console.log here may NOT show the new listOfitems value because
// setListOfitems is asynchronous, and getListOfitems may also be
}, []) // only loads on the initial mount
I assume getListOfitems will retrieve the data from an external source initially, and calls setListOfitems inside it. The code in the post does not show what is happening.
A second useEffect could look like:
useEffect(()=>{
// do something with listOfitems
}, [listOfitems]) // gets called every time listOfitems is updated
I want someone to confirm my intuition on below problem.
My goal was to keep track (similar to here) of props.personIdentId and when it changed, to call setSuggestions([]).
Here is code:
function Identification(props) {
let [suggestions, setSuggestions] = useState([]);
let prevPersonIdentId = useRef(null)
let counter = useRef(0) // for logs
// for accessing prev props
useEffect(() => {
prevPersonIdentId.current = props.personIdentId;
console.log("Use effect", props.personIdentId, prevPersonIdentId.current, counter.current++)
});
// This props value has changed, act on it.
if (props.personIdentId != prevPersonIdentId.current) {
console.log("Props changed", props.personIdentId, prevPersonIdentId.current, counter.current++)
setSuggestions([])
}
But it was getting in an infinite loop as shown here:
My explanation why this happens is that: initially when it detects the prop has changed from null to 3, it calls setSuggestions which schedules a re-render, then next re-render comes, but previous scheduled useEffect didn't have time to run, hence the prevPersonIdentId.current didn't get updated, and it again enters and passes the if condition which checks if prop changed and so on. Hence infinite loop.
What confirms this intuition is that using this condition instead of old one, fixes the code:
if (props.personIdentId != prevPersonIdentId.current) {
prevPersonIdentId.current = props.personIdentId;
setSuggestions([])
}
Can someone confirm this or add more elaboration?
useEffect - is asynchronous function!
And you put yours condition in synchronous part of component. Off course synchronous part runs before asynchronous.
Move condition to useEffect
useEffect(() => {
if (personIdentId !== prevPersonIdentId.current) {
prevPersonIdentId.current = personIdentId;
console.log(
"Use effect",
personIdentId,
prevPersonIdentId.current,
counter.current++
);
setSuggestions([]);
}
});
It could be read as:
When mine component updates we check property personIdentId for changes and if yes we update ref to it's value and run some functions
It looks like this is what's happening:
On the first pass, props.personId is undefined and
prevPersonIdentId.current is null. Note that if (props.personIdentId !=
prevPersonIdentId.current) uses != so undefined is coerced to
null and you don't enter the if.
Another render occurs with the same conditions.
props.personId now changes, so you enter the if.
setSuggestions([]) is called, triggering a re-render.
loop forever
Your useEffect is never invoked, because you keep updating your state and triggering re-renders before it has a chance to run.
If you want to respond to changes in the prop, rather than attempting to roll your own change-checking, you should just use useEffect with the value you want to respond to in a dependency array:
useEffect(() => {
setSuggestions([])
}, [props.personId] );
I am using React Hooks to manage states within a component.
const addNode = () => {
let pform = pForm
let handles = [vForm, yForm, hForm]
let access_info = [virtualForm, management1Form, management2Form, consoleForm]
let newObj = {
...currentForm,
p: pform,
handles: handles,
access_info: access_info,
}
console.log('newObj', newObj)
setCurrentForm(
newRouterObj
)
console.log(currentForm)
let currArr = [...addedNodes]
currArr.push(currentForm)
setAddedNodes(currArr)
intializeForms()
}
The function above is an onClick that I use when I press an Add button. The forms (pForm, vForm, yForm, etc.) are all separate states. I gather them together and put them into a single object newObj and use setCurrentForm to update the currentForm state to newObj.
When I console.log the newObj, everything goes in fine. However, when I check the currentForm after the setCurrentForm, the fields (p, handles, and access_info) are empty.
I know that states in React can have a delay in updates so I might have to use useEffect. However, in my use case, which is to gather different states and put them in as a new field in the currentForm state seems useEffect is not the best way to solve it. Can anyone help please?
You are misunderstanding exactly how useState works. When you call the useState setter function, the state value isn't actually updated immediately, instead it will trigger the component to re-render with the updated value. Even though you call the setter half way through the function, the state value will remain the original value for the entire lifetime of that function call.
You could slightly tweak what you have to be
const addNode = () => {
...
let currArr = [...addedNodes]
// you know that currentForm is supposed to be newObj, so just push that
// see my explanation above to understand why it currentForm isn't what you expect
currArr.push(newObj)
...
}
It's an async action so values will not be assigned/updated instantly. You need to watch for the changes using useEffect hook to log new values and to do anything in case
useEffect(() => {
// Whenever `currentForm` will be updated, this callback will be invoked
console.log('updated currentForm values', currentForm);
},[currentForm]);
My application consists of a basic input where the user types a message. The message is then appended to the bottom of all of the other messages, much like a chat. When I add a new chat message to the array of messages I also want to scroll down to that message.
Each html element has a dynamically created ref based on its index in the loop which prints them out. The code that adds a new message attempts to scroll to the latest message after it has been added.
This code only works if it is placed within a setTimeout function. I cannot understand why this should be.
Code which creates the comments from their array
comments = this.state.item.notes.map((comment, i) => (
<div key={i} ref={i}>
<div className="comment-text">{comment.text}</div>
</div>
));
Button which adds a new comment
<input type="text" value={this.state.textInput} onChange={this.commentChange} />
<div className="submit-button" onClick={() => this.addComment()}>Submit</div>
Add Comment function
addComment = () => {
const value = this.state.textInput;
const comment = {
text: value,
ts: new Date(),
user: 'Test User',
};
const newComments = [...this.state.item.notes, comment];
const newState = { ...this.state.item, notes: newComments };
this.setState({ item: newState });
this.setState({ textInput: '' });
setTimeout(() => {
this.scrollToLatest();
}, 100);
}
scrollToLatest = () => {
const commentIndex = this.state.xrayModalData.notes.length - 1;
this.refs[commentIndex].scrollIntoView({ block: 'end', behavior: 'smooth' });
};
If I do not put the call to scrollToLatest() inside of a setTimeout, it does not work. It doesn't generate errors, it simply does nothing. My thought was that it was trying to run before the state was set fully, but I've tried adding a callback to the setState function to run it, and it also does not work.
Adding a new comment and ref will require another render in the component update lifecycle, and you're attempting to access the ref before it has been rendered (which the setTimeout resolved, kind of). You should endeavor to use the React component lifecycle methods. Try calling your scrollToLatest inside the lifecycle method componentDidUpdate, which is called after the render has been executed.
And while you're certainly correct that setting state is an asynchronous process, the updating lifecycle methods (for example, shouldComponentUpdate, render, and componentDidUpdate) are not initiated until after a state update, and your setState callback may be called before the component is actually updated by render. The React docs can provide some additional clarification on the component lifecycles.
Finally, so that your scroll method is not called on every update (just on the updates that matter), you can implement another lifecycle method, getSnapshotBeforeUpdate, which allows you to compare your previous state and current state, and pass a return value to componentDidUpdate.
getSnapshotBeforeUpdate(prevProps, prevState) {
// If relevant portion or prevState and state not equal return a
// value, else return null
}
componentDidUpdate(prevProps, prevState, snapshot) {
// Check value of snapshot. If null, do not call your scroll
// function
}