I'm receiving this error: Invariant failed: A state mutation was detected inside a dispatch and can't seem to find a solution for it. I'm fetching user support tickets via an API and saving it to a state in order to display the ticket information. Here is my code:
const initialState: stateType = {
User: new User(),
SupportTickets: new SupportTickets()
}
const userSlice = createSlice({
name: "User",
initialState,
reducers: {
setUser: (state, {payload}) => {
state.User = payload
},
setSupportTickets: (state, {payload}) => {
state.SupportTickets.tickets = payload
}
}
})
export const {setUser, setSupportTickets} = userSlice.actions
export const userSelector = (state: stateType) => state.User
export const userSupportTicketsSelector = (state: stateType) => state.SupportTickets
export function fetchSupportTickets(userId: string, token: string) {
return async (dispatch: AppDispatch) => {
return await fetch(API_URL, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`
}
}).then(response => {
if (!response.ok) { throw response }
return response.json()
}).then(json => {
dispatch(setSupportTickets(json))
}).catch(error => {
console.log('Failed to fetch support tickets', error);
})
}
}
interface stateType {
User: User
SupportTickets: SupportTickets
}
export class SupportTickets {
tickets: SupportTicket[] = []
}
export class SupportTicket {
id: number = 0
type: string = ""
subject: string = ""
priority: string = ""
status: string = ""
created_at: string = ""
updated_at: string = ""
}
I've tried changing/moving things around without any success. Any help is greatly appreciated. Thanks in advance.
I see the problem. It looks like you're creating some kind of classes to store your data, and putting those in the Redux state:
const initialState: stateType = {
User: new User(),
SupportTickets: new SupportTickets()
}
You should never put class instances or other non-serializable values into the Redux state. Instead, you should use plain JS objects, arrays, and primitives.
The good news is that you're using Redux Toolkit's createSlice API, which will let you write "mutating" update logic that becomes a safe and correct immutable update. But, you need to be using plain JS objects in our state for that to work correctly.
I'd strongly recommend reading through the two tutorials in the Redux core docs:
The "Redux Essentials" tutorial, which teaches beginners "how to use Redux, the right way", using our latest recommended tools and practices.
The "Redux Fundamentals" tutorial, which teaches "how Redux works" from the bottom up.
Related
I've been working on the user registration of my React-Redux project (I'm really new to Redux), and I'm having trouble with registering Users with error handling...
My curriculum teaches nothing on using Redux with React (which is what my project does) to send data to an backend database.
I have been using useState for setting both the current user (currentUser, onLogin) and any errors (errors, setErrors) inside of UserInput.jsx, and I'm trying to move that functionality over to usersSlice.js.
Right now, I am trying to set my User object to be the action.payload. I also need to deal with how to pass the new User's info from my form, to my Slice.
Any solutions?
My code:
usersSlice.js
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
export const signup = createAsyncThunk("users/signup", async ({username, password}, thunkAPI) => {
await fetch("/signup", {
method: "POST",
headers: headers,
body: JSON.stringify({user: {username, password}})
}).then((data) => {
data.json().then((data) => {
if(data.errors){
return thunkAPI.rejectWithValue(data.errors);
} else{
// Find a way to set the current user!!!
return data;
}
})
})
});
const usersSlice = createSlice({
name: "users",
initialState: {
user: [], // This should be an SINGLE User Object!!!
errorMessage: null,
status: 'idle',
},
reducers: {
userLogin(state, action){
state.user.push(action.payload);
},
userLogout(state){
state.user = [];
},
},
extraReducers(builder){
// Omit extraReduxers logic
}
});
UserInput.jsx:
function UserInput({onLogin, username, setUsername, password, setPassword, errors, setErrors}){
const dispatch = useDispatch();
function handleSubmit(e){
e.preventDefault();
const user ={
username: username,
password: password
}
dispatch(signup(user));
if(user.errors) { // Returns null due to the user object above...
setErrors(user.errors);
}
else{
setErrors(null);
onLogin(user); // setCurrentUser(user);
}
}
// Omit the signup form
}
export default UserInput;
const authSlice = createSlice({
name: "auth",
initialState: {
value: {
fields: initialState,
},
},
reducers: {
loginUser: async (state, action) => {
state.value.fields.loading = true;
await http
.post("/login", {
email: state.value.fields.email,
password: state.value.fields.password,
})
.then((response) => {
console.log(response.data);
});
},
},
});
I'm receiving an error saying A non-serializable value was detected in the state, in the path: auth. Value: When I try to make this loginUser async.When I remove async await from the loginUser it's working.Really appreciate it if somebody could help.
You must never do any async work inside of a Redux reducer!
One of the primary rules of Redux is:
Reducers must always be 100% synchronous and "pure", with no side effects ( https://redux.js.org/style-guide/#reducers-must-not-have-side-effects )
Additionally, all state updates must be done by "dispatching an action" (like store.dispatch({type: "todos/todoAdded", payload: "Buy milk"}) ), and then letting the reducer look at that action to decide what the new state should be.
If you need to do async work, the right answer is usually to put the async logic into a "thunk", and dispatch actions based on the async results.
I'd strongly recommend going through the Redux docs tutorials to learn more about this and how to use Redux:
https://redux.js.org/tutorials/index
CONTEXT
I have two store modules : "Meetings" and "Demands".
Within store "Demands" I have "getDemands" action, and within store "Meetings" I have "getMeetings" action. Prior to access meetings's data in Firestore, I need to know demands's Id (ex.: demands[i].id), so "getDemands" action must run and complete before "getMeetings" is dispatched.
Vuex documentation dispatching-action is very complete, but still, I don't see how to fit it in my code. There are also somme other good answered questions on the topic here :
Vue - call async action only after first one has finished
Call an action from within another action
I would like to know the best way to implement what I'm trying to accomplish. From my perspective this could be done by triggering one action from another, or using async / await, but I'm having trouble implementing it.
dashboard.vue
computed: {
demands() {
return this.$store.state.demands.demands;
},
meetings() {
return this.$store.state.meetings.meetings;
}
},
created() {
this.$store.dispatch("demands/getDemands");
//this.$store.dispatch("meetings/getMeetings"); Try A : Didn't work, seems like "getMeetings" must be called once "getDemands" is completed
},
VUEX store
Module A – demands.js
export default {
namespaced: true,
state: {
demands:[], //demands is an array of objects
},
actions: {
// Get demands from firestore UPDATED
async getDemands({ rootState, commit, dispatch }) {
const { uid } = rootState.auth.user
if (!uid) return Promise.reject('User is not logged in!')
const userRef = db.collection('profiles').doc(uid)
db.collection('demands')
.where('toUser', "==", userRef)
.get()
.then(async snapshot => {
const demands = await Promise.all(
snapshot.docs.map(doc =>
extractDataFromDemand({ id: doc.id, demand: doc.data() })
)
)
commit('setDemands', { resource: 'demands', demands })
console.log(demands) //SECOND LOG
})
await dispatch("meetings/getMeetings", null, { root: true }) //UPDATE
},
...
mutations: {
setDemands(state, { resource, demands }) {
state[resource] = demands
},
...
Module B – meetings.js
export default {
namespaced: true,
state: {
meetings:[],
},
actions: {
// Get meeting from firestore UPDATED
getMeetings({ rootState, commit }) {
const { uid } = rootState.auth.user
if (!uid) return Promise.reject('User is not logged in!')
const userRef = db.collection('profiles').doc(uid)
const meetings = []
db.collection('demands')
.where('toUser', "==", userRef)
.get()
.then(async snapshot => {
await snapshot.forEach((document) => {
document.ref.collection("meetings").get()
.then(async snapshot => {
await snapshot.forEach((document) => {
console.log(document.id, " => ", document.data()) //LOG 3, 4
meetings.push(document.data())
})
})
})
})
console.log(meetings) // FIRST LOG
commit('setMeetings', { resource: 'meetings', meetings })
},
...
mutations: {
setMeetings(state, { resource, meetings }) {
state[resource] = meetings
},
...
Syntax:
dispatch(type: string, payload?: any, options?: Object): Promise<any
Make the call right
dispatch("meetings/getMeetings", null, {root:true})
I am trying to get my firestore data that I am getting to be stored in my state but it does not show up in my Vue dev tools in the state.
When I console.log() the data I am getting through the store action I can see I am getting the right data but it will not update the state.
I am using middle-ware on my home page and another page to dispatch my action in order to get the required data.
I have also used a conditional statement within the middle-ware below to try to only dispatch action when my other state variables are not null because the firestore query requires state.user
//this is check-auth middleware
export default function(context) {
// in initAuth we are forwarding it the req
context.store.dispatch('initAuth', context.req)
console.log('WE ARE GRABBING USER INFO')
context.store.dispatch('grabUserInfo', context.req)
console.log('There is already user info there')
// context.store.dispatch('currentUser')
}
We are dispatching grabUserInfo to run a action that has a firestore query in it.
grabUserInfo(vuexContext, context) {
let userInfo = []
var userRef = db.collection('users')
var query = userRef
.where('user_id', '==', vuexContext.state.user)
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
console.log(doc.data())
userInfo.push(doc.data())
})
})
vuexContext.commit('setUserInfoSub', userInfo)
}
my
console.log(doc.data()) is showing
subscribers: ["noFace2"]
subscriptions: ["noFace3"]
user_id: "VbomJvANYDNe3Bek0suySs1L8oy1"
username: "noFace1"
my info should be going through a mutation and commiting to state, but it does not show up in my state vue dev tools.
setUserInfoSub(state, payload) {
state.userInfoSub = payload
}
I don't understand how the data is not ending up in my state. Here is my State and mutations.
const createStore = () => {
return new Vuex.Store({
state: {
loadedCards: [],
user: null,
username: null,
userInfoSub: [],
token: null
},
mutations: {
setCards(state, cards) {
state.loadedCards = cards
},
setUser(state, payload) {
state.user = payload
},
setUsername(state, payload) {
state.username = payload
},
setUserInfoSub(state, payload) {
state.userInfoSub = payload
},
setToken(state, token) {
state.token = token
}
Change your mutation to this:
setUserInfoSub(state, payload) {
Vue.set(state, 'userInfoSub', payload);
}
This will allow Vue's reactivity system to kick back in for the state variable reassignment.
Per #Alexander's comment, you should also move the commit() inside then() given the async nature of the Firebase query.
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.