What I am trying to achieve is sending action payload from one slice to another and I have been stuck several hours trying to do so.
I have tried accessing the global store but the problem is I am getting errors on doing so
I am using redux-tool-kit to manage the state of my react application and I am trying to pass a payload from one slice to another, the following is my first slice:
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from 'axios';
import { clearAlert, displayIncorrectEmail } from "./features.js/Alert";
const initialState = {
user: user ? JSON.parse(user) : null,
isMember: false,
isLoading: true
}
This section still for the first slice
export const getRegisteredUser = createAsyncThunk('auth/getRegistrationRes', async (currentUser, thunkAPI) => {
try {
const response = await axios.post('/api/v1/auth/register', currentUser)
return response.data
} catch (error) {
// console.log(error.message)
thunkAPI.rejectWithValue(error.message)
}
})
export const getLoginUser = createAsyncThunk('auth/getLoginRes', async (currentUser, thunkAPI) => {
try {
const response = await axios.post('/api/v1/auth/login', currentUser)
thunkAPI.dispatch(displaySuccess())
setTimeout(() => {
thunkAPI.dispatch(clearAlert())
}, 3000);
return response.data
} catch (error) {
thunkAPI.dispatch(displayIncorrectEmail())
// console.log(error.response.data.msg);
thunkAPI.rejectWithValue(error.message)
//the below return is the action-payload I want to pass to another slice
return error.response.data.message
//
}
})
const authenticationSlice = createSlice({
name: 'auth',
initialState,
reducers: {
},
extraReducers: {
// login user reducers
[getLoginUser.pending]: (state) => {
state.isLoading = true;
},
[getLoginUser.fulfilled]: (state, action) => {
state.isLoading = false;
// console.log(action.payload.getState());
// action.payload.load = true
state.user = action.payload.user
},
[getLoginUser.rejected]: (state) => {
state.isLoading = false;
state.user = null
},
}
})
export const { registerUser, loginUser } = authenticationSlice.actions
export default authenticationSlice.reducer
This is the second slice is the code below
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
showAlert: false,
alertText: '',
alertType: '',
}
const alertSlice = createSlice({
name: 'alert',
initialState,
reducers: {
displayIncorrectEmail: (state, action) => {
state.showAlert = !state.showAlert
//I want to pass the action.payload to this instead of hard-coding it to 'incorrect email' //below
state.alertText = 'incorrect email'
//the state.alertText above
state.alertType = "danger"
},
clearAlert: (state) => {
// setTimeout(() => {
state.showAlert = !state.showAlert;
// }, 4000);
}
}
})
export const { displayDanger, clearAlert, displaySuccess, displayIncorrectEmail } = alertSlice.actions
export default alertSlice.reducer
Kindly help if you have an idea on how to sort this.
cheers.
Just add an extraReducer for getLoginUser.rejected to the second slice as well. You can add that to the extraReducers of as many slices as you want to.
By the way, you really should not be the map object notation of extraReducers, but the extraReducers "builder callback" notation. The object notation you are using is soon going to be deprecated and we have been recommending against it in the docs for a long time.
Related
I have a async action name editLoginIdData() in loginsIdSlice.js,
which i am dispatching from certain component, which edits the data in mongoDB database, then when the action is fullfilled, i mutate the state in extraReducers
editLoginIdData.fulfilled.
But now what i want to do that whenever the editLoginIdData action is fullfilled
i want to also add the updated(which i will get from server responese -> updatedData at editLoginIdData()) data to activitiesData state, which i am handling in activitiesSlice.js
So basically is there a way that when editLoginIdData action is fullfilled we can
dispatch somehow mutate the state in other slice.
One approach i have taken is to import the editLoginIdData() action in activitiesSlice.js and then creating a extraReducer with editLoginIdData.fullfilled
and mutating activitiesData state.
I have done the above approach and seems its working correctly.
But there is a catch, like how show i get the response data at editLoginIdData()
to passed to activitiesSlice.js because i will required that updated data.
if the above appraoch is not correct, then how should i do it
loginsIdSlice.js
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import * as api from "../../api"
const initialState = {
loginsIdData: [],
}
export const fecthLoginIdsData = createAsyncThunk("loginIds/fetch", async ({ user_id }, { getState }) => {
const res = await api.fetchUserLoginIds(user_id);
console.log(res);
const { data } = res;
data.reverse();
return data;
});
export const addNewLoginIdData = createAsyncThunk("loginIds/add", async ({ data, user_id }, { getState }) => {
const res = await api.addNewLoginIdA(data, user_id)
const { loginIdsArray } = res.data;
return loginIdsArray[loginIdsArray.length - 1];
});
export const editLoginIdData = createAsyncThunk("loginIds/edit", async ({ updatedData, login_id }, { getState }) => {
const res = await api.editLoginId(login_id, updatedData);
// console.log(updatedData);
return updatedData;
});
export const deleteLoginData = createAsyncThunk("loginIds/delete", async ({ login_id, user_id }, { getState }) => {
const res = await api.deleteLoginId(login_id, user_id);
// console.log(res);
const { data } = res;
// console.log(data);
return data.reverse();
});
//* Slice
const loginsIdSlice = createSlice({
name: 'loginsId',
initialState: initialState,
extraReducers: (builder) => {
builder.
addCase(fecthLoginIdsData.fulfilled, (state, action) => {
return {
...state,
loginsIdData: action.payload
};
}).
addCase(addNewLoginIdData.fulfilled, (state, action) => {
return {
...state,
loginsIdData: [action.payload, ...state.loginsIdData]
};
}).
addCase(editLoginIdData.fulfilled, (state, action) => {
const newArray = state.loginsIdData.map((loginId) => {
if (loginId._id === action.payload._id) {
return action.payload;
} else {
return loginId;
}
});
return {
...state,
loginsIdData: newArray,
};
}).
addCase(deleteLoginData.fulfilled, (state, action) => {
return {
...state,
loginsIdData: action.payload
};
})
}
})
export const { deleteLoginId, editLoginId } = loginsIdSlice.actions;
export default loginsIdSlice.reducer;
activitiesSlice
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import * as api from "../../api"
import { editLoginIdData } from "../loginsId/loginsIdSlice"
const initialState = {
activitiesData: [],
}
const activitiesSlice = createSlice({
name: 'activities',
initialState: initialState,
extraReducers: (builder) => {
builder.
addCase(editLoginIdData.fullfilled, (state, action) => {
console.log("ss")
return {
...state,
activitiesData: []
};
})
}
})
export default activitiesSlice.reducer;
Is there a way that when editLoginIdData action is fullfilled we can
dispatch somehow mutate the state in other slice.
I have a slice using reduxjs/toolkit, state holds a ServiceRequest object and a ServiceRequest array.
What I would like to achieve is; On loading a component I would like to dispatch a call to a reducer which checks, by id, if the ServiceRequest already exists in the array, if so, populate the ServiceRequest with the found object, if not, call an AsyncThunk method in the same slice to retrieve it from a WebAPI.
It is the calling of the AsyncThunk method from within the reducer or reducer method that I can't figure out. Maybe it shouldn't be done this way, but it seems like a nice spot to keep everything together.
How can I achieve this?
This is what I have so far: (you'll see the spots where I think the AsyncThunk method should be called is commented out)
import { createAsyncThunk, createSlice, PayloadAction } from "#reduxjs/toolkit";
import { ServiceRequest } from "./models/ServiceRequest.interface";
import csmRequestDataService from "./services/csmRequestDataService";
interface AsyncState {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
}
interface CSMState extends AsyncState {
serviceRequest: ServiceRequest | null;
serviceRequests: ServiceRequest[];
}
const initialState: CSMState = {
isLoading: false,
isSuccess: false,
isError: false,
serviceRequest: null,
serviceRequests: [],
}
export const getServiceRequest = createAsyncThunk(
'csm/getServiceRequest',
async (serviceRequestId: number) => {
try {
console.log('getServiceRequest');
return await csmRequestDataService.getServiceRequest(serviceRequestId);
} catch (error) {
console.log('Error: ', error);
}
});
const getOpenedServiceRequests = (
serviceRequests: ServiceRequest[],
serviceRequestId: number
) => {
const serviceRequest = serviceRequests.find(
(tsr) => tsr.ServiceRequestId === serviceRequestId
) || null;
/*
if (serviceRequest == null) {
console.log('GET REQUEST FROM API');
getServiceRequest(serviceRequestId);
} else {
console.log('GOT REQUEST FROM STORE')
}
*/
return serviceRequest;
};
export const csmRequestDataSlice = createSlice({
name: ' csmRequestData',
initialState,
reducers: {
retrieveServiceRequest: (state, action: PayloadAction<number>) => {
const serviceRequest = getOpenedServiceRequests(
state.serviceRequests,
action.payload
);
state.serviceRequest = serviceRequest;
/*
if (serviceRequest == null) {
console.log('GET REQUEST FROM API');
getServiceRequest(action.payload);
} else {
console.log('GOT REQUEST FROM STORE')
}
*/
}
},
extraReducers(builder) {
builder
.addCase(getServiceRequest.pending, (state) => {
state.isLoading = true;
})
.addCase(getServiceRequest.fulfilled, (state, action) => {
if (action.payload && action.payload.serviceRequest !== null) {
state.serviceRequests.push({ ...action.payload.serviceRequest });
state.serviceRequest = action.payload.serviceRequest;
}
state.isLoading = false;
state.isSuccess = true;
console.log('got request data');
})
.addCase(getServiceRequest.rejected, (state) => {
state.isLoading = false;
state.isError = true;
})
},
});
export const { retrieveServiceRequest } = csmRequestDataSlice.actions;
export default csmRequestDataSlice.reducer;
When uncommented the call to getServiceRequest in either spot it doesn't seem to do anything, I was thinking maybe doing a dispatch as I would from a component could possibly work, but unsure how to implement that.
Update:
The following is the service that is called from the slice:
const getServiceRequest = async (serviceRequestId: number) => {
const response = await axiosConfig.get(
'api/csm/getServiceRequest/' + serviceRequestId
);
if (response.data) {
console.log('got service request data in service');
return { serviceRequest: response.data };
}
return { serviceRequest: null };
}
const csmRequestDataService = {
getServiceRequest,
}
export default csmRequestDataService;
Update 2:
I have altered getOpenedServiceRequests:
export const getOpenedServiceRequests = createAsyncThunk(
'csm/getOpenedServiceRequests',
async (
serviceRequestId: number,
{ dispatch, getState }
) => {
const state: any = getState();
const { serviceRequests } = state.csmRequestDataReducer;
let serviceRequest = serviceRequests.find(
(tsr: ServiceRequest) => tsr.ServiceRequestId === serviceRequestId
) || null;
if (!serviceRequest) {
const payloadAction: any = await dispatch(getServiceRequest(serviceRequestId));
serviceRequest = payloadAction.payload.serviceRequest
console.log('***********Retieved SeriveRequest from API');
} else {
console.log('***********Retieved SeriveRequest from array');
}
return { serviceRequest: serviceRequest };
}
);
As you can see I have added async/await and unpacked the getServiceRequest results.
The payloadAction results are as follows:
This works fine other than it feeling a bit messy with the unpacking, also I'll need to add a flag in the extra reducer to say whether the service request gets added to the ServiceRequests array.
What I'm thinking is, not calling dispatch(getServiceRequest(serviceRequestId)) but calling the service directly, then I won't need to unpack/repack the results, I'll also be able to set a flag in each result stating whether the service request should be added to the array
Reducer functions are to be considered pure, synchronous functions. They are synchronous functions of a previous state and an action, and return the next state. What you are looking, or asking, for is another asynchronous action that does the check and conditionally dispatches another asynchronous action.
Convert getOpenedServiceRequests into a Thunk and access the second argument, thunkAPI, to the payload creator function. Use getState to get the full state object, and dispatch to dispatch further actions like getServiceRequest.
import {
createAsyncThunk,
createSlice,
PayloadAction
} from "#reduxjs/toolkit";
import { ServiceRequest } from "./models/ServiceRequest.interface";
import csmRequestDataService from "./services/csmRequestDataService";
...
export const getServiceRequest = createAsyncThunk(
"csmRequestData/getServiceRequest",
async (serviceRequestId: number, { rejectWithValue }) => {
try {
return await csmRequestDataService.getServiceRequest(serviceRequestId);
} catch (error) {
console.warn('Error: ', error);
rejectWithValue(error);
}
},
);
export const getOpenedServiceRequests = createAsyncThunk(
"csmRequestData/getOpenedServiceRequests",
(serviceRequestId: number, { dispatch, getState }) => {
// get the current complete state
const state = getState();
// access into state to get the serviceRequests array
const { serviceRequests } = state.csmRequestData; // <-- * NOTE
const serviceRequest = serviceRequests.find(
(tsr) => tsr.ServiceRequestId === serviceRequestId
);
if (!serviceRequest) {
// No service request, dispatch action to get it
const { payload } = await dispatch(getServiceRequest(serviceRequestId));
return { serviceRequest: payload.serviceRequest };
}
// Fulfill with found service request
return { serviceRequest };
},
);
export const csmRequestDataSlice = createSlice({
name: 'csmRequestData',
initialState,
extraReducers(builder) {
builder
.addCase(getOpenedServiceRequests.pending, (state) => {
state.isLoading = true;
})
.addCase(getOpenedServiceRequests.fulfilled, (state, action) => {
const { serviceRequest } = action.payload;
if (serviceRequest) {
state.serviceRequests.push({ ...serviceRequest });
state.serviceRequest = serviceRequest;
}
state.isLoading = false;
state.isSuccess = true;
})
.addCase(getOpenedServiceRequests.rejected, (state) => {
state.isLoading = false;
state.isError = true;
});
},
});
export default csmRequestDataSlice.reducer;
*NOTE: Here you will need to access into the global state object, following the path created by how you combine your reducers and form the state tree.
Q1. We would like to receive additional data from the server when clicking the "Show More" button.
For example, it shows 5 at first, then shows 5 more when the button is pressed.
I implemented it as below using redux-saga.
But I'm not sure if using redux-saga is the right way to do this.
Q2. Also, I want to filter out only the data with type: special value among all the data received from the server.
However, the first data obtained through useSelector only has 5 data. (because of 'slice') Is there a way to handle this inside redux-saga?
dispatch(getCarsCount());
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
isLoading: false,
data: [],
error: null,
count: 5,
};
const carSlice = createSlice({
name: "cars",
initialState,
reducers: {
getCarsFetch: (state, action) => {
state.isLoading = true;
state.error = null;
},
getCarsSuccess: (state, action) => {
state.isLoading = false;
state.data = action.payload.slice(0, state.count);
},
getCarsFailure: (state, action) => {
state.isLoading = false;
state.error = action.payload;
},
getCarsCount: (state, action) => {
state.count = state.count + 5;
},
},
});
export const { getCarsFetch, getCarsSuccess, getCarsFailure, getCarsCount } =
carSlice.actions;
export default carSlice.reducer;
import { call, put, takeEvery } from "redux-saga/effects";
import { getCarsSuccess, getCarsFailure } from "./carSlice";
import { loadCarsApi } from "./api";
function* workGetCarsFetch() {
try {
const cars = yield call(loadCarsApi);
yield put(getCarsSuccess(cars));
} catch (error) {
yield put(getCarsFailure(error));
}
}
function* carSaga() {
yield takeEvery("cars/getCarsFetch", workGetCarsFetch);
yield takeEvery("cars/getCarsCount", workGetCarsFetch);
}
export default carSaga;
Please help me how I can introduce new function like getOrdersByCustomer in ordersSlice. I have provided full code of ordersSlice below. Please tell me what is extraReducers and how it works.
import { createSlice, createAsyncThunk, createEntityAdapter } from '#reduxjs/toolkit';
import axios from 'axios';
export const getOrders = createAsyncThunk('eCommerceApp/orders/getOrders', async () => {
const response = await axios.get('/api/e-commerce-app/orders');
const data = await response.data;
return data;
});
export const removeOrders = createAsyncThunk(
'eCommerceApp/orders/removeOrders',
async (orderIds, { dispatch, getState }) => {
await axios.post('/api/e-commerce-app/remove-orders', { orderIds });
return orderIds;
}
);
const ordersAdapter = createEntityAdapter({});
export const { selectAll: selectOrders, selectById: selectOrderById } = ordersAdapter.getSelectors(
state => state.eCommerceApp.orders
);
const ordersSlice = createSlice({
name: 'eCommerceApp/orders',
initialState: ordersAdapter.getInitialState({
searchText: ''
}),
reducers: {
setOrdersSearchText: {
reducer: (state, action) => {
state.searchText = action.payload;
},
prepare: event => ({ payload: event.target.value || '' })
}
},
extraReducers: {
[getOrders.fulfilled]: ordersAdapter.setAll,
[removeOrders.fulfilled]: (state, action) => ordersAdapter.removeMany(state, action.payload)
}
});
export const { setOrdersSearchText } = ordersSlice.actions;
export default ordersSlice.reducer;
In Addition
Also can you please tell me what I will do with this following code for my custom function getOrdersByCustomer.
export const { selectAll: selectOrders, selectById: selectOrderById } = ordersAdapter.getSelectors(
state => state.eCommerceApp.orders
);
because, in my component I have used like
const orders = useSelector(selectOrders);
You can introduce new (async) functions as you already have (I used the customerId as part of the url -> you could access it through the params in your backend):
export const getOrdersByCustomer = createAsyncThunk('eCommerceApp/orders/getOrdersByCustomer', async (customerId) => {
const response = await axios.get(`/api/e-commerce-app/orders/${customerId}`);
const data = await response.data;
return data;
});
Then you can handle the response in your extraReducer:
extraReducers: {
[getOrders.fulfilled]: ordersAdapter.setAll,
[removeOrders.fulfilled]: (state, action) => ordersAdapter.removeMany(state, action.payload),
[getOrdersByCustomer.fulfilled]: (state, action) =>
// set your state to action.payload
}
The extraReducers handle actions like async thunks. The createAsyncThunk function return 3 possible states (along with other things): pending, rejected or fulfilled. In your case you only handle the fulfilled response. You could also set your state with the other two options (in your case [getOrdersByCustomer.pending] or [getOrdersByCustomer.rejected]
I'm working on an app where I have multiple slices. I'm using createAsyncThunk for API calls and I like it cause it provides action creators for different state of API request, so that I can track loading state and errors within the reducer. But my question is, what if I want to have a separate reducer to track loading, error and success of my API calls how do I accomplish that with redux-toolkit
I know I can dispatch an action from within my createAsyncThunk function but it doesn't feel right and kinda defeats the purpose of the function itself. Also, side effects inside the reducer are considered to be a bad practice. So, I'm kinda confused at this point, I want to have just one Loader component in the root of the app that gets triggered when the loading state is true and it doesn't matter what exactly is loading
Here is an example of my current code:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
import { AxiosError } from 'axios'
import { masterInstance } from 'api'
import { GetAccessCodeParams, RegistrationStateType } from 'store/slices/registration/types'
export const getAccessCodeRequest = createAsyncThunk<void, GetAccessCodeParams, { rejectValue: { message: string } }>(
'registration/getAccessCodeRequest',
async ({ email }, { rejectWithValue }) => {
try {
await masterInstance.post(`/authorization/getAccessCodeWc`, { email })
} catch (err) {
let error: AxiosError = err
if (error) {
return rejectWithValue({
message: `Error. Error code ${error.response?.status}`,
})
}
throw err
}
}
)
const initialState: RegistrationStateType = {
isLoading: false,
error: null,
}
const registrationSlice = createSlice({
name: 'registration',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getAccessCodeRequest.fulfilled, (state) => {
state.isLoading = false
state.error = null
})
builder.addCase(getAccessCodeRequest.pending, (state) => {
state.isLoading = true
state.error = null
})
builder.addCase(getAccessCodeRequest.rejected, (state, action) => {
if (action.payload) {
state.error = {
message: action.payload.message,
}
} else {
state.error = action.error
}
state.isLoading = false
})
},
})
export const registrationReducer = registrationSlice.reducer
I want isLoading and error to be in a separate reducer
You could have a shared reducer matcher function.
// mySharedStuff.js
export const handleLoading = (action, (state) => {
state.loading = action.type.endsWith('/pending'); // or smth similar
});
export const handleError = (action, (state) => {
state.error = action.type.endsWith('/rejected'); // or smth similar
});
// mySlice.js
const mySlice = createSlice({
name: 'FOO',
initialState: {},
reducers: {},
extraReducers: builder => {
builder.addMatcher(handleLoading),
builder.addMatcher(handleError),
...