Callback after state update in Reducer of Redux Toolkit - reactjs

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

Related

How to add an object to an array using redux toolkit

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);
},
},
});

Can't access to redux-toolkit state

I'm trying to learn redux-toolkit but I struggle a little bit to access my data after set it.
I tried to create a registration form, once the form is filled in I set the values in my store and try to access them through a hook on another page.
However, I only get the initial state and not the values set earlier.
This is my code:
auth.ts
const tokenManager = new TokenManager();
const userManager = new UserManager();
export const auth = createSlice({
name: "auth",
initialState: {
token: "",
user: null
},
reducers: {
get: (state) => state,
setUser: (state, action) => {
userManager.setUser(action.payload);
state.user = action.payload;
},
setToken: (state, action) => {
if (action.payload !== tokenManager.getToken()) {
tokenManager.setToken(action.payload);
}
state.token = action.payload;
}
},
});
export const authActions = auth.actions;
export const authReducer = auth.reducer;
store.ts
export const store = configureStore({
reducer: {
auth: authReducer,
},
});
useUser.ts
export const useUser = () => {
return useSelector((state: any) => state.auth.user);
}
SignupView.tsx
function handleOnSubmit(values: SignupValues) {
dispatch(authActions.setToken("ahohegogogao"));
dispatch(authActions.setUser(values));
navigate("/");
}
When I try to access the value that useUser gives me, I end up with the initial state of my reducer but i don't understand why.
Does anyone have any idea where the problem might come from?
Thanks for your help.
------- EDIT :
Looks like I need to do that :
initialState: {
token: tokenManager.getToken(),
user: userManager.getUser()
},
To be able to retrieve my state when i change the url...

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)
});

Is there a way to access global state in createSlice?

Here is an example:
const user = createSlice({
name: 'user',
initialState: { name: '', age: 20 },
reducers: {
setUserName: (state, action) => {
state.name = action.payload // mutate the state all you want with immer
}
},
// "map object API"
extraReducers: {
[counter.actions.increment]: (state, action) => {
state.age += 1
}
}
})
Can I get access to the counter state?
Let's say I want to increment age only when the counter is 30. Otherwise, I would need to listen when the count is changing in useEffect hook and dispatch some action that will handle age increment (?).
In other words, what's the best way to compute the slice of state based on the current global state using redux-toolkit?
This is covered in the Redux FAQ entry on sharing state between reducers.
Pasting the key points:
If a reducer needs to know data from another slice of state, the state tree shape may need to be reorganized so that a single reducer is handling more of the data.
You may need to write some custom functions for handling some of these actions. This may require replacing combineReducers with your own top-level reducer function. You can also use a utility such as reduce-reducers to run combineReducers to handle most actions, but also run a more specialized reducer for specific actions that cross state slices.
Async action creators such as redux-thunk have access to the entire state through getState(). An action creator can retrieve additional data from the state and put it in an action, so that each reducer has enough information to update its own state slice.
I think you could use thunkAPI and extrareducers like below, assuming that you have an auth slice which has the current user object:
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import ordersService from "./ordersService";
const initialState = {
orders: [],
isError: false,
isSuccess: false,
isLoading: false,
message: "",
};
//get orders
export const getOrders = createAsyncThunk(
"orders/getOrders",
async (__, thunkAPI) => {
try {
const userId = thunkAPI.getState().auth.user.id
return await ordersService.getOrders(userId);
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const ordersSlice = createSlice({
name: "orders",
initialState,
reducers: {
reset: (state) => {
state.isError = false;
state.isSuccess = false;
state.isLoading = false;
state.message = "";
},
},
extraReducers: (builder) => {
builder
.addCase(getOrders.pending, (state) => {
state.isLoading = true;
})
.addCase(getOrders.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.orders = action.payload;
})
.addCase(getOrders.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
state.orders = [];
});
},
});
export const { reset, setOrder } = ordersSlice.actions;
export default ordersSlice.reducer;

Resources