I'm using Vuex and I've created a module called claims that looks like this:
import to from 'await-to-js'
import { functions } from '#/main'
import Vue from 'vue'
const GENERATE_TX_SUCCESS = 'GENERATE_TX_SUCCESS'
const GENERATE_TX_ERROR = 'GENERATE_TX_ERROR'
export default {
state: [ ],
mutations: {
[GENERATE_TX_SUCCESS] (state, generateTxData) {
state.push({ transaction: { ...generateTxData } })
},
[GENERATE_TX_ERROR] (state, generateTxError) {
state.push({ transaction: { ...generateTxError } })
}
},
actions: {
async generateTx ({ commit }, data) {
const [generateTxError, generateTxData] = await to(functions.httpsCallable('generateTx')(data))
if (generateTxError) {
commit(GENERATE_TX_ERROR, generateTxError)
} else {
commit(GENERATE_TX_SUCCESS, generateTxData)
}
}
},
getters: { }
}
Then, in the .vue component I do have this watch:
watch: {
claims: {
handler (newTxData, oldTxData) {
console.log(newTxData)
}
}
}
The problem I'm facing here is that newTxData is the same as oldTxData.
From my understanding, since this is a push and it detects the change, it's not one of these caveats: https://v2.vuejs.org/v2/guide/list.html#Caveats
So basically the problem is this one:
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
https://v2.vuejs.org/v2/api/#vm-watch
Then my question is: how should I workaround this in the mutation?
Edit:
I tried also with Vue.set(state, state.length, generateTxData) but got the same behaviour.
Edit 2 - temporary solution - (bad performance-wise):
I'm adapting #matthew (thanks to #Jacob Goh) to my solution with vuexfire:
computed: {
...mapState({
claims: state => cloneDeep(state.claims)
})
},
This answer is based on this pretty smart answer by #matthew
You will need lodash cloneDeep function
Basically, create a computed value like this
computed: {
claimsForWatcher() {
return _.cloneDeep(this.claims);
}
}
What happens is, everytime a new value is pushed into claims, claimsForWatcher will become an entirely new object.
Therefore, when you watch claimsForWatcher, you won't have the problem where 'the old value will be the same as new value because they reference the same Object/Array' anymore.
watch: {
claimsForWatcher(oldValue, newValue) {
// now oldValue and newValue will be 2 entirely different objects
}
}
This works for ojects as well.
The performance hit is exaggerated for the real life use cases till you probably have hundreds (nested?) properties (objects) in your cloning object.
Either with clone or cloneDeep the clone takes 1/10 of a millisecond as reported with window.performance.now(). So measured under Node with process.hrtime() it could show even lower figures.
You can assign state to a new object:
mutations: {
[GENERATE_TX_SUCCESS] (state, generateTxData) {
state = [
...state,
{ transaction: { ...generateTxData } }
]
},
[GENERATE_TX_ERROR] (state, generateTxError) {
state = [
...state,
{ transaction: { ...generateTxError } }
]
}
}
Related
I've been breaking my head for a week or something with this !!
My redux state looks similar to this
{
data: {
chunk_1: {
deep: {
message: "Hi"
}
},
chunk_2: {
something: {
something_else: {...}
}
},
... + more
},
meta: {
session: {...},
loading: true (or false)
}
}
I have an array of keys like ["path", "to", "node"] and some data which the last node of my deeply nested state object should be replaced with, in my action.payload.
Clearly I can't use spread operator as shown in the docs (coz, my keys array is dynamic and can change both in values and in length).
I already tried using Immutable.js but in vain.. Here's my code
// Importing modules ------
import {fromJS} from "immutable";
// Initializing State ---------
const InitialState = fromJS({ // Immutable.Map() doesn't work either
data: { ... },
meta: {
session: {
user: {},
},
loading: false,
error: "",
},
});
// Redux Root Reducer ------------
function StoreReducer(state = InitialState, action) {
switch (action.type) {
case START_LOADING:
return state.setIn(["meta"], (x) => {
return { ...x, loading: true };
});
case ADD_DATA: {
const keys = action.payload.keys; // This is a valid array of keys
return state.updateIn(keys, () => action.payload); // setIn doesn't work either
}
}
Error I get..
Uncaught TypeError: state.setIn (or state.updateIn) is not a function
at StoreReducer (reducers.js:33:1)
at k (<anonymous>:2235:16)
at D (<anonymous>:2251:13)
at <anonymous>:2464:20
at Object.dispatch (redux.js:288:1)
at e (<anonymous>:2494:20)
at serializableStateInvariantMiddleware.ts:172:1
at index.js:20:1
at Object.dispatch (immutableStateInvariantMiddleware.ts:258:1)
at Object.dispatch (<anonymous>:3665:80)
What I want ?
The correct way to update my redux state (deeply nested object) with a array containing the keys.
Please note that you are using an incredibly outdated style of Redux. We are not recommending hand-written switch..case reducers or the immutable library since 2019. Instead, you should be using the official Redux Toolkit with createSlice, which allows you to just write mutating logic in your case reducers (and thus also just using any helper library if you want to use one).
Please read Why Redux Toolkit is how to use Redux today.
you could use something like that:
import { merge, set } from 'lodash';
export default createReducer(initialState, {
...
[updateSettingsByPath]: (state, action) => {
const {
payload: { path, value },
} = action;
const newState = merge({}, state);
set(newState, path, value);
return newState; },
...}
Have been following a few tutorials on youtube and have pretty much never seen anyone explicitly define an action that mutates the state they just throw in into the store. I have been doing the same and while it works a 100% it throws a warning on react native. Just wondering how you could define that something is an action and maybe if someone has a good way to separate the actions into a different file. Here is my store.
export function createCurrencyStore() {
return {
currencies: [
'AED',
'ARS',
'AUD',
],
selectedCurrencyFrom: 'USD',
selectedCurrencyTo: 'EUR',
loading: false,
error: null,
exchangeRate: null,
amount: 1,
fromFilterString: '',
fromFilteredCurrencies: [],
toFilterString: '',
toFilteredCurrencies: [],
setSelectedCurrencyFrom(currency) {
this.selectedCurrencyFrom = currency
},
setSelectedCurrencyTo(currency) {
this.selectedCurrencyTo = currency
},
async getExchangeRate() {
const conn = await fetch(
`https://api.exchangerate-api.com/v4/latest/${this.selectedCurrencyFrom}`
)
const res = await conn.json()
console.log(res)
this.exchangeRate = res.rates[this.selectedCurrencyTo]
},
setFromFilters(string) {
this.fromFilterString = string
if (this.fromFilterString !== '') {
this.fromFilteredCurrencies = this.currencies.filter((currency) =>
currency.toLowerCase().includes(string.toLowerCase())
)
} else {
this.fromFilteredCurrencies = []
}
},
setToFilters(string) {
this.toFilterString = string
if (this.toFilterString !== '') {
this.toFilteredCurrencies = this.currencies.filter((currency) =>
currency.toLowerCase().includes(string.toLowerCase())
)
} else {
this.toFilteredCurrencies = []
}
},
}
}
have pretty much never seen anyone explicitly define an action
Well, this is weird because it is a very common thing to only mutate state through actions to avoid unexpected mutations. In MobX6 actions are enforced by default, but you can disable warnings with configure method:
import { configure } from "mobx"
configure({
enforceActions: "never",
})
a good way to separate the actions into a different file
You don't really need to do it, unless it's a very specific case and you need to somehow reuse actions or something like that. Usually you keep actions and the state they modify together.
I am not quite sure what you are doing with result of createCurrencyStore, are you passing it to observable? Anyway, the best way to create stores in MobX6 is to use makeAutoObservable (or makeObservable if you need some fine tuning). So if you are not using classes then it will look like that:
import { makeAutoObservable } from "mobx"
function createDoubler(value) {
return makeAutoObservable({
value,
get double() {
return this.value * 2
},
increment() {
this.value++
}
})
}
That way every getter will become computed, every method will become action and all other values will be observables basically.
More info in the docs: https://mobx.js.org/observable-state.html
UPDATE:
Since your getExchangeRate function is async then you need to use runInAction inside, or handle result in separate action, or use some other way of handling async actions:
import { runInAction} from "mobx"
async getExchangeRate() {
const conn = await fetch(
`https://api.exchangerate-api.com/v4/latest/${this.selectedCurrencyFrom}`
)
const res = await conn.json()
runInAction(() => {
this.exchangeRate = res.rates[this.selectedCurrencyTo]
})
// or do it in separate function
this.handleExchangeRate(res.rates[this.selectedCurrencyTo])
},
More about async actions: https://mobx.js.org/actions.html#asynchronous-actions
What I want?
I want a mobx-react component to be binded on boxed observable primitive value from state. So I expect component to rerender if value changes.
Using lodash,
type BusinessData = { root: { path: { value: string } };
#observable state = buildInitialState();
const buildInitialState = () : BusinessData => {root: {}};
<BindedComponent value = {_.get(state, 'root.path.value')} />
const fetchState = () : BusinessData => { root: { path: { value: 'potato' } } };
What is the problem?
At the moment of first application rendering root.path is undefined. It will be fetched later on some stage of some internal component lifecycle or on user action. Furthermore, it might even not be fetched from server. Such path might not exist in data until user edits some input and this value will be set.
Supposable solution - initialize whole state explicitly:
const buildInitialState = () : BusinessData => { root: { path: { value: undefined } } };
Then BindedComponent can bind on boxed undefined and observe changes. This is bad, because when state is deep nested, I have to write such a boilerplate. And also in my case shape of business data can have a lof of implementations. So I have to initialize explicitly every one of them in all my projects.
Any ideas on how I can solve this without boilerplate?
Try to keep your state structure simple:
#observable state = { root: { path: { value: null } }
You can then create a simple update function:
async setStateValue(value) {
try {
this.state.root.path.value = await value
} catch (e) {
console.log(e)
}
}
Calling this at the rendering stage, will automatically update your component once the promise has been completed:
async updateFromComponent() {
await setStateValue('potato')
}
const {value} = prop.store.state.root.path
render (){
return (
<BindedComponent value = {value} />
)
}
I have a react app with some redux state that looks like:
{
shape1: {
constraints: {
constraint1: {
key: value
},
constraint2: {
key: value
}
}
},
shape2: {
constraints: {
constraint1: {
key: value
},
constraint2: {
key: value
}
}
}
}
I dispatch an action and want to delete one of the constraint objects, ie. constraint1 for shape1. Here is what my reducer looks like for this action, say I'm trying to delete constraint1 from shape1:
case DELETE_CONSTRAINT:
shape = action.payload; // ie. shape1, the parent of the constraint I
// am trying to delete
let {
[shape]: {'constraints':
{'constraint1': deletedItem}
}, ...newState
} = state;
return newState;
This removes the entire shape1 object from the state instead of just the individual constraint1 object. Where I am going wrong/ what is the best approach for doing this? I'd prefer to use object rest in order to be consistent with the rest of my code.
Thanks.
When using the rest syntax in destructuring to get a slice of the object, you'll get everything else on the same "level".
let {
[shape]: {'constraints':
{'constraint1': deletedItem}
}, ...newState
} = state;
In this case newState takes everything else is everything but [shape].
Since your state has multiple nesting levels, you'll have to extract the new constraints using destructuring and rest syntax, and then create a new state.
const state = {
shape1: {
constraints: {
constraint1: {
key: 'value'
},
constraint2: {
key: 'value'
}
}
},
shape2: {
constraints: {
constraint1: {
key: 'value'
},
constraint2: {
key: 'value'
}
}
}
};
const shape = 'shape1';
const constraint = 'constraint1';
// extract constraints
const {
[shape]: {
constraints: {
[constraint]: remove,
...constraints
}
}
} = state;
// create the next state
const newState = {
...state,
[shape]: {
...state[shape], // if shape contains only constraints, you keep skip this
constraints
}
}
console.log(newState);
In short, no not with an object - not with the spread operator.
You can do it other ways without mutating your state though, such as a filter, for example:
return state.filter((element, key) => key !== action.payload);
Consistency sidenote
As a sidenote - there is a vast difference between consistency in approach and style vs consistency of actual code. Don't feel the need to shoe horn something for consistency if it makes more logical sense to do it a different way. If it truely breaks the consistency of the application that other developers are working on, document why it's different.
I have a state Object that looks like this:
//State Object
playlistDict: {
someId1: {
active: false,
id: 'someId',
name: 'someName',
pages: [ 'pageId1', 'pageId2', 'pageId3', etc ] // I want to remove an element from this array
},
someId2: {
active: false,
id: 'someId2',
name: 'someName2',
pages: [ 'pageId11', 'pageId22', 'pageId33', etc ] // I want to remove an element from this array
}
}
I'm trying to write a reducer that removes a page element given the index to remove without using immutability helper.
This is what I'm trying to do but my syntax is off and I'm not sure what's the correct way to write the reducer:
export function _removePageFromPlaylist( playlistId, pageIndex ) {
return { type: types.REMOVE_PAGE_FROM_PLAYLIST, playlistId, pageIndex };
}
case types.REMOVE_PAGE_FROM_PLAYLIST: {
let playlistCopy = Object.assign( {}, state.playlistDict[ action.playlistId ]);
playlistCopy.pages = Object.assign( {}, state.playlistDict[ action.playlistId ].pages );
playlistCopy.pages.splice( action.pageIndex, 1 );
return Object.assign({}, state, {
playlistDict: { ...state.playlistDict, playlistDict[ action.playlistId ]: playlistCopy } // doesn't like this syntax in particular
});
}
EDIT: In regards to people thinking it's the same as my other question, I'm asking this question because I'm trying to figure out if my reducer USING immutability helper is messing up my Firebase database, so I'm trying to figure out how to write the reducer WITHOUT using immutability helper so i can help eliminate what my bug is.
Use spread operator, and write it like this:
case types.REMOVE_PAGE_FROM_PLAYLIST: {
let playlistCopy = [...(state.playlistDict[ action.playlistId ].pages)];
playlistCopy.splice( action.pageIndex, 1 );
return {
...state,
playlistDict: {
...state.playlistDict,
[action.playlistId]: {
...state.playlistDict[action.playlistId],
pages: playlistCopy
}
}
}
}
Using Object Spread Operator:
case types.REMOVE_PAGE_FROM_PLAYLIST: {
return {
...state,
playlistDict: {
...state.playlistDict,
[action.playlistId]: {
...state.playlistDict[action.playlistId],
pages: [...state.playlistDict[action.playlistId].pages].splice(action.pageIndex, 1)
}
}
}
}