State Variable triggers mutation error - javascript

I have the following pseudo-code in my store module
const state = {
users: []
}
const actions = {
addUsers: async ({commit, state}, payload) => {
let users = state.users // <-- problem
// fetching new users
for(let i of newUsersThatGotFetched) {
users.push('user1') // <-- really slow
}
commit('setUsers',users)
}
}
const mutations = {
setUsers: (state, { users }) => {
Vue.set(state, 'users', users)
}
}
Now - when I run this code, I get the following error Error: [vuex] Do not mutate vuex store state outside mutation handlers.
When I put strict mode to false - the error is gone - but the process-time is really, really slow - as if the errors still happen but without getting displayed.
The problem seems to be where I commented // <-- problem, because after I change that line to
let users = []
everything runs flawlessly, but I can't have that because I need the data of state.users

The problem is: users.push('user1'), this is the line that mutates the state.
Remove anything that mutates the state (writes or changes it) from actions and move that into a mutation.
addUsers: async ({ commit }, payload) => {
// fetching new users
commit('setUsers', newUsersThatGotFetched)
}
Then add the new users in the mutation.
const mutations = {
setUsers: (state, users) => {
state.users.concat(users);
// or if you have custom logic
users.forEach(user => {
if (whatever) state.users.push(user)
});
}
}
The reason it is slow is related to Strict mode
Strict mode runs a synchronous deep watcher on the state tree for detecting inappropriate mutations, and it can be quite expensive when you make large amount of mutations to the state. Make sure to turn it off in production to avoid the performance cost.
If you want to speed up the mutation, you could do the changes on a new array which would replace the one in the state when ready.
const mutations = {
setUsers: (state, newUsers) => {
state.users = newUsers.reduce((users, user) => {
if (whatever) users.push(user);
return users;
}, state.users.slice()); // here, we start with a copy of the array
}
}

Related

How do I store a non serialisable variable in a react - redux state without breaking it if I really must (like saving a device connection via WebAPIs)

I keep getting the statement "do not save non-serializable variables in your state" in almost every google search result - But what happens when I really should?
Progect: I am building an app for deviceS connected via SerialPort (using SerialPort WebAPI).
I wish to save the connection instance since I use it throughout all my application and I am honestly tired of passing the instance down and up whenever I need it without react knowing to re-render data and display new data - which is important for me too.
Steps that I have done:
It was easy to ignore the non-serializable error using serializableCheck: false:
export default configureStore({
reducer: {
serialport: SerialPortDevicesReducer,
bluetooth: BluetoothDevicesReducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
thunk,
serializableCheck: false
}).concat(logger),})
But now I am facing the big problem:
Whenever I create a connection I get the object that handles that specific SerialPort device object that is connected.
deviceReducer: {
id: 1,
instance: SerialPort{[attr and methods here]},
...
}
Whenever I use methods like open(), write() or read() it changes the main connection instance object and breaks with that known error:
Error: Invariant failed: A state mutation was detected between
dispatches, in the path 'serialport.0.instance.readable'. This may
cause incorrect behavior
Since It's not serializable I cannot clone it (which I think is the reason?) and then re-assign it + I think cloning a connection instance will cause other device-connection issues.
I ended up writing the connect method case directly in the state with a "promise" new variable to handle the result.
// click in a react component
const handleConnect = () => {
try {
if ( dispatch(connect(device)) ) {
setActiveStep((prevActiveStep) => prevActiveStep + 1)
return true
}
}
catch (e) {
console.error("Device cannot connect: ", e)
}
}
// In a file that trigges dispatch() to the reduces
const connect = (deviceId) => async (dispatch, getState) => {
try {
dispatch({
type: "serialport/connect",
payload: deviceId
})
} catch(e) {
console.log(e)
}
}
// in reducer
const SerialPortDevicesReducer = (state = initialState, action) => {
switch (action.type) {
case 'serialport/connect':
try {
return {
...state,
[action.payload]: {
...state[action.payload],
promise: state[action.payload].instance.open({baudRate: 115200})
}
}
} catch (e) {
console.error("Cannot run promise inside reducer: ", e)
}
This is the only workaround I currently found. And this basically forces me to handle (maybe some complex) things in the reducer instead of just passing data to it. I tried applying the same for the write method:
// click in component
const handleExecute = (command) => {
try {
dispatch(writeToSP(device1.device, command))
} catch (e) {
console.log(e)
}
}
// In file which trigges the dispatch()
const writeToSP = (deviceId, command = "Z !\n") => async (dispatch) => {
let startTime = new Date().getTime()
let encoder = new TextEncoder()
try {
dispatch({
type: "serialport/write",
payload: {
id: deviceId,
// cmd: encoder.encode(command),
// startTime
}
})
} catch (e) {
console.error("error writing: ", e)
}
}
// in reducer
...
case 'serialport/write':
try {
const writer = state[action.payload.id].instance.writable.getWriter()
} catch (e) {
console.error("Cannot run promise inside reducer: ", e)
}
and again, get the error of "Error: Invariant failed: A state mutation was detected..." which I am guessing a result of it changing other attributes in the SerialPort instance.
Having packages like redux-promise-middleware are awesome, but it seems like an object in my state is the one responsible for its own promise and changes.
How do I handle this specific situation?
Simple: don't put it into Redux. Redux is made for data, not for arbirtary external libraries/dependency injection.
If that value will never change after initialization and you do the initialization outside of React, just put it into a global variable that is exported.
If that value will change over time, you should use the Dependency Injection mechanism of React for it: Context. This is really what context is made for - not sharing state values, but global dependencies.

Firebase firestore : Unhandled Rejection (RangeError): Maximum call stack size exceeded

Started playing around with createAsyncThunk for learning purpose, decided to implement a shopping cart with firebase firestore but I ran into problems when trying to implement pagination in my react app.
How should I return the last visible state into my redux state during the initial load and subsequent load (infinite loading)
I am basing on code from redux tutorial sandbox :https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/checkpoint-3-postRequests/?from-embed, but instead of connecting to a fake api, I am using firebase firestore.
Code to fetch product from firestore : ProductSlice.js
const InitialState = {
products : [],
status: 'idle',
error: null,
last: null, //to hold lastVisible when fetching data from firestore
}
export const fetchProducts = createAsyncThunk(types.RECEIVE_PRODUCTS, async (limit) => {
const resp = await fire_base_product.firestore()
.collection(collection_name).orderBy('id').limit(limit)
let result = resp.get().then((querySnapshot) => {
const lastVisible = querySnapshot.docs[querySnapshot.docs.length - 1] //how set this to redux state
const products = querySnapshot.docs.map((doc)=> {
return { ...doc.data()}
})
return {
products: products,
lastVisible: lastVisible
};
})
return result;
}
I am not quite sure on how to set this lastVisible data back into redux state, is it possible to do that with reference?
#Edit:
Tried to return both product list and last visible as an array and assign lastVisible in createSlice as stated below:
const productSlice = createSlice({
name:'products',
initialState:
reducers: {},
extraReducers:{
[fetchProducts.fulfilled]: (state, action) => {
state.products = state.products.concat(action.payload.products)
state.last = action.payload.lastVisible // this causes call stack error
}
}
});
With the above coding, two error will be reported if I run react app,
Trying to assign non serialize value into redux state
Maximum call stack size exceeded in firestore
I then tried to add middleware serializableCheck during create configuration as below:
export default configureStore({
middlleware: getDefaultMiddlleWare({
serializableCheck: {
//ignore action type
ignoredActions : ['RECEIVE_PRODUCTS/fulfilled']
// ignore path
ignoredPath: ['products.last']
}
}),
... // reducer codes
})
Even though now I have dismissed the first error, call stack exceeded still exists. Does anyone knows why this is happening ? Feel free to discuss if there is any workaround on this. Thanks.
Edit 2
Similar approach works when using context but does not work when using redux. Do I need to wrap return in promise as suggested in Firebase Unhandled error RangeError: Maximum call stack size exceeded ?
Did not managed to find a way to save lastVisible, but I found a workaround by just keeping track of the last retrieve id of my firestore data by saving it into redux state.
export const fetchProducts = createAsyncThunk(types.RECEIVE_PRODUCTS, async (limit) => {
const resp = await fire_base_product.firestore()
.collection(collection_name).orderBy('id').limit(limit)
let result = resp.get().then((querySnapshot) => {
var lastVisible = limit - 1; //only keep track of ID so we can avoid saving un-serialize coded
const products = querySnapshot.docs.map((doc)=> {
return { ...doc.data()}
})
return {
products: products,
lastVisible: lastVisible
};
})
return result;
}
And when during fetch of additional data we can then access the state by using getState() as below:
export const fetchMoreProducts = createAsyncThunk(types.LOAD_MORE_PRODUCTS, async (limit, {getState}) => {
const last = getState().products.last
var newProducts = await firebase_product.firestore()
.collection('store_products').orderBy('id')
.startAfter(last).limit(limit)
const result = newProducts.get().then((querySnapshot) => {
var lastVisible = last + limit;
const products = querySnapshot.docs.map((doc) => {
return { ...doc.data() }
})
return {
products : products,
lastVisible: lastVisible
}
})
// return retrieved data from firebase
return result
})
But doing this, I could skip the serialization check config all together as well. Not sure if this is the correct way, but this is how I got pagination working. Feel free to let me know if there is other way to approach this.
This error will come if you store non-serializable object in redux store!
if you are getting some non-serializable data from firebase, serialize it before storing it in redux store!
const nonSerializable = firestore().collection().doc(uid).get().data();
// if using this nonSerializable object in reducer, serialized it using JSON.parse(JSON.stringify())
const serializable = JSON.parse(JSON.stringify(nonSerializable))
you should save last visible path not the whole data like this
const lastVisible = querySnapshot.docs[querySnapshot.docs.length - 1].ref.path

Vue, Vuex, JavaScript: includes() does not work as expected

I wanna store some objects inside an array if the array doesn't already contain some object with the same id. anyways, everything works fine til i start adding more than one object at a time.
Here is the related code using Vuex:
// filter function to check if element is already included
function checkForDuplicate(val) {
for( let sessionItem of state.sessionExercises ) {
return sessionItem._id.includes(val._id);
}
};
// related array from vuex state.js
sessionExercises: [],
// vuex mutation to store exercises to session exercises
storeSessionExercises: (state, payload) => {
// Pre filtering exercises and prevent duplicated content
if( checkForDuplicate(payload) === true ) {
console.log("Exercise ist bereits für session registriert!");
} else {
state.sessionExercises.push(payload);
}
},
// Related vuex action
storeSessionExercises: ({ commit }, payload) => {
commit("storeSessionExercises", payload)
},
As I wrote before everything works fine as long i ad a single object, checkForDuplicate() will find duplicated objects and deny a push to the array.
now there is a case in which I wanna push a bundle of objects to the array, which i am doing through an database request, looping through the output, extracting the objects and pushing them through the same function as I do with the single objects:
// get user related exercises from database + clear vuex storage + push db-data into vuex storage
addSessionWorkout: ({ commit, dispatch }, payload) => {
axios.post(payload.apiURL + "/exercises/workout", payload.data, { headers: { Authorization: "Bearer " + payload.token } })
.then((result) => {
// loop through output array and
for( let exercise of result.data.exercises ) {
// push (unshift) new exercise creation to userExercises array of vuex storage
dispatch("storeSessionExercises", exercise)
};
})
.catch((error) => {
console.error(error)
});
},
The push does also work as it should, the "filter function" on the other hand doesn't do its job. It will filter the first object and deny to push it to the array, but if there is a second one that one will be pushed to the array even inf the same object (same Id) is already included, what am I not seeing here!? makes me nuts! :D
I understand it like the loop will put each object through the checkForDuplicate() and look if there is an duplicate it should output true, so the object doesn't get pushed into the array. If anybody sees what I currently don't just let me know.
the mistake is your filter function. you want to loop over your sessionExercises and only return true if any of them matches. However, at the moment you return the result of the very first check. Your loop will always only run one single time.
Option 1: only return if matched
function checkForDuplicate(val) {
for( let sessionItem of state.sessionExercises ) {
if (sessionItem._id.includes(val._id)) {
return true;
}
}
return false;
};
Option 2: use es6 filter
storeSessionExercises: (state, payload) => {
var exercises = state.sessionExercises.filter(ex => (ex._id.includes(payload._id)));
if(exercises.length) {
console.log("Exercise ist bereits für session registriert!");
} else {
state.sessionExercises.push(payload);
}
}
I would change the addSessionWorkout action, I would create a new exercises array with the old and new entries and then update the state.
// related array from vuex state.js
sessionExercises: [],
// vuex mutation to store exercises to session exercises
storeSessionExercises: (state, payload) => {
state.sessionExercises = payload;
},
// Related vuex action
storeSessionExercises: ({ commit }, payload) => {
commit("storeSessionExercises", payload)
},
addSessionWorkout: async({
commit,
dispatch,
state
}, payload) => {
const result = await axios.post(payload.apiURL + "/exercises/workout", payload.data, {
headers: {
Authorization: "Bearer " + payload.token
}
})
try {
const newExercices = result.data.exercises.reduce((acc, nextItem) => {
const foundExcercise = acc.find(session => session.id === nextItem.id)
if (!foundExcercise) {
return [...acc, nextItem]
}
return acc
}, state.sessionExercises)
dispatch("storeSessionExercises", foundExcercise)
} catch (e) {
console.error(error)
}
},

Redux.js and data with relations

Imagine that you develop some react-redux application (with global immuatable tree-state). And some data have some rules-relations in different tree-branches, like SQL relations between tables.
I.e. if you are working on some company's todos list, each todo has relation(many-to-one) with concrete user. And if you add some new user, you should add empty todo list (to other branch in the state). Or delete user means that you should re-assign user's todos to some (default admin) user.
You can hardcode this relation directly to source code. And it is good and works OK.
But imagine that you have got million small relations for data like this. It will be good that some small "automatic" operations/checks (for support/guard relations) performs automatically according to rules.
May be existed some common approach/library/experience to do it via some set of rules: like triggers in SQL:
on add new user => add new empty todos
on user delete => reassign todos to default user
There are two solutions here. I don't think that you should aim to have this kind of functionality in a redux application, so my first example is not quite what you're looking for but I think is more conical. The second example adopts a DB/orm pattern, which may work fine, but is not conical, and requires
These could be trivially added safely with vanilla redux and redux-thunk. Redux thunk basically allows you to dispatch a single action that its self dispatches multiple other actions--so when you trigger CREATE_USER, just do something along the lines of triggering CREATE_EMPTY_TODO, CREATE_USER, and ASSIGN_TODO in the createUser action. For deleting users, REASSIGN_USER_TODOS and then DELETE_USER.
For the examples you provide, here are examples:
function createTodoList(todos = []) {
return dispatch => {
return API.createTodoList(todos)
.then(res => { // res = { id: 15543, todos: [] }
dispatch({ type: 'CREATE_TODO_LIST_SUCCESS', res });
return res;
});
}
}
function createUser (userObj) {
return dispatch => {
dispatch(createTodoList())
.then(todoListObj => {
API.createUser(Object.assign(userObj, { todoLists: [ todoListObj.id ] }))
.then(res => { // res = { id: 234234, name: userObj.name, todoLists: [ 15534 ]}
dispatch({ type: 'CREATE_USER_SUCCESS', payload: res });
return res;
})
})
.catch(err => console.warn('Could not create user because there was an error creating todo list'));
}
}
Deleteing, sans async stuff.
function deleteUser (userID) {
return (dispatch, getState) => {
dispatch({
type: 'REASSIGN_USER_TODOS',
payload: {
fromUser: userID,
toUser: getState().application.defaultReassignUser
});
dispatch({
type: 'DELETE_USER',
payload: { userID }
});
}
}
The problem with this method, as mentioned in the comments, is that a new developer might come onto the project without knowing what actions already exist, and then create their own version of createUser which doesn't know to create todos. While you can never completely take away their ability to write bad code, you can try to be more defensive by making your actions more structured. For example, if your actions look like this:
const createUserAction = {
type: 'CREATE',
domain: 'USERS',
payload: userProperies
}
you can have a reducer structured like this
function createUserTrigger (state, userProperies) {
return {
...state,
todoLists: {
...state.todoLists,
[userProperies.id]: []
}
}
}
const triggers = {
[CREATE]: {
[USERS]: createUserTrigger
}
}
function rootReducer (state = initialState, action) {
const { type, domain, payload } = action;
let result = state;
switch (type) {
case CREATE:
result = {
...state,
[domain]: {
...state[domain],
[payload.id]: payload
}
};
break;
case DELETE:
delete state[domain][payload.id];
result = { ...state };
break;
case UPDATE:
result = {
...state,
[domain]: {
...state[domain],
[payload.id]: _.merge(state[domain][payload.id], payload)
}
}
break;
default:
console.warn('invalid action type');
return state;
}
return triggers[type][domain] ? triggers[type][domain](result, payload) : result;
}
In this case, you're basically forcing all developers to use a very limited possible set of action types. Its very rigid and I don't really recommend it, but I think it does what you're asking.

Isomorphic Redux app not registering Redux-Thunk?

I seem to have a weird bug. I'm currently using Redux isomorphically and am also including redux-thunk as the middleware for async actions. Here's what my store config looks like:
// Transforms state date from Immutable to JS
const transformToJs = (state) => {
const transformedState = {};
for (const key in state) {
if (state.hasOwnProperty(key)) transformedState[key] = state[key].toJS();
}
return transformedState;
};
// Here we create the final store,
// If we're in production, we want to leave out development middleware/tools
let finalCreateStore;
if (process.env.NODE_ENV === 'production') {
finalCreateStore = applyMiddleware(thunkMiddleware)(createStore);
} else {
finalCreateStore = applyMiddleware(
createLogger({transformer: transformToJs}),
thunkMiddleware
)(createStore);
}
// Exports the function that creates a store
export default function configureStore(initialState) {
const store = finalCreateStore(reducers, initialState);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('.././reducers/index', () => {
const nextRootReducer = require('.././reducers/index');
store.replaceReducer(nextRootReducer);
});
}
return store;
}
The weird part about this is that I don't think there's anything wrong with this file because my createLogger is applied just fine. It logs out all my actions and state, but the minute I return a function instead of an object in an action creator, the execution is lost. I've tried throwing in debugger statements, which never hit and reordering the middleware also doesn't seem to help.
createUser(data) {
// This `debugger` will hit
debugger;
return (dispatch) => {
// This `debugger` will NOT hit, and any code within the function will not execute
debugger;
setTimeout(() => {
dispatch(
AppActionsCreator.createFlashMessage('yellow', 'Works!')
);
}, 1000);
};
},
Has anyone experienced something like this before?
DOH! I wasn't dispatching the action. I was only calling the action creator. Gonna have to get used to that with Redux!
How I thought I was invoking an action:
AppActionCreators.createFlashMessage('some message');
How to actually invoke an action in Redux:
this.context.dispatch(AppActionCreators.createFlashMessage('some message'));
Where dispatch is a method provided by the Redux store, and can be passed down to every child component of the app through React's childContextTypes

Categories

Resources