I have a Vuex store with two states.
notes (notes that have been synced with the server/DB)
localNotes (which has not been synced with the server/DB, when synced with the server/DB they will be moved to 'notes' state)
I am using these states to show the notes in a list with a getter. This getter merges the two objects and return the merge objects
The problem I have now is that if I add a new note to one of the states it will not be shown in the note list because the getter doesn't pick the 'change' up. I think this happens because I return a variable instead of a function.
This is my getter:
const getters = {
notesObject: (state, getters, rootState, rootGetters) => {
let result = {};
let mergedObject = {...state.notes, ...state.localNotes};
Object.keys(mergedObject).forEach(key => {
const item = mergedObject[key];
if (rootGetters['tags/activeKey'] === null) {
result[key] = item
} else if (item.tag_id === rootGetters['tags/activeKey']) {
result[key] = item
}
});
return result;
},
};
Example object:
example: {
5: {
title: 'Testing title',
text: 'text'
},
6: {
title: 'Testing title',
text: 'text'
}
}
I hope someone can help me out to find the best solution for this. I was thinking about using a watcher, but I know these need to be avoided.
A solution was to let the watcher merge the two states into a new state.
Then the getter doesn't have to merge the two objects
Vue's reactivity system doesn't detect adding new properties. Try using Vue.set(object, key, value) when adding new notes.
In your mutation function replace state.localObject[payload.id] = payload; with Vue.set(state.localObject, payload.id, payload); The getter should then work properly.
Related
I have a pimRegistration state initialization as shown in the chrome redux-devtools screen capture below. The nesting being referenced is pimRegistration (state.domain.patient):
I updated the patient.name object with the following spread operator statement:
store.update((state) => ({
...state,
...patientPath,
...{ [property]: value },
}));
...where property is the "name" property of the patient object with value. After the update, the following screenshot shows the new state:
Note that the original patient object (purple in the screenshot) is updated with the name object, duplicated and placed at the root of the state (yellow in screenshot).
I would like to overwrite the properties of the pimRegistration(state).domain.patient object, not to create a new patient object.
The state update is called as shown below.
store.update((state) => ({
...state,
...patientPath, // state.domain.patient
...{ [property]: value },
}));
I have tried my different combinations without achieving the desired result.
The complete update function is shown below.
update(property: string, path: string, value: any) {
const paths: string[] = path.split(".");
const pathReducer = (state: IRegistrationState, path_: string) => {
if (paths.length <= 0) {
return state.domain;
}
return state[path_];
};
const domainPath = state.domain;
let patientPath, nokPath, referrerPath;
if (path.includes("patient")) {
patientPath = paths.reduce(pathReducer, state);
}
if (path.includes("nok")) {
nokPath = paths.reduce(pathReducer, state);
}
if (path.includes("referrer")) {
referrerPath = paths.reduce(pathReducer, state);
}
store.update((state) => ({
...state,
...patientPath,
...{ [property]: value },
}));
}
The function above is invoked with the following statement in Angular 2.
if (this.path.includes("patient")) {
this._repo.update("name", "domain.patient", this.name);
}
Thanks
Deep updates to a store can be tricky. In your function you seem to be spreading the updates at the root rather than at the level you want the update at. This answer here outlines the usual practice to update the state. In short, something like
const newState = {
...state,
domain: {
...state.domain,
patient: {
...state.domain.patient,
[property]: value
}
}
}
Dynamically passing a path and updating this state can be… cumbersome. There are libraries that can help you do it such as immer, but you can possibly hack your way around with normal JS/TS.
Lets start with explaining the structure. I have the page dedicated to a specific company and a component Classification.vue on this page which displays categories of labels and labels itself which are assigned to the current company. First of all I get all possible categories with axios get request, then I get all labels, which are assigned to the current company, and after all I map labels to respective categories. Here is the Classification.vue:
import DoughnutChart from "#comp/Charts/DoughnutChart";
import ModalDialog from '#comp/ModalDialog/ModalDialog';
const EditForm = () => import('./EditForm');
export default {
components: {
DoughnutChart, ModalDialog, EditForm
},
props: ['companyData'],
async created() {
const companyLabels = await this.$axios.get('/companies/' + this.companyData.id + '/labels');
const allLabelsCategories = await this.$axios.get('/labels/categories');
allLabelsCategories.data.map(cat => {
this.$set(this.labelsCategories, cat.labelCategoryId, {...cat});
this.$set(this.labelsCategories[cat.labelCategoryId], 'chosenLabels', []);
});
companyLabels.data.map(l => {
this.labelsCategories[l.label.labelCategory.labelCategoryId].chosenLabels.push({...l.label, percentage: l.percentage})
});
},
computed: {
portfolioChartData() {
let portfolios = [];
// 35 id stands for 'Portfolio' labels category
if (this.labelsCategories[35] !== undefined && this.labelsCategories[35].chosenLabels !== undefined) {
this.labelsCategories[35].chosenLabels.map(label => {
portfolios.push({label: label.name, value: label.percentage});
});
}
return portfolios;
},
portfolioLabels() {
let portfolios = [];
// 35 id stands for Portfolio labels category
if (this.labelsCategories[35] !== undefined && this.labelsCategories[35].chosenLabels !== undefined) {
return this.labelsCategories[35].chosenLabels;
}
return portfolios;
}
},
data() {
return {
labelsCategories: {}
}
}
}
So far so good, I get the object labelsCategories where keys are ids of categories and values are categories objects which now also have chosenLabels key, which we set up in created(). And as you can see I use computed properties, they are necessary for a chart of 'Portfolio' category. And I used $set method in created() exactly for the purpose of triggering reactivity of labelsCategories object so computed properties can respectively react to this.
Now I have a new component inside Classification.vue - EditForm.vue, which is dynamically imported. In this component I do pretty much the same thing, but now I need to get every possible label for every category, not just assigned. So I pass there prop like this:
<modal-dialog :is-visible="isFormActive" #hideModal="isFormActive = false">
<EditForm v-if="isFormActive" ref="editForm" :labels-categories-prop="{...labelsCategories}" />
</modal-dialog>
And EditForm component looks like this:
export default {
name: "EditForm",
props: {
labelsCategoriesProp: {
type: Object,
required: true,
default: () => ({})
}
},
created() {
this.labelsCategories = Object.assign({}, this.labelsCategoriesProp);
},
async mounted() {
let labels = await this.$axios.get('/labels/list');
labels.data.map(label => {
if (this.labelsCategories[label.labelCategoryId].labels === undefined) {
this.$set(this.labelsCategories[label.labelCategoryId], 'labels', []);
}
this.labelsCategories[label.labelCategoryId].labels.push({...label});
});
},
data() {
return {
labelsCategories: {}
}
}
}
And now the problem. Whenever I open modal window with the EditFrom component my computed properties from Calssification.vue are triggered and chart is animating and changing the data. Why? Quite a good question, after digging a bit I noticed, that in EditForm component I also use $set, and if I will add with $set some dummy value, for example:
this.$set(this.labelsCategories[label.labelCategoryId], 'chosenLabels', ['dummy']);
it will overwrite the labelsCategories value in the parent component (Classification.vue)
How is it even possible? As you can see I tried to pass prop as {...labelsCategories} and even did this.labelsCategorie = Object.assign({}, this.labelsCategoriesProp); but my parent object is still affected by changes in child. I compared prop and labelsCategories objects in the EditForm component by === and by 'Object.is()' and they are not the same, so I am completely confused. Any help is highly appreciated.
Btw, I can solve this issue by passing prop as :labels-categories-prop="JSON.parse(JSON.stringify(labelsCategories))" but it seems like a hack to me.
Okay, I was digging deeper in this issue and learned, that neither {...labelsCategories} nor Object.assign({}, this.labelsCategoriesProp) don't create a deep copy of an object only the shallow one. So, I suppose that was the cause of the problem. In this article I learned about shallow and deep copies of objects: https://medium.com/javascript-in-plain-english/how-to-deep-copy-objects-and-arrays-in-javascript-7c911359b089
So, I can leave my hacky way using JSON.parse(JSON.stringify(labelsCategories)) or I can use a library such as lodash:
_.cloneDeep(labelsCategories)
But according to the article I also can create a custom method. And this one option is quite suitable for me. I already had a vue mixin for processing objects, so I just added deepCopy() function there:
deepCopy(obj) {
let outObject, value, key;
if (typeof obj !== "object" || obj === null) {
return obj; // Return the value if obj is not an object
}
// Create an array or object to hold the values
outObject = Array.isArray(obj) ? [] : {};
for (key in obj) {
value = obj[key];
// Recursively (deep) copy for nested objects, including arrays
outObject[key] = this.deepCopy(value);
}
return outObject;
},
I've been thinking about what would be the best way among these options to update a nested property using React setState() method. I'm also opened to more efficient methods considering performance and avoiding possible conflicts with other possible concurrent state changes.
Note: I'm using a class component that extends React.Component. If you're using React.PureComponent you must be extra careful when updating nested properties because that might not trigger a re-render if you don't change any top-level property of your state. Here's a sandbox illustrating this issue:
CodeSandbox - Component vs PureComponent and nested state changes
Back to this question - My concern here is about performance and possible conflicts between other concurrent setState() calls when updating a nested property on state:
Example:
Let's say I'm building a form component and I will initialize my form state with the following object:
this.state = {
isSubmitting: false,
inputs: {
username: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
},
email: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
}
}
}
From my research, by using setState(), React will shallow merge the object that we pass to it, which means that it's only going to check the top level properties, which in this example are isSubmitting and inputs.
So we can either pass it a full newState object containing those two top-level properties (isSubmitting and inputs), or we can pass one of those properties and that will be shallow merged into the previous state.
QUESTION 1
Do you agree that it is best practice to pass only the state top-level property that we are updating? For example, if we are not updating the isSubmitting property, we should avoid passing it to setState() in other to avoid possible conflicts/overwrites with other concurrent calls to setState() that might have been queued together with this one? Is this correct?
In this example, we would pass an object with only the inputs property. That would avoid conflict/overwrite with another setState() that might be trying to update the isSubmitting property.
QUESTION 2
What is the best way, performance-wise, to copy the current state to change its nested properties?
In this case, imagine that I want to set state.inputs.username.touched = true.
Even though you could do this:
this.setState( (state) => {
state.inputs.username.touched = true;
return state;
});
You shouldn't. Because, from React Docs, we have that:
state is a reference to the component state at the time the change is
being applied. It should not be directly mutated. Instead, changes
should be represented by building a new object based on the input from
state and props.
So, from the excerpt above we can infer that we should build a new object from the current state object, in order to change it and manipulate it as we want and pass it to setState() to update the state.
And since we are dealing with nested objects, we need a way to deep copy the object, and assuming you don't want to use any 3rd party libraries (lodash) to do so, what I've come up with was:
this.setState( (state) => {
let newState = JSON.parse(JSON.stringify(state));
newState.inputs.username.touched = true;
return ({
inputs: newState.inputs
});
});
Note that when your state has nested object you also shouldn't use let newState = Object.assign({},state). Because that would shallow copy the state nested object reference and thus you would still be mutating state directly, since newState.inputs === state.inputs === this.state.inputs would be true. All of them would point to the same object inputs.
But since JSON.parse(JSON.stringify(obj)) has its performance limitations and also there are some data types, or circular data, that might not be JSON-friendly, what other approach would you recommend to deep copy the nested object in order to update it?
The other solution I've come up with is the following:
this.setState( (state) => {
let usernameInput = {};
usernameInput['username'] = Object.assign({},state.inputs.username);
usernameInput.username.touched = true;
let newInputs = Object.assign({},state.inputs,usernameInput);
return({
inputs: newInputs
});
};
What I did in this second alternative was to create an new object from the innermost object that I'm going to update (which in this case is the username object). And I have to get those values inside the key username, and that's why I'm using usernameInput['username'] because later I will merge it into a newInputs object. Everything is done using Object.assign().
This second option has gotten better performance results. At least 50% better.
Any other ideas on this subject? Sorry for the long question but I think it illustrates the problem well.
EDIT: Solution I've adopted from answers below:
My TextInput component onChange event listener (I'm serving it through React Context):
onChange={this.context.onChange(this.props.name)}
My onChange function inside my Form Component
onChange(inputName) {
return(
(event) => {
event.preventDefault();
const newValue = event.target.value;
this.setState( (prevState) => {
return({
inputs: {
...prevState.inputs,
[inputName]: {
...prevState.inputs[inputName],
value: newValue
}
}
});
});
}
);
}
I can think of a few other ways to achieve it.
Deconstructing every nested element and only overriding the right one :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
username: {
...prevState.inputs.username,
touched: true
}
}
}))
Using the deconstructing operator to copy your inputs :
this.setState(prevState => {
const inputs = {...prevState.inputs};
inputs.username.touched = true;
return { inputs }
})
EDIT
First solution using computed properties :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
[field]: {
...prevState.inputs.[field],
[action]: value
}
}
}))
You can try with nested Object.Assign:
const newState = Object.assign({}, state, {
inputs: Object.assign({}, state.inputs, {
username: Object.assign({}, state.inputs.username, { touched: true }),
}),
});
};
You can also use spread operator:
{
...state,
inputs: {
...state.inputs,
username: {
...state.inputs.username,
touched: true
}
}
This is proper way to update nested property and keep state immutable.
I made a util function that updates nested states with dynamic keys.
function _recUpdateState(state, selector, newval) {
if (selector.length > 1) {
let field = selector.shift();
let subObject = {};
try {
//Select the subobject if it exists
subObject = { ..._recUpdateState(state[field], selector, newval) };
} catch {
//Create the subobject if it doesn't exist
subObject = {
..._recUpdateState(state, selector, newval)
};
}
return { ...state, [field]: subObject };
} else {
let updatedState = {};
updatedState[selector.shift()] = newval;
return { ...state, ...updatedState };
}
}
function updateState(state, selector, newval, autoAssign = true) {
let newState = _recUpdateState(state, selector, newval);
if (autoAssign) return Object.assign(state, newState);
return newState;
}
// Example
let initState = {
sub1: {
val1: "val1",
val2: "val2",
sub2: {
other: "other value",
testVal: null
}
}
}
console.log(initState)
updateState(initState, ["sub1", "sub2", "testVal"], "UPDATED_VALUE")
console.log(initState)
You pass a state along with a list of key selectors and the new value.
You can also set the autoAssign value to false to return an object that is a copy of the old state but with the new updated field - otherwise autoAssign = true with update the previous state.
Lastly, if the sequence of selectors don't appear in the object, an object and all nested objects with those keys will be created.
Use the spread operator
let {foo} = this.state;
foo = {
...foo,
bar: baz
}
this.setState({
foo
})
I have a todo list that holds a delete button in a grandchild, that when clicked fires an event in the parent - I am wanting this event to delete the array entry corresponding to the grandchild clicked.
Parent (contains the array and my attempt at the function)
const tasks = [
{ name: 'task1', isComplete: false },
{ name: 'task2', isComplete: true },
{ name: 'task3', isComplete: false },
]
// taskToDelete is the name of the task - doesn't contain an object
deleteTask(taskToDelete) {
this.state.tasks.remove(task => task.name === taskToDelete);
this.setState({ tasks: this.state.tasks });
}
Any help would be appreciated
Two issues there:
You're seeming to try to direct modify this.state.tasks. It's important not to do that, never directly modify this.state or any object on it. See "Do Not Modify State Directly" in the React documentation for state.
You're passing an object to setState that is derived from the current state. It's important never to do that, too. :-) Instead, pass setState a function and use the state object it passes you when calling that function. From "State Updates May Be Asynchronous" in the documentation:
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state... [Instead]...use a second form of setState() that accepts a function rather than an object.
(my emphasis)
I figure your remove on an array was intended to be hypothetical, but for the avoidance of doubt, arrays don't have a remove method. In this case, the best thing to do, since we need a new array, is to use filter to remove all entries that shouldn't still be there.
So:
deleteTask(taskToDelete) {
this.setState(prevState => {
const tasks = prevState.tasks.filter(task => task.name !== taskToDelete);
return { tasks };
});
}
You could simply filter the array :
this.setState(prevState => ({
tasks: prevState.tasks.filter(task => task.name !== 'taskToDelete')
}));
Also when updating based on this.state, its better to use the function form because setState is async.
You can use filter to remove one object from an array following the immutable pattern (filter will create a new array) :
deleteTask(taskToDelete) {
const newTaskArray = this.state.tasks.filter(task => task.name !== taskToDelete);
this.setState({ tasks: newTaskArray });
}
Edit : codepend of the solution : https://codepen.io/Dyo/pen/ZvPoYP
You can implement deleteTask method as below:
deleteTask(taskToDelete) {
this.setState((prevState, props) => {
const tasks = [...prevState.tasks];
const indexOfTaskToDelete = tasks.findIndex(
task => task.name === taskToDelete
);
tasks.splice(indexOfTaskToDelete, 1);
return { tasks };
});
}
A. Find the index of taskToDelete.
B. Then use splice method to delete the item from the collection
C. Then call setState to update the state with tasks.
You can use higher order function Array#filter to delete the task.
let updatedTasks = this.state.tasks.filter(task => task.name !== taskToDelete);
this.setState({ tasks: updatedTasks });
I have followed below steps to delete a particular selected Object from the state array:
Here I am using a list of checkBoxes, when I am selecting a checkBox it will add it in the state array and when it gets de-selected then it will get deleted from the array.
if (checked) {
var tempObject = { checkboxValue: data, label: title }
this.state.checkBoxState.push(resTemp);
} else {
var element = data; //data is coming from different method.
for (let index = 0; index < this.state.checkBoxState.length; index++) {
if (element === this.state.checkBoxState[index].checkboxValue) {
this.state.checkBoxState.splice(index, 1);
}
}
}
I got stuck for this question and I am sharing my solution. Hope it will help you.
I'm modifying the value of an existing property on an object that is in an array of objects in my Vuex.store. When I update the store, it is not triggering a re-render of my computed property that is accessing the store. If I reset the stored value to an empty array, and then set it again to my new array, it'll trigger the change. But simply updating the property of the array of objects does not trigger a change.
I have tried using Vue.set() like the docs talk about, and that updates the store, but still does not trigger a re-render of the computed property. What am I missing? Using Vue 2.2.4 and Vuex 2.2.0.
//DEBUG: An example of the updated post I'm adding
let myNewScheduledPost = {
id: 1,
name: 'James'
};
this.$store.dispatch('addScheduledPost', post);
//DEBUG: My store
const options = {
state: {
scheduledPosts: [
{ id: 1, name: 'Jimmy'}
],
},
mutations: {
scheduledPosts: (state, scheduledPosts) => {
//This triggers the reactivity/change so my computed property re-renders
//But of course seems the wrong way to do it.
state.scheduledPosts = [];
state.scheduledPosts = scheduledPosts;
//Neither of these two lines triggers my computed property to re-render, even though there is a change in scheduledPosts
state.scheduledPosts = scheduledPosts;
Vue.set(state, 'scheduledPosts', scheduledPosts);
},
},
actions: {
addScheduledPost({ commit, getters }, newScheduledPost) {
let scheduledPosts = getters.scheduledPosts;
const idx = scheduledPosts.findIndex(existingScheduledPost => existingScheduledPost.id === newScheduledPost.id);
//If the post is already in our list, update that post
if (idx > -1) {
scheduledPosts[idx] = newScheduledPost;
} else {
//Otherwise, create a new one
scheduledPosts.push(newScheduledPost);
}
commit('scheduledPosts', scheduledPosts);
//DEBUG: This DOES have the correct updated change - but my component does not see the change/reactivity.
console.log(getters.scheduledPosts);
}
},
getters: {
scheduledPosts: (state) => {
return state.scheduledPosts;
}
}
};
//DEBUG: Inside of my component
computed: {
mySortedPosts()
{
console.log('im being re-rendered!');
return this.$store.getters.scheduledPosts.sort(function() {
//my sorted function
});
}
}
Your problem is if you are wanting to access a portion of the state you don't use a getter https://vuex.vuejs.org/en/state.html.
computed: {
mySortedPosts(){
return this.$store.state.scheduledPosts
}
}
Getters are for computed properties in the store https://vuex.vuejs.org/en/getters.html. So in your case you might create a getter to sort your scheduled posts then name it sortedScheduledPosts and then you can add it to your components computed properties like you are now.
The key thing is your getter needs to have a different name then your state property just like you would in a component.