Structuring the state in a Vuex Store - javascript

I recently started learning Vuex and I would like to have some insight on how to properly structure the state of a Vuex/Flux-like stores
Lets take a look at the example below
ProductStore
state: {
name: 'some name',
price: 'some price',
variants: [],
selectedVariant: {},
}
mutations: {
[ADD_VARIANT] (state, newVariant) {
state.variants.push(newVariant)
}
[DELETE_VARIANT] (state, deletedId) {
state.variants = _.filter(state.variants, c => c.id == deleteID )
}
[EDIT_VARIANT] (state, editedComment) {
//...
}
[EDIT_SELECTED_VARIANT_TYPE] (state, variantType) {
state.selectedVariant.type = variantType
}
}
How do you structure states in instances like the one above when you have a parentComponent of sorts(the Product) and you have to manage childComponent states as well(the Variant).
In my specific instance, I have a ProductPage. In it, I have a VariantTable. Selecting an item in the VariantTable brings up a VariantModal that lets you edit variant attributes which should propagate to the parent table.

Normalize your store's state. If Product-Variant relationship is pure 1-n, the store's state can be:
state: {
name: 'some name',
price: 'some price',
variants: [
{ variantId: 'V1', ProductId: 'P1' },
...
],
selectedVariant: {},
products: [
{ productId: 'P1' },
...
]
}
Then with Vuex's action you can add an action to handle update both Variant and Product together:
..., // state goes above
mutations: {
...
[EDIT_PRODUCT] (args) => { ... }
},
actions: {
[EDIT_PRODUCT_VARIANT] ({ commit, state }, editedComment) {
// take extra data if need via state
commit([EDIT_VARIANT], editedComment);
commit([EDIT_PRODUCT], { productId: editedComment.ProductId })
}
}
The point is to avoid data duplication and nested data as much as possible, while allowing data to be updated fast and efficiently.
Read more about data normalization at normalizr

Related

watch changes of nested data in vuejs

I want to watch changes of families variable which contains nested objects
<component-test
v-for="family of familiesToDisplay"
// rest
/>
data: () => ({
families: [],
}),
computed: {
familiesToDisplay() {
return this.families.fillter(family => family.members > 4);
},
},
In some response I have seen some recomanding the use of watch but in my case I didn't knew how to implement it since I have never used before.
so the request is to get changes of nested objects in families (as objects I have person and work so if a name of a person has been changed or its work has been changed changes must be retreived here)
This code should work fine, however, it is necessary to correctly mutate objects inside an array. You need to use this.$set or replace the object with a new one to trigger vue re-rendering a list and recalculation of a list.
For example, you can change them like this, (full working example you can find here):
export default {
name: "HelloWorld",
components: {
Display,
},
data: () => ({
families: [
{ name: "foo-1", members: 1 },
{ name: "foo-5", members: 5 },
{ name: "bar-6", members: 6 },
{ name: "bar-3", members: 3 },
],
}),
methods: {
onClick() {
this.families.forEach((family) => {
this.$set(family, "members", this.getRandomNumber());
});
},
getRandomNumber() {
return Math.floor(Math.random() * 10) + 1;
},
},
};

Redux Toolkit caseReducers skip prepare callback in reducer function

I have a reducer for adding comment as follow
addComment: {
reducer: (state,action)=>{
const { newId, comment } = action.payload
console.log(newId)
// functions that create an new comment entry with the newID and update state
},
prepare: (input) =>{
return {
payload: {
...input,
newId: nanoid(),
}
}
}
If I call dispatch within my app as follow, the console.log shows the id generated by nanoid(). So far so good.
dispatch(addComment({comment: comment}))
However if I call the reducer within another reducer using the caseReducers, console.log returns undefined
commentSlice.caseReducers.addComment(state,{
payload: {
comment : comment
}
})
How do I get the prepare callback to run when using caseReducers.
Based on the docs it looks like if you are to invoke using the caseReducer way, you need to include the "type":
const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => state + action.payload,
},
})
// now available:
slice.actions.increment(2)
// also available:
slice.caseReducers.increment(0, { type: 'increment', payload: 5 })

How to add array to vue-kanban blocks

I'm implementing vue-kanban component in my web application. There I'd like to display some objects from my database but I need some help to add them to the kanban board.
This is my array with the projects:
props: {
projects: {
type: Array,
required: true,
}
},
And here I'd like to add them to the kanban board, it should be instead of blocks:
data() {
return {
stages: ['open', 'doing', 'close'],
blocks: [
{
id: 1,
status: 'open',
title: 'test',
},
],
};
}
I use that component: https://github.com/BrockReece/vue-kanban
See What's the correct way to pass props as initial data in Vue.js 2?
If the Kanban component is expecting an attribute like :blocks="[...]" and nothing is going to happen to the data can you not pass the projects array directly to it? e.g :blocks="projects"
If no and the data name blocks is a must and the data needs to be mutable then see below.
export default {
name: "YourComponent",
props: {
projects: {
type: Array,
required: true
}
},
data() {
return {
blocks: this.projects
}
}
}

Issues with Redux - Adding & Removing Items From State

I'm working on a shopping cart and I'm trying to wrap my head around two problems with my app:
Adding items to the store is overwriting previous items in the store:
Initial state:
const initialState = {
items: {},
showCart: false
};
Add to Cart Reducer:
Problem: This works for adding an item to the cart, but when I go to add another item in the cart, it overwrites the previous item. Why would that be / How do I preserve the items in the previous state?
let addToCartState = {...state,
items: {
[action.id]: {
id: action.id,
color: action.product_selection.color,
size: action.product_selection.size,
quantity: 1
}
},
showCart: true
}
return state.merge(addToCartState);
Remove All From Cart Reducer:
Problem: This seems to work, but I can't seem to grab data from the state map. I can't seem to call "state.cart.items" (see mapStateToProps) like I can on my other states.
let removeFromCartState = {...state,
items: {
...state.items
},
showCart: true
}
function mapStateToProps(state) {
console.log(state.cart);
console.log("🙃");
return { products: state.products, items: state.cart.items }
}
state.cart:
Map {size: 8, _root: ArrayMapNode, __ownerID: undefined, __hash: undefined, __altered: false}
size: 8
__altered: false
__hash: undefined
__ownerID: undefined
_root: ArrayMapNode
entries: Array(8)
0: Array(2)
0: "items"
1: Map
size: 0
...
^ No items now (size: 0, was 1 after the previous reducer); do I need to use something like fromJS to parse this now or should I not have to do that?
Edit - combineReducers:
import {combineReducers} from 'redux';
import app from './appReducer';
import products from './productsReducer';
import cart from './cartReducer';
import user from './userReducer';
export default combineReducers({
app: app,
products: products,
cart: cart,
user: user
});
The root of the problem is that you're treating Immutable.js objects like regular JavaScript objects instead of using the built-in Immutable.js features intended for the tasks you're performing.
Problem: This works for adding an item to the cart, but when I go to add another item in the cart, it overwrites the previous item. Why would that be / How do I preserve the items in the previous state?
Let's take a look at your code:
let addToCartState = { ...state,
items: { [action.id]: { /* ... */ } },
showCart: true
};
The spread operator (...) does a "shallow" merge. What your code is doing, essentially, is this:
let addToCartState = shallowCopy(state);
addToCartState.items = { [action.id]: { /* ... */ } };
addToCartState.showCart = true;
In other words, it "overwrites the previous item" because you're replacing the items property with a new object with only one item. One solution is to merge items yourself:
const addToCartState = { ...state,
items: { ...state.items,
[action.id]: { /* ... */ },
},
showCart: true,
};
...but since you're using Immutable.js, you shouldn't do that. You should use its built-in mergeDeep method:
function addToCart(prevState, action) {
const addToCartState = {
items: {
[action.id]: {
color: action.product_selection.color,
// ...
},
},
showCart: true,
};
return prevState.mergeDeep(addToCartState);
}
let state = Immutable.fromJS({ items: {} });
console.log('Original state:', state);
console.log('Add blue thing');
state = addToCart(state, {
id: '123',
product_selection: { color: 'blue' },
});
console.log('State is now:', state);
console.log('Add green thing');
state = addToCart(state, {
id: '456',
product_selection: { color: 'green' },
});
console.log('State is now:', state);
.as-console-wrapper{min-height:100%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>
Problem: This seems to work, but I can't seem to grab data from the state map. I can't seem to call "state.cart.items" (see mapStateToProps) like I can on my other states.
state is not a "plain" JavaScript object, it's an Immutable.Map. You can't access its values like ordinary object properties. One solution is convert it to a plain object using toJS, then retrieve its properties (and sub-properties) like usual. An alternative, which will be preferable if your state object is potentially large, is to retrieve the values using Immutable.js' get and getIn (for "deep" properties). With the latter you'll have to use toJS on the individual values if they're also Immutable objects. You can see both approaches below.
function mapStateToProps(state) {
const obj = state.toJS();
return { products: obj.products, items: obj.cart.items };
}
// or...
function mapStateToPropsAlt(state) {
return {
products: state.get('products').toJS(),
items: state.getIn(['cart', 'items']).toJS(),
};
}
const state = Immutable.fromJS({
products: [ '¯\\_(ツ)_/¯' ],
cart: {
items: {
'123': { id: '123', color: 'blue', /* ... */ },
},
},
});
console.log('mapStateToProps(state) =>', mapStateToProps(state));
console.log('mapStateToPropsAlt(state) =>', mapStateToPropsAlt(state));
.as-console-wrapper{min-height:100%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>

Push to vuex store array not working in VueJS

I'm using Vuex to show a list of users from 'store.js'. That js file has array like this.
var store = new Vuex.Store({
state: {
customers: [
{ id: '1', name: 'user 1',},
]
}
})
I want to insert a new set of values to the same array
{ id: '1', name: 'user 1',}
The above values are obtained from a URL (vue-resource). Below is the code to push the obtained data to the array. However, the data is not inserting
mounted: function() {
this.$http.get('http://localhost/facebook-login/api/get_customers.php')
.then(response => {
return response.data;
})
.then(data => {
store.state.customers.push(data) // not working!!
console.log(data) // prints { id: '2', name: 'User 2',}
store.state.customers.push({ id: '2', name: 'User 2',})
});
}
You are trying to modify the vuex state from the vue component, You can not do it. You can only modify vuex store from a mutation
You can define a mutation like following:
var store = new Vuex.Store({
state: {
customers: [
{ id: '1', name: 'user 1',},
]
},
mutations: {
addCustomer (state, customer) {
// mutate state
state.customers.push(customer)
}
}
})
Now you can commit this mutation from the vue instance, like following:
mounted: function() {
this.$http.get('http://localhost/facebook-login/api/get_customers.php')
.then(response => {
return response.data;
})
.then(data => {
store.commit('addCustomer', { id: '2', name: 'User 2'})
});
}

Categories