Vue 3 - Watching prop stops working after emitting to parent - javascript

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.

Related

React/Next-js : Getting TypeError: Cannot read properties of undefined (reading 'id'), but the object is clearly not empty?

I am having an issue with a Next-js React checkbox list snippet after extracting it into the sandbox.
whenever I clicked the checkbox, I get the error:
TypeError: Cannot read properties of undefined (reading 'id')
which originated from line 264:
setCheckedThread(prev => new Set(prev.add(pageData.currentThreads[index].id)));
but at the top of the index.js I have defined the static JSON
and in useEffect() I update the pageData state with:
setPageData({
currentPage: threadsDataJSON.threads.current_page,
currentThreads: threadsDataJSON.threads.data,
totalPages: totalPages,
totalThreads: threadsDataJSON.threads.total,
});
so why when I clicked the checkbox it throws the error?
my sandbox link: https://codesandbox.io/s/infallible-goldberg-vfu0ve?file=/pages/index.js
It looks like your useEffect on line 280 only triggers once you've checked a box (for some reason), so until you trigger that useEffect, pageData.currentThreads remains empty, which is where the error you're running into comes from.
I'd suggest moving all the state initialization from the useEffect into the useState call itself. E.g.
// Bad
const [something, setSomething] = useState(/* fake initial state */);
useEffect(() => {
setSomething(/* real initial state */)
}, []);
// Good
const [something, setSomething] = useState(/* real initial state */);
Here's a fork of your sandbox with this fix.
This is occurring because in Home you've created the handleOnChange function which is passed to the List component that is then passed to the memoized Item component. The Item component is kept the same across renders (and not rerendered) if the below function that you've written returns true:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked
);
}
This means that the Item component holds the first initial version of handleOnChange function that was created when Home first rendered. This version of hanldeOnChange only knows about the initial state of pageData as it has a closure over the initial pageData state, which is not the most up-to-date state value. You can either not memoize your Item component, or you can change your itemPropsAreEqual so that Item is rerendered when your props.handleOnChange changes:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked &&
prevItem.handleOnChange === nextItem.handleOnChange // also rerender if `handleOnChange` changes.
);
}
At this point you're checking every prop passed to Item in the comparison function, so you don't need it anymore and can just use React.memo(Item). However, either changing itemPropsAreEqual alone or removing itemPropsAreEqual from the React.memo() call now defeats the purpose of memoizing your Item component as handleOnChange gets recreated every time Home rerenders (ie: gets called). This means the above check with the new comparison function will always return false, causing Item to rerender each time the parent Home component rerenders. To manage that, you can memoize handleOnChange in the Home component by wrapping it in a useCallback() hook, and passing through the dependencies that it uses:
const handleOnChange = useCallback(
(iindex, id) => {
... your code in handleOnChange function ...
}
, [checkedState, pageData]); // dependencies for when a "new" version of `handleOnChange` should be created
This way, a new handleOnChange reference is only created when needed, causing your Item component to rerender to use the new up-to-date handleOnChange function. There is also the useEvent() hook which is an experimental API feature that you could look at using instead of useCallback() (that way Item doesn't need to rerender to deal with handleOnChange), but that isn't available yet as of writing this (you could use it as a custom hook for the time being though by creating a shim or using alternative solutions).
See working example here.

Vue 3 Typescript watcher not responding after passing prop through two components

I've got three components in my vue 3 app (using typescript and the composition api)
I'm reading a string value in the first component passing it throught a modal (2nd component) to a 3rd one.
Now the property gets to the 2nd component without an issue and I can read it there use it in a ref or whatever.
When passing the prop further to the 3rd component the watcher for that prop is not reacting to changes.
When I print the prop via another watcher I can see that its value is changing as I intended but the watcher does not react at all
Watcher code looks like this and I'm just passing props through the other components normally but this watcher is not reacting to changes.
const { myprop } = toRefs(props);
watch(myprop, (val: string) => {
output.value = val;
});
Thanks in advance for your time and help :)
Try to refactor your watcher like the follow example.
watch(() => props.myprop, (val: string) => {
output.value = val;
}, { immediate: true });
I think your watcher doesnt recognize the changes because myprops hold a reference from the parent component.
Im not quit sure, i need more context to understand.

React Hooks: Adding new fields to an Object state does not get reflected immediately

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

Function only updates states when it's invoked twice

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.

React: scrollIntoView only works inside of setTimeout

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
}

Categories

Resources