How to call AsyncThunk method from Reducer method in the same Slice - reactjs

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.

Related

Redux toolkit how to call action from other slice on one action fullfilled

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.

Trying to pass action.payload from one slice to another different slice

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.

Adding function in Slice for redux

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]

redux to dispatch response and a return message

In my action i am dispatching the type and the payload but what if i also want the res.status and a return JSON message to be included into my props. How would i do so in my action and reducer?
action
export const fetchUserPosts = () => (dispatch) => {
fetch(`${currentPort}/user/recipes`,
{
withCredentials: true,
credentials: 'include',
})
.then((res) => {
if (res.status !== 401) return res.json().then((data) => data);
return { message: { msgBody: 'UnAuthorized' }, msgError: true };
})
.then((posts) => dispatch({
type: FETCH_USER_POSTS,
payload: posts,
}));
};
reducer
export default function (state = initialState, action) {
switch (action.type) {
case FETCH_USER_POSTS:
return {
...state,
fetchUsersPosts: action.payload,
};
default:
return state;
}
}
You can combine multiple items into a single payload. I would create different actions for success and error. Using the same action complicates the reducers logic. It's also easier to work with async/await then with nested promises.
This is a working example that uses SpaceX open API:
const FETCH_USER_POSTS_SUCCESS = 'FETCH_USER_POSTS_SUCCESS'
const FETCH_USER_POSTS_FAILED = 'FETCH_USER_POSTS_FAILURE'
const fetchPostSuccessAction = (payload) => ({
type: 'FETCH_USER_POSTS_SUCCESS',
payload,
})
const fetchPostFailureAction = (payload) => ({
type: 'FETCH_USER_POSTS_FAILURE',
payload,
})
const fetchUserPosts = () => async dispatch => {
const res = await fetch('https://api.spacexdata.com/v3/launches/latest');
if (res.status !== 401) {
const { ships: posts } = await res.json();
dispatch(fetchPostSuccessAction({
posts,
status: res.status,
}))
} else {
dispatch(fetchPostFailureAction({
message: { msgBody: 'UnAuthorized' },
}))
}
};
fetchUserPosts()(console.log)
The reducer can handle the object by destructuring it, and the properties to the new state in any way you need. You can also change other properties, for example changing errMsg to true or false according to the action's type:
export default function (state = initialState, { type, payload }) {
switch (type) {
case FETCH_USER_POSTS_SUCCESS: {
const { posts, status } = payload;
return {
...state,
status,
fetchUsersPosts: posts,
msgError: false,
message: null
};
}
case FETCH_USER_POSTS_FAILURE: {
const { message } = payload;
return {
...state,
status: 401,
fetchUsersPosts: null,
msgError: true,
message
};
}
default:
return state;
}
}
If I am following correctly you are using this action inside of a component to send a fetch. You don't have access to the components props with the reducer. You can send the http request in the component and use that to store the response in the state. Or use connect from 'react-redux' package to map the redux store to access the fetch result.
import { connect } from 'react-redux'
const component = props => {
//to access redux state in component use props.myprop
return <div>{props.myprops.title}</div>
}
const mapStateToProps = state => {
return{
myprop: state.fetchUsersPosts
}
}
export default connect(mapStateToProps)(component)
If this was what you were looking for you can learn more at https://react-redux.js.org/api/connect

Actions must be plain objects. Use custom middleware for async actions. react-redux

I am new with reac-redux, I am trying to get collection from Firestore but now when firebase returns the data and I try to map the info to storage through redux-observable I get an error "Actions must be plain objects. Use custom middleware for async actions." I guess it must be about the epic configuration, then I leave the code
Epic
import { getFirestore } from "redux-firestore";
import {
GET_DOCUMENTS,
GET_COLLECTIONS_BY_DOCUMENT,
setStatus,
getDocumentsSuccess,
getDocumentsFailed
} from "../actions/dataActions";
import { switchMap } from "rxjs/operators";
import { ofType } from "redux-observable";
import { concat, of } from "rxjs";
export default function dataEpics(action$) {
const getFS = getFirestore();
return action$.pipe(
ofType(GET_DOCUMENTS, GET_COLLECTIONS_BY_DOCUMENT),
switchMap(action => {
if (action.type === GET_DOCUMENTS) {
return concat(
of(setStatus("pending")),
getFS
.collection("en")
.get()
.then(querySnapshot => {
let listDocumentIds = [];
querySnapshot.forEach(doc => {
listDocumentIds.push(doc.id);
getDocumentsSuccess(listDocumentIds);
});
})
.catch(err => of(getDocumentsFailed(err)))
);
}
})
);
}
Action
export const SET_STATUS = "SET_STATUS";
export const GET_DOCUMENTS = "GET_DOCUMENTS";
export const GET_DOCUMENTS_SUCCESS = "GET_COLLECTIONS_SUCCESS";
export const GET_DOCUMENTS_FAILED = "GET_COLLECTIONS_FAILED";
export function setStatus(status) {
return {
type: SET_STATUS,
payload: status
};
}
export function getDocumentsSuccess(documents) {
return {
type: GET_DOCUMENTS_SUCCESS,
payload: documents
};
}
reducer
import {
GET_DOCUMENTS_SUCCESS,
GET_DOCUMENTS_FAILED,
SET_STATUS
} from "../actions/dataActions";
const initState = {
status: "idle", // "idle" | "logout" | "pending" | "login" | "success" | "failure";
documents: [],
collections: []
};
const dataReducers = (state = initState, action) => {
switch (action.type) {
case SET_STATUS: {
return {
...state,
status: action.payload
};
}
case GET_DOCUMENTS_SUCCESS: {
return {
...state,
status: "success",
documents: action.payload
};
}
default:
return state;
}
};
export default dataReducers;
I think the error is in the epic, I have more code in a similar way
Thanks for help me.
I found the solution, the error was in the epic, I was trying to call the action inside querySnapshot, this is no possible, then I move the getDocumentsSuccess after
getFS
.collection(action.payload.language + "_" + "MachinesAndEquipment")
.get()
.then(querySnapshot => {
let listDocumentIds = [];
querySnapshot.forEach(doc => {
listDocumentIds.push(doc.id);
});
getDocumentsSuccess(listDocumentIds);

Resources