import { createSlice } from "#reduxjs/toolkit";
const initialState = {
items: [],
};
export const basketSlice = createSlice({
name: "basket",
initialState,
reducers: {
// actions & reducers; actions named same as reducers in redux toolkit
addToBasket: (state, action) => {
state.items = [...state.items, action.payload];
},
removeFromBasket: (state, action) => {
const index = state.items.findIndex(
(basketItem) => action.payload.id === basketItem.id
);
let newBasket = [...state.items];
if (index >= 0) {
// item exists in the basket; remove it
newBasket.splice(index, 1);
} else {
// else the item doesn't exist
console.warn(
`Can't remove product (id: ${action.payload.id}) as it's not in the basket.`
);
}
state.items = newBasket;
},
},
});
export const { addToBasket, removeFromBasket } = basketSlice.actions;
export const basketActions = basketSlice.actions;
export const selectItems = (state) => state.basket.items;
export const selectTotal = (state) =>
state.basket.items.reduce((total, item) => total + item.price, 0);
export default basketSlice.reducer;
Where is the passed in "state" variable coming from? I don't understand how this works. A function is being returned here and React is injecting the state as the first argument? But since I'm defining the function, how does React know to do this?
Does every returned function have state as the first property? But even then, I don't understand how this will work as state is coming from the above basketSlice.
I see, the function declaration has access to the global state, meaning all slices.
In this example we're getting the basket state via the state.basket reference and the function declaration is just a helper/wrapper function that doesn't need to even be in the basketSlice module.
I tried the following in another module and it works well.
const items = useSelector((state) => state.basket.items);
Now I understand.
Related
I am making this shopping cart in redux-toolkit and rtk query. I want to change the cartItem index with product id.
I think the problem is with this line of code:
const cartItems = state.cartItems
My full code:
import { createSlice, current } from '#reduxjs/toolkit';
const initialState = {
cartItems: [],
};
export const cartSlice = createSlice({
name: 'cartSlice',
initialState: initialState,
reducers: {
setToCart: (state, action) => {
const { payload:product, newQty = 1 } = action;
const cartItems = state.cartItems;
const quantity = cartItems?.[product?._id]
? parseInt(cartItems[product._id].quantity + newQty)
: 1;
cartItems[product._id] = {
...product,
quantity,
};
return {
...state,
cartItems: cartItems
}
},
},
});
export const {setToCart} = cartSlice.actions;
export default cartSlice.reducer;
Here is action.payload:
{
img: "_OCT2HGtyHbioAuVTMGcA-mauntain.jpg",
name: "iphone",
price: 600001,
_id: "60d375ed3224711bc0f3538a"*
}
As the error states, when using an Immer-powered reducer you must Either return a new value or modify the draft.
You are modifying the cartItems array. You do not need to return anything from your reducer. Just modify the values that you want to change. That's the magic of Redux Toolkit! (powered by Immer).
There are some other issues with your reducer:
action will not have a property newQty. It will only have a type and payload. Perhaps newQty is a property of the payload?
It seems like cartItems is a dictionary keyed by _id, but you initialize it as an empty array [] instead of an empty object {}.
parseInt doesn't make sense outside of an addition statement. parseInt("2" + "2") will be 22, not 4.
A "fixed" version might look like this, depending on how you structure your payload:
import { createSlice } from '#reduxjs/toolkit';
const initialState = {
cartItems: {},
};
export const cartSlice = createSlice({
name: 'cartSlice',
initialState: initialState,
reducers: {
setToCart: (state, action) => {
const { payload } = action;
const { product, newQty = 1 } = payload;
const cartItems = state.cartItems;
// If already in cart, increase quantity.
if (cartItems[product._id]) {
cartItems[product._id].quantity += newQty;
}
// Otherwise add to cart.
else {
cartItems[product._id] = {
...product,
quantity: newQty
}
}
// Don't return anything.
},
},
});
export const {setToCart} = cartSlice.actions;
export default cartSlice.reducer;
I am trying to make a system like shopping cart with redux and react. The products are stored in a redux slice array as a whole object. The product object goes like this:
This is my code for my checkbox input
const products = useSelector((state) => state.prodSlice.value)
const handleChange = (event) => {
const button = event.target
const isActive = button.checked
const itemName = event.currentTarget.id
const items = products.items
const itemsArr = {}
items.forEach((items) => {
if (items.productName === itemName) {
itemsArr['productName'] = items.productName
itemsArr['total'] = items.total
itemsArr['quantity'] = items.quantity
if (isActive) {
dispatch(checkout({products: itemsArr}))
} else {
dispatch(removeItem({products: itemsArr}))
}
}
})
}
When adding products to the array, there is no problem,
However, when I uncheck an item, and get the value of array it returns just an empty array instead of removing just 1 item.
I just want to delete that one item from the array, here is my redux slice code
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
value: {
products: [],
}
}
export const checkOut = createSlice({
name: "checkout",
initialState,
reducers: {
checkout: (state, action) => {
state.value.products.push(action.payload)
},
removeItem: (state, action) => {
state.value.products = state.value.products.filter((products) => products.produdctName !== action.payload.productName)
}
}
})
export const { checkout, removeItem } = checkOut.actions
export default checkOut.reducer
I hope someone can help me pls
your removeItem reducer should simply return the filtered array
removeItem: (state, action) => {
return state.value.products.filter((products) => products.productName !== action.payload.productName)
}
I am getting 'TypeError: props.numberArray.map is not a function react' this error on fetching array from redux, It is working fine if I use redux's useSelector to fetch array data
component
<Typography>Name:{props.name}</Typography>
{
props.numberArray && props.numberArray.map(numbers=>
<Typography>{numbers} </Typography>)
}
redux-connect
const mapStateToProps = (state) =>
{
return{
name:state.name,
numberArray:state.numberArray,
}
}
const mapStateToDispatch = (dispatch) =>
{
return{
updateName:() =>dispatch(updateName('orange')),
numberArray:() =>dispatch(updateNumberArray(15)),
}
}
reducer
const initialState = { name:'apple', numberArray:[1,2,3,4]}
const userReducer = createSlice({ name:'user',initialState,
reducers:{
updateName(state,action){ state.name = action.payload },
updateNumberArray(state,action){ state.numberArray.push(action.payload)}
}
Both mapStateToProps and mapStateToDispatch define the numberArray property so there is a collision.
The property numberArray is a function (instead of an array) because you have numberArray:() => dispatch(updateNumberArray(15)) in the object returned by mapStateToDispatch function (by the way the correct name would be mapDispatchToProps)
I would rename numberArray to updateNumberArray
const mapDispatchToProps = (dispatch) => ({
updateName:() =>dispatch(updateName('orange')),
updateNumberArray:() =>dispatch(updateNumberArray(15))
})
In your component you just need to call props.updateNumberArray()
I made a todo list a while ago as a way to practice react and redux. Now I'm trying to rewrite it with redux toolkit and having some trouble with the action creators.
Here is the old actions creator:
export const changeDescription = (event) => ({
type: 'DESCRIPTION_CHANGED',
payload: event.target.value })
export const search = () => {
return (dispatch, getState) => {
const description = getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
axios.get(`${URL}?sort=-createdAt${search}`)
.then(resp => dispatch({ type: 'TODO_SEARCHED', payload: resp.data }))
} }
export const add = (description) => {
return dispatch => {
axios.post(URL, { description })
.then(() => dispatch(clear()))
.then(() => dispatch(search()))
} }
export const markAsDone = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: true })
.then(() => dispatch(search()))
} }
export const markAsPending = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: false })
.then(() => dispatch(search()))
} }
export const remove = (todo) => {
return dispatch => {
axios.delete(`${URL}/${todo._id}`)
.then(() => dispatch(search()))
} }
export const clear = () => {
return [{ type: 'TODO_CLEAR' }, search()] }
Now this is the one that I'm working on, I'm trying to replicate the actions of the old one but using redux toolkit:
export const fetchTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
const response = await axios.get(`${URL}?sort=-createdAt${search}`)
return response.data
})
export const addTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const response = await axios.post(URL, {description})
return response.data
})
export const todoReducer = createSlice({
name: 'counter',
initialState: {
description: '',
list: []
},
reducers: {
descriptionChanged(state, action) {
return {...state, dedescription: action.payload}
},
descriptionCleared(state, action) {
return {...state, dedescription: ''}
},
},
extraReducers: builder => {
builder
.addCase(fetchTodos.fulfilled, (state, action) => {
const todo = action.payload
return {...state, list: action.payload}
})
.addCase(addTodos.fulfilled, (state, action) => {
let newList = state.list
newList.push(action.payload)
return {...state, list: newList}
})
}
})
The thing is, I can't find anywhere how to export my extra reducers so I can use them. Haven't found anything in the docs. Can someone help?
extraReducers
Calling createSlice creates a slice object with properties reducers and actions based on your arguments. The difference between reducers and extraReducers is that only the reducers property generates matching action creators. But both will add the necessary functionality to the reducer.
You have correctly included your thunk reducers in the extraReducers property because you don't need to generate action creators for these, since you'll use your thunk action creator.
You can just export todoReducer.reducer (personaly I would call it todoSlice). The reducer function that is created includes both the reducers and the extra reducers.
Edit: Actions vs. Reducers
It seems that you are confused by some of the terminology here. The slice object created by createSlice (your todoReducer variable) is an object which contains both a reducer and actions.
The reducer is a single function which takes the previous state and an action and returns the next state. The only place in your app when you use the reducer is to create the store (by calling createStore or configureStore).
An action in redux are the things that you dispatch. You will use these in your components. In your code there are four action creator functions: two which you created with createAsyncThunk and two which were created by createSlice. Those two will be in the actions object todoReducer.actions.
Exporting Individually
You can export each of your action creators individually and import them like:
import {fetchTodos, descriptionChanged} from "./path/file";
Your fetchTodos and addTodos are already exported. The other two you can destructure and export like this:
export const {descriptionChanged, descriptionCleared} = todoReducer.actions;
You would call them in your components like:
dispatch(fetchTodos())
Exporting Together
You might instead choose to export a single object with all of your actions. In order to do that you would combine your thunks with the slice action creators.
export const todoActions = {
...todoReducer.actions,
fetchTodos,
addTodos
}
You would import like this:
import {todoActions} from "./path/file";
And call like this:
dispatch(todoActions.fetchTodos())
I have seen solutions for clearing/resetting the store after logout but did not understand how to implement the same functionality for the following way of setting up the redux store.
Store.js:
import { configureStore, getDefaultMiddleware } from '#reduxjs/toolkit'
import authReducer from './ducks/authentication'
import snackbar from './ducks/snackbar'
import sidebar from './ducks/sidebar'
import global from './ducks/global'
import quickView from './ducks/quickView'
import profileView from './ducks/profileView'
const store = configureStore({
reducer: {
auth: authReducer,
snackbar,
sidebar,
global,
quickView,
profileView,
},
middleware: [...getDefaultMiddleware()],
})
export default store
Here is how all the reducers implemented using createAction and createReducer from #reduxjs/toolkit.
snackbar.js:
import { createAction, createReducer } from '#reduxjs/toolkit'
export const handleSnackbar = createAction('snackbar/handleSnackbar')
export const openSnackBar = (
verticalPosition,
horizontalPosition,
message,
messageType,
autoHideDuration = 10000
) => {
return async dispatch => {
dispatch(
handleSnackbar({
verticalPosition,
horizontalPosition,
message,
autoHideDuration,
messageType,
isOpen: true,
})
)
}
}
export const closeSnackbar = () => {
return dispatch => {
dispatch(handleSnackbar({ isOpen: false }))
}
}
const initialState = {
verticalPosition: 'bottom',
horizontalPosition: 'center',
message: '',
autoHideDuration: 6000,
isOpen: false,
messageType: 'success',
}
export default createReducer(initialState, {
[handleSnackbar]: (state, action) => {
const {
isOpen,
verticalPosition,
horizontalPosition,
message,
autoHideDuration,
messageType,
} = action.payload
state.isOpen = isOpen
state.verticalPosition = verticalPosition
state.horizontalPosition = horizontalPosition
state.message = message
state.autoHideDuration = autoHideDuration
state.messageType = messageType
},
})
As per Dan Abramov's answer, create a root reducer which will simply delegate the action to your main or combined reducer. And whenever this root reducer receives a reset type of action, it resets the state.
Example:
const combinedReducer = combineReducers({
first: firstReducer,
second: secondReducer,
// ... all your app's reducers
})
const rootReducer = (state, action) => {
if (action.type === 'RESET') {
state = undefined
}
return combinedReducer(state, action)
}
So, if you have configured your store with #reduxjs/toolkit's configureStore, it might look like this:
import { configureStore } from '#reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export default configureStore({
reducer: {
counter: counterReducer,
// ... more reducers
},
});
where configureStore's first parameter reducer accepts a function (which is treated as root reducer) or an object of slice reducers which is internally converted to root reducer using combineReducers.
So, now instead of passing object of slice reducers (shown above), we can create and pass root reducer by ourselves, here is how we can do it:
const combinedReducer = combineReducers({
counter: counterReducer,
// ... more reducers
});
Now, lets create a root reducer which does our reset job when needed:
const rootReducer = (state, action) => {
if (action.type === 'counter/logout') { // check for action type
state = undefined;
}
return combinedReducer(state, action);
};
export default configureStore({
reducer: rootReducer,
middleware: [...getDefaultMiddleware()]
});
Here is CodeSandbox
I wanted to extend Ajeet's answer so that it is accessible to those who want complete type safety throughout their Redux store.
The key differences are that you need to declare a RootState type, which is documented in the RTK docs
const combinedReducer = combineReducers({
counter: counterReducer
});
export type RootState = ReturnType<typeof combinedReducer>;
And then in your rootReducer, where you are executing your logout function, you want to maintain type safety all the way down by giving the state param the RootState type, and action param AnyAction.
The final piece of the puzzle is setting your state to an empty object of type RootState instead of undefined.
const rootReducer: Reducer = (state: RootState, action: AnyAction) => {
if (action.type === "counter/logout") {
state = {} as RootState;
}
return combinedReducer(state, action);
};
I forked Ajeet's answer on CodeSandbox, added the required types, and you can view it here.
If you're looking to reset each slice to its initial state (unlike setting the entire state to an empty object) you can use extraReducers to respond to a logout action and return the initial state.
In auth.tsx:
const logout = createAction('auth/logout')
In foo.tsx:
const initialState = {
bar: false,
}
const fooSlice = createSlice({
name: 'foo',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(logout, () => {
return initialState
})
},
})
A simplified example with two reducers:
// actions and reducer for state.first
const resetFirst = () => ({ type: 'FIRST/RESET' });
const firstReducer = (state = initialState, action) => {
switch (action.type) {
// other action types here
case 'FIRST/RESET':
return initialState;
default:
return state;
}
};
// actions and reducer for state.second
const resetSecond = () => ({ type: 'SECOND/RESET' });
const secondReducer = (state = initialState, action) => {
switch (action.type) {
// other action types here
case 'SECOND/RESET':
return initialState;
default:
return state;
}
};
const rootReducer = combineReducers({
first: firstReducer,
second: secondReducer
});
// thunk action to do global logout
const logout = () => (dispatch) => {
// do other logout stuff here, for example logging out user with backend, etc..
dispatch(resetFirst());
dispatch(resetSecond());
// Let every one of your reducers reset here.
};
The simple solution - just add a reducer like this...
resetList: (state) => {
return (state = []);
},
... and call it with a button:
const handleResetList = () => {
dispatch(resetList());
};
return (
<div>
<div>List</div>
<button onClick={handleResetList}>Reset</button>