How to add an object to an array using redux toolkit - reactjs

I want to add an object to an array reducer
This is the object am trying to add
const id = 1;
const type = 'deposit';
dispatch(addTransaction({id, type}))
This is my reducer
import { createSlice } from '#reduxjs/toolkit';
const initialState = {
transactions: [],
};
const transactionSlice = createSlice({
name: 'transaction',
initialState,
reducers: {
addTransaction: (state, action) => {
state.transactions = [...state.transactions, action.payload];
},
},
});
const { actions, reducer } = transactionSlice;
export const {
addTransaction,
} = actions;
export default reducer;
Anytime I dispatch the object data, It updates the previous data, instead adding a new object in my transaction array.

The problem is that you are not copying the transactions state before adding the new object to it.
so do this instead:
reducers: {
addTransaction: (state, action) => {
state.transactions = [...state.transactions, action.payload];
}
this way you copy all objects inside transactions array to a new array, plus the new object you want to add.

try :
change
const initialState = {
transactions: {
items: [],
},
};
to
const initialState = {
transactions:[]
};
{ state.transactions: [...transactions, action.transaction] };
or keep initial state but change reducer to:
state.transactions.items = [...state.transactions.items, action.payload];
and according to redux toolkit docs there is no return in reducer

Any of these two options should work:
Returning the updated state
const transactionSlice = createSlice({
name: 'transaction',
initialState,
reducers: {
addTransaction: (state, action) => {
return [...state.transactions, action.payload];
},
},
});
Or for future scalability, make your initial state to be an object and add a key for your transactions, in this case items
const initialState = {
transactions: {
items: [],
},
};
const transactionSlice = createSlice({
name: 'transaction',
initialState,
reducers: {
addTransaction: (state, action) => {
transactions.items = [...state.transactions.items, action.payload];
},
},
});
With this object approach you can take advantage of Immer and get rid of having to spread the state just pushing the incoming payload
const transactionSlice = createSlice({
name: 'transaction',
initialState,
reducers: {
addTransaction: (state, action) => {
transactions.items.push(action.payload);
},
},
});

Related

Callback after state update in Reducer of Redux Toolkit

Problem: I want to write some code more efficiently.
Below you will find my full code example of a Redux Toolkit slice
import { createSlice } from '#reduxjs/toolkit';
import { setCookie } from '../../utils/storageHandler';
const initialState = {
name: null,
age: null
}
const formSlice = createSlice({
name: 'formSlice',
initialState: initialState,
reducers: {
setName(state, action) {
state.name = action.payload;
setCookie('name', action.payload);
},
setAge(state, action) {
state.age = action.payload;
setCookie('age', action.payload);
}
}
});
export const { setName, setAge, } =
formSlice.actions;
export default formSlice.reducer;
I do not want to write setCookie(name, value) each time I run a reducer.
Rather I want to write it once and call it each time a reducer function has been called.
I would pass a payload of:
{type: string, value: string}
and then run a callback from every reducer and call the setCookie(...) function as follows:
setCookie(action.payload.type, action.payload.value)
Ideally I would write this code within the slice as so
const formSlice = createSlice({
name: 'formSlice',
initialState: initialState,
reducers: {
setName(state, action) {
state.name = action.payload;
},
setAge(state, action) {
state.age = action.payload;
}
},
callback(action) {
setCookie(action.payload.type, action.payload.value)
}
});
Is there a way of achieving this?
Or maybe an another way of thinking?
My main goal is to have form values stored within cookies, so as to prefill the registration form each time a customer visits it.
There is no authentication.
I will not write cookie storage logic within a component!
Thank you for your attention and I wish you a pleasant day :)
Have you looked into passing a middleware function? You can create your own custom middleware like this
const saveForm = (api: MiddlewareAPI) => (next) => (action) => {
if (action.type === 'setName') { ... }
if (action.type === 'setAge') { ... }
return next(action);
}
and then concat it to the default
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(saveForm),
})
https://redux-toolkit.js.org/api/configureStore#middleware

Redux toolkit update state in thunk

I have an initial state like this.
const initialState = {
loading: false,
products: []
}
For the get products, I use thunk and I fetch datas from an API. loading field describes status of API call. If loading is true, i show progress in my component.
export const getProducts = createAsyncThunk('product/getProducts ',
async (payload, { rejectWithValue }) => {
try {
//fetch from somewhere
}
catch (e) {
return rejectWithValue(e.message)
}
}
)
const productSlice = createSlice({
name: "product",
initialState,
reducers: {
setLoading: (state, action) => {
state.loading = action.payload;
}
},
})
In a component first dispatch for loading and then getProducts.
// In some component
dispatch(setLoading(true))
dispatch(getProducts())
My question is, can I only call for getProducts and update loading state inside that function?
//In some component
dispatch(getProducts())
Yes you can. This is very simple
Update your createSlice section like this
const productSlice = createSlice({
name: "product",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getProducts.pending, (state) => {
state.loading = true;
});
builder.addCase(getProducts.fulfilled, (state) => {
state.loading = false;
});
},
})
You used createAsyncThunk and it is built in already to handle pending/fulfilled/rejected state with extraReducers in createSlice.
You can also handle the rejected state if fetch fail.
const productSlice = createSlice({
name: 'product',
initialState,
extraReducers: (builder) => {
builder.addCase(getProducts.pending, (state, action) => {
state.isLoading = true;
})
builder.addCase(getProducts.fulfilled, (state, action) => {
state.isLoading = true;
state.products = action.payload
})
builder.addCase(getProducts.rejected, (state, action) => {
state.isLoading = false;
})
},
})
To use it, will be enough to simple call dispatch(getProducts()) and extraReducers with thunk take care of the rest.

Can I change state from one slice to another slice in Redux?

I have 2 slices, the first of which contains state errors and the second of which contains logic.
Is it possible to change the value state in the error slice from a logical slice?
Error slice
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
error: false,
};
export const errorSlice = createSlice({
name: "error",
initialState,
reducers: {
setError: (state, action) => {
state.error = action.payload;
},
},
});
export const { setError } = errorSlice.actions;
export default errorSlice.reducer;
Logical slice
import { createSlice } from "#reduxjs/toolkit";
export const doSomething = (data) => {
return (dispatch) => {
dispatch(setData(data.text))
// here I want dispatch setError from errorSlice
// dispatch(setError(data.error))
};
};
const initialState = {
data: null,
};
export const logicalSlice = createSlice({
name: "logical",
initialState,
reducers: {
setData: (state, action) => {
state.error = action.payload;
},
},
});
export const { setData } = logicalSlice.actions;
export default logicalSlice.reducer;
And I need to run it from a component with a single dispatch
dispatch(doSomething(data))
Is there such a possibility?
Thank you!

dispatch in redux reducer (project created by tookit)

I want to dispatch in changeCategory reducer. how should I do it?
I am using create-react-app tool
Thanks
export const searchParamsSlice = createSlice({
name: 'searchParams',
initialState,
reducers: {
changeLocation: (state, action) => {
state.location = action.payload;
},
changeCategory: (state, action) => {
state.category = action.payload;
const dispatch = useDispatch()
dispatch(fetchResturantsAsync({ city: state.location, category: state.category, searchKey: state.seachText, page: 0, size: 10 }))
},
}
You cannot dispatch in a reducer - it is one of the three Redux core principles that reducers have to be side-effect-free.
If you want to react to another action by dispatching a new one, you could always use the listenerMiddleware provided by RTK Query, or write a thunk action creator that dispatches both of those actions after each other.
Here is how you should do it
create this Middleware
export const asyncDispatchMiddleware = store => next => action => {
let syncActivityFinished = false;
let actionQueue = [];
function flushQueue() {
actionQueue.forEach(a => store.dispatch(a)); // flush queue
actionQueue = [];
}
function asyncDispatch(asyncAction) {
actionQueue = actionQueue.concat([asyncAction]);
if (syncActivityFinished) {
flushQueue();
}
}
const actionWithAsyncDispatch =
Object.assign({}, action, { asyncDispatch });
const res = next(actionWithAsyncDispatch);
syncActivityFinished = true;
flushQueue();
return res;
};
Then add it to your store
export const store = configureStore({
reducer: {
counter: counterReducer,
//....
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(asyncDispatchMiddleware),
});
Then in your reducer do something like this
changeWeekday: (state, action) => {
state.weekName = action.payload;
action.asyncDispatch(fetchSomethingAsync({
weekName: state.weekName
}))
}
I had changeWeekday in my code, in your case it could be any reducer.

When to update state or return action.payload for redux thunk extra reducers?

I understand the first way to update the store, which is concatenating the new post to the existing list of posts. But how does returning the list of users set the state to the list like in the second image? I only ask because it seems I can't achieve the same for users by doing something similar to what was done with posts.
Also here is the link to the redux tutorial: https://redux.js.org/tutorials/essentials/part-5-async-logic
Updating posts list using concat:
updating user list by returning payload
If you want to mutate the whole state, return the data to replace the whole state.
import { createSlice, createAsyncThunk, configureStore } from '#reduxjs/toolkit';
interface User {
name: string;
}
const initialState = [] as User[];
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
return [{ name: 'teresa teng' }];
});
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
// state = action.payload;
return action.payload;
});
},
});
const store = configureStore({ reducer: { users: usersSlice.reducer } });
store.dispatch(fetchUsers());
store.subscribe(() => {
console.log(store.getState());
// { users: [] }, if you use `state = action.payload`
// { users: [ { name: 'teresa teng' } ] }, if you use `return action.payload`
});
If you want to mutate some fields in the state, you can directly assign values (Why? Because RTK use immer underly) ​​to the fields without returning.
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'success'
state.posts = state.posts.concat(action.payload)
});

Resources