Redux - Does initialState need to be an object in createSlice()? - redux

This code below throws an error when I dispatch:
// countSlice.js
const countSlice = createSlice({
name: "count",
initialState: 0,
reducers: {
add(state) {state += 1},
},
});
However if I change initialState into an object, the code works fine:
// countSlice.js
const countSlice = createSlice({
name: "count",
initialState: {value: 0}, // now it works!
reducers: {
add(state) {state.value += 1},
},
});
The code also works if I keep initialState as a number, but write the reducer as an array function:
// countSlice.js
const countSlice = createSlice({
name: "count",
initialState: 0,
reducers: {
add: state => state + 1, // also works!
},
});
I'm just learning Redux and got confused by this. Does this have something to do with Immer? Did I make a mistake with the reducer function?

The reason why
const countSlice = createSlice({
name: "count",
initialState: 0,
reducers: {
add(state) {state += 1},
},
});
doesn't work, is because your add reducer isn't actually returning anything. This is fine if the state is an object, because objects are mutable. But if your state is just an integer, then you need to return it to actually update the state, as integers are immutable.

You simply return the state itself if you want to set initial state as a value and not an object
const idSlice = createSlice({
name: "idSlice",
initialState: "",
reducers: {
setId(state, action) {
state = action.payload;
return state;
},
},
});
I did this, works for me

Related

Redux - Slice doesn't store data in state?

I want to create a Slice that returns some data.
The Problem: from the redux-logger (action) I can tell that the requested data is returned, but isn't stored in a state...
This is the slice:
const postSlice = createSlice({
name: 'postboxApp/post',
initialState: [],
reducers: {
newPost: (state, action) => PostModel(),
resetPost: () => null,
},
extraReducers: {
[getPost.fulfilled]: (state, action) => action.payload,
},
});
Here is the way I call the Asny Thunk:
dispatch(getPost(routeParams.id));
Does anyone see my mistake?

What is the use of the `prepare` argument in the createSlice function of Redix Toolkit?

I'm following the Redux tutorial from redux site, and in Part 8, when writing a reducer to handle todos they do the following:
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
//...
todoColorSelected: {
reducer(state, action) {
const { color, todoId } = action.payload
state.entities[todoId].color = color
},
prepare(todoId, color) {
return {
payload: { todoId, color }
}
}
},
//...
}
})
They make use of the prepare function; however, I wrote that code without knowing the existence of the prepare function as follows (only using the reducer) and it works as well.
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
//...
todoColorSelected: {
const { color, todoId } = action.payload
state.entities[todoId].color = color
},
//...
}
})
So why do we need in this case the prepare function? or is it just a simple example where it makes no difference?
It's a helper to run code before action.payload is handled by a reducer, you can transform the payload etc. https://redux-toolkit.js.org/api/createAction#using-prepare-callbacks-to-customize-action-contents

Multiple actions from one redux-toolkit thunk

toolkit, and I'm trying to understand exactly how createAsyncThunk fits in all of this. I want to make a call to my server to get session info and the user's info in one call. However, from what I've seen so far on createAsyncThunk, it only uses a single slice of state? Could someone elaborate on how I could hit both session and user at once? Below is an example to illustrate.
const payload = await myApi.login(loginInfo)
dispatch(loginSession(payload.session)
dispatch(loginUser(payload.user)
export const sessionSlice = createSlice({
name:'session',
initialState,
reducers: {
loginSession: (state,action:PayloadAction<schema.Session>) => {
state = action.payload
},
},
})
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
loginUser:(state,action:PayloadAction<schema.User>) => {
if (state !== null) {
throw new Error('a user is already logged in')
}
state = action.payload
},
}
})
const payload = await myApi.login(loginInfo)
dispatch(loginFulfilled({
session: payload.session,
user: payload.user,
})
export const sessionSlice = createSlice({
name:'session',
initialState,
reducers: {
loginFulfilled: (state,action:PayloadAction<{session: schema.Session}>) => {
state = action.payload.session
},
},
})
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
loginFulfilled:(state,action:PayloadAction<{user: schema.User>}) => {
if (state !== null) {
throw new Error('a user is already logged in')
}
state = action.payload.user
},
}
})

redux-toolkit sharing state between slice reducer

I'm building an app where a "slice reducer" needs to access state of another "slice reducer". The redux docs talks about using a custom combine reducer in order to pass in the root state to the reducer - Beyond combineReducers
Thus far, I have this for my root reducer:
import cats from '../slices/cats'
import dogs from '../slices/dogs'
import status from '../slices/status'
function combinedReducer(state = {}, action) {
return {
status: status(state.status, action),
dogs: dogs(state.dogs, action),
cats: cats(state.cats, action, state),
};
}
export default configureStore({ reducer: combinedReducer });
I don't seem to be able to get the root state for my cats reducer - passed in as the 3rd arg above.
const assetsSlice = createSlice({
name: 'cats',
initialState,
reducers: {
setFetched: (state, { payload }, root) => {
// root is undefined
state.type = payload + root.dogs.legs;
},
},
});
This should work, no?
If I use a vanilla reducer that's not created by createSlice I am able to get the root state
export default (state = initialState, action, root) => {
// root - { status: {}, dogs: {}, cats: {} }
};
This is not possible as a third argument since RTK's reducers only pass the first two arguments to the case reducers.
You could just add it to the action though (but granted, that's hacky):
function combinedReducer(state = {}, action) {
const actionWithFullState = { ...action, meta: {...action.meta, fullState: state }}
return {
status: status(state.status, action),
dogs: dogs(state.dogs, action),
cats: cats(state.cats, actionWithFullState),
};
}

How does one have multiple cases that run the same code with Redux Toolkit?

I'm working on converting some older Redux code to use the new Redux Toolkit. I've run into a problem where, in the old code, multiple case statements would trigger the same reducer logic. How does one do this with the new case reducer functions?
In the old code, REGISTER_FAIL, AUHT_ERROR, LOGIN_FAIL, LOGOUT all run the same code. Is it possible to have this same type scenario in the createSlice reducers object?
Old Code
case REGISTER_FAIL:
case AUTH_ERROR:
case LOGIN_FAIL:
case LOGOUT:
localStorage.removeItem('token');
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
};
default:
return state;
New Code
const authUserSlice = createSlice({
name: 'authUser',
initialState,
reducers: {
registerFail(state, action) {
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
};
},
registerSuccess
},
});
There's a couple different options.
First, you could write the case reducer functions separately, then pass them to createSlice multiple times to generate corresponding actions for each field name:
function resetState() {
Object.assign(state, {
token: null,
// etc
});
}
const authUserSlice = createSlice({
name: 'authUser',
initialState,
reducers: {
registerFailed: resetState,
logout: resetState,
// etc
}
});
The other option is to use the extraReducers field, and use builder.addMatcher() to handle multiple cases with the same reducer:
const authUserSlice = createSlice({
name: 'authUser',
initialState,
reducers: {
// omit
},
extraReducers: builder => {
builder.addMatcher(
// can pass multiple RTK action creators here
isAnyOf(registerFailed, loginFailed),
(state, action) => {
// reset state here
}
)
}
});
If you're still interoping with old code and have old-style action constants like const LOGIN_FAILED = "LOGIN_FAILED" and need to match those, you can write your own "matching" function that just does a string type comparison, like:
builder.addMatcher(
(action) => [LOGIN_FAILED, REGISTER_FAILED].includes(action.type),
(state, action) => {}
)

Resources