I ask for help with the Redux-saga, namely with refactoring the code below. Any ideas or explanations are welcome. The code below gets a list of hotels from the API on request with parameters from the function. It also has a check to see if data is currently being loaded or not at all. If the data is received, the action creator set Hotels is successfully executed. Thanks in advance for your reply.
hotels.js
export const getHotels = (cityName = 'London', date = currentDate(), days = 1, limit = 30) => {
return async (dispatch) => {
dispatch(setIsFetching(true))
dispatch(setIsEmpty(false))
try {
const response = await axios.get(`http://engine.hotellook.com/api/v2/cache.json?location=${cityName}¤cy=rub&checkIn=${date}&checkOut=${addDays(date, days)}&limit=${limit}`)
response.data.length === 0 ? dispatch(setIsEmpty(true)) : dispatch(setHotels(response.data))
}
catch (e) {
dispatch(setIsEmpty(true))
}
}
hotelsReducer.js
const SET_HOTELS = "SET_HOTELS";
const SET_IS_FETCHING = "SET_IS_FETCHING";
const SET_IS_EMPTY = "SET_IS_EMPTY";
const defaultState = {
hotels: [],
isFetching: true,
isEmpty: false,
};
export const hotelsReducer = (state = defaultState, action) => {
switch (action.type) {
case SET_HOTELS:
return {
...state,
hotels: action.payload,
isFetching: false,
};
case SET_IS_FETCHING:
return {
...state,
isFetching: action.payload,
};
case SET_IS_EMPTY:
return {
...state,
isEmpty: action.payload,
};
default:
return state;
}
};
export const setHotels = (results) => {return { type: SET_HOTELS, payload: results }};
export const setIsFetching = (bool) => {return { type: SET_IS_FETCHING, payload: bool }};
export const setIsEmpty = (bool) => {return { type: SET_IS_EMPTY, payload: bool }};
The saga is going to be very similar, you just need to replace the thunk with a new action that will trigger the saga:
import { put, takeLatest } from "redux-saga/effects";
function* fetchHotelsSaga() {
yield put(setIsFetching(true));
yield put(setIsEmpty(false));
try {
const response = yield axios.get(`http://engine.hotellook.com/api/v2/cache.json?location=${cityName}¤cy=rub&checkIn=${date}&checkOut=${addDays(date, days)}&limit=${limit}`);
response.data.length === 0
? yield put(setIsEmpty(true))
: yield put(setHotels(response.data));
} catch (e) {
yield put(setIsEmpty(true));
}
}
function* hotelsSaga() {
// FETCH_HOTELS is a type for the new action that will be dispatched instead of the thunk
yield takeLatest(FETCH_HOTELS, fetchHotelsSaga);
}
Related
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.
I'm using redux-saga to fetch an endpoint and want to present it on first page load using useEffect(). But mine is not fetching anything. The screen is blank and reduxDevTools is also not showing anything. I can't understand what did I miss.
My saga:
export function* watcherSaga() {
yield takeLatest("FETCH_TOP_NEWS_REQUEST", workerSaga);}
function fetchTopNews() {
return axios({
method: 'get',
url: 'https://newsapi.org/v2/top-headlines?country=us&apiKey=API_KEY'
});}
function* workerSaga() {
try{
const response = yield call(fetchTopNews);
const news = response.data.articles;
yield put({ type: "FETCH_TOP_NEWS_SUCCESS", news });
}
catch (error) {
yield put({ type: "FETCH_TOP_NEWS_ERROR", error });
}
}
I defined 3 actions:
const initialState = {
fetching: false,
error: null,
news: []
};
const NewsReducer = (state=initialState, action) => {
switch(action.type){
case types.fetchTopNewsRequest:
return { ...state, fetching: true, error: null };
case types.fetchTopNewsSuccess:
return { ...state, fetching: false, news: action.news[0] };
case types.fetchTopNewsError:
return { ...state, fetching: false, news: null, error: action.error };
default:
return state;
}
}
export default NewsReducer;
At last my component, I imported the fetchTopNewsRequest() action here:
const TopHeadline = (props) => {
const { news, getTopNews } = props;
useEffect(() => {
getTopNews();
}, [getTopNews]);
return (
<div className="newsItem">
<h1>Title: {news.title}</h1>
</div>
);}
const mapStateToProps= (state) => {
return {
news: state.news,
};
};
const mapDispatchToProps = (dispatch) => {
return {
getTopNews: () => dispatch( fetchTopNewsRequest() )
};
};
export default connect(mapStateToProps, mapDispatchToProps)(TopHeadline);
I'm trying to fetch only the articles.title.
DevTools shows it's successfully fetching the data:
Buy my states are not updating:
I am using redux to maintain my state in my react app, in my functional component i am dispatching some action in following way,
const handleClickOpen = () => {
console.log('in handle');
let data = {
reports_interval : currentSetting
};
dispatch(updateEmailSettingsAction({data: data, id: settings.id}))
};
in actions i have updateEmailSettingsAction and updateEmailSettingsActionSuccess showEmailSuccessAction which looks like following.
export const updateEmailSettingsAction = (settings) => {
console.log('in actrions');
return {
type: UPDATE_EMAIL_SETTINGS,
payload: settings
};
};
export const updateEmailSettingsActionSuccess = (settings) => {
console.log('success dispatched');
return {
type: UPDATE_EMAIL_SETTINGS_SUCCESS,
payload: settings
};
};
export const showEmailSuccessAction = (message) => {
return {
type: SHOW_EMAIL_SETTINGS_SUCCESS,
payload: message
}
};
Following are my sagas
const updateEmailSettings_request = async (data, id) =>
await updateEmailSettingsService(data, id)
.then(settings => settings)
.catch(error => error);
function* updateEmailSettingsFunction(payload) {
console.log('in func');
const {data, id} = payload.payload;
try {
const req = yield call(updateEmailSettings_request, data, id);
if (req.status === 200) {
console.log('in 200');
yield put(updateEmailSettingsActionSuccess(req.data));
yield put(showEmailSuccessAction('Success'));
}
else {
if (!req.data) {
yield put(showEmailSettingsAlert(req.message))
}else {
for (let key in req.data) {
yield put(showEmailSettingsAlert(req.data[key]));
}
}
}
} catch (error) {
yield put(showEmailSettingsAlert(error));
}
}
export function* updateEmailSettings() {
console.log('in final');
yield takeLatest(UPDATE_EMAIL_SETTINGS, updateEmailSettingsFunction)
}
and in following are my reducers.
const INIT_STATE = {
alertMessage: '',
settings: null,
successMessage: '',
showEmailSuccess: false,
};
export default (state = INIT_STATE, action) => {
switch (action.type) {
case UPDATE_EMAIL_SETTINGS_SUCCESS: {
return {
...state,
loader: false,
settings: action.payload,
}
}
case SHOW_EMAIL_SETTINGS_SUCCESS: {
console.log('in here reducer');
return {
...state,
loader: false,
showEmailSuccess: true,
successMessage: action.payload
}
}
}
i am accessing this showEmailSuccess in my component usinf useSelector in and showing success message in following way
{(showEmailSuccess && NotificationManager.success(successMessage) && show)}
<NotificationContainer/>
everything is working fine but this notification container is being shown twice, i have been stuck in this for quite sometime now but can't understand why.any help is appriciated.
case UPDATE_EMAIL_SETTINGS_SUCCESS: {
return {
...state,
loader: false,
showEmailSuccess: false, // Add this line
settings: action.payload,
}
}
I'm trying to create a function add product into cart with redux-react
and how can I get my product info from mongoDB into initialState?
this is how my product info looks like:
img_url1: "https://thebeuter.com/wp-content/uploads/2020/06/38-1.jpg"
price: 1290000
title: "BEUTER BACK2BACK ZIPPER WHITE JACKET"
here is my reducer:
import {
ADD_PRODUCT_BASKET,
GET_NUMBERS_BASKET
} from '../actions/type'
const initialState = {
basketNumbers: 0,
cartCost: 0,
products: {
}
}
export default (state = initialState, action) => {
switch (action.type) {
case ADD_PRODUCT_BASKET:
let addQuantity = {
...state.products[action.payload]
}
console.log(addQuantity)
return {
...state,
basketNumbers: state.basketNumbers + 1,
};
case GET_NUMBERS_BASKET:
return {
...state
};
default:
return state;
}
}
Here is my github if you want to look at my code:
https://github.com/nathannewyen/the-beuter
You solve your problem using redux-saga (or redux-thunk) by fetching your data from DB before rendering your page:
productBasket.js (with redux-saga)
import axios from 'axios';
import { action as createAction } from 'typesafe-actions';
import {
put, select, takeLatest,
} from 'redux-saga/effects';
export const FETCH_PRODUCT_BASKET = 'FETCH_PRODUCT_BASKET';
export const FETCH_PRODUCT_BASKET_SUCCESS = 'FETCH_PRODUCT_BASKET_SUCCESS';
export const FETCH_PRODUCT_BASKET_ERROR = 'FETCH_PRODUCT_BASKET_ERROR';
export const actionCreators = {
fetchProductBasket: () =>
createAction(FETCH_PRODUCT_BASKET),
fetchProductBasketSuccess: (products) =>
createAction(FETCH_PRODUCT_BASKET_SUCCESS, { products }),
fetchProductBasketError: (error) =>
createAction(FETCH_PRODUCT_BASKET_ERROR, { error }),
};
export const {
fetchProductBasket,
fetchProductBasketSuccess,
fetchProductBasketError
} = actionCreators;
export const initialState = {
isFetching: false,
isError: false,
basketNumber: 0,
products: []
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_PRODUCT_BASKET:
return {
...state,
isFetching: true,
isError: false
}
case FETCH_PRODUCT_BASKET_SUCCESS:
return {
...state,
isFetching: false,
isError: false,
products: action.payload.products
}
case FETCH_PRODUCT_BASKET_ERROR:
return {
...state,
isFetching: false,
isError: true
}
default:
return state;
}
}
export default reducer;
export function* basketSaga() {
yield takeLatest(FETCH_PRODUCT_BASKET, fetchProductBasketSaga);
}
function* fetchProductBasketSaga() {
try {
// here is code with fetching data
const { data } = yield axios.get('some address');
yield put(fetchProductBasketSuccess(data));
} catch (err) {
console.log(err);
yield put(fetchProductBasketError(err));
}
}
And after that dispatch fetchProductBasket action in useEffect scope in your component. You can show skeleton to user while your data is fetching.
I am new to redux and I am trying to make it work with my application, but I have problems with understanding how to work with async actions in it. I have action that is api call. This action should be called as soon as my other state is not empty. I do not get any mistakes but do not think that my action is called since the data is empty. Can anybody help to understand what I am doing wrong?
Here is my actions.js. The wordsFetchData is the action I need to call:
export function wordsFetchDataSuccess(items){
return{
type: 'WORDS_FETCH_DATA_SUCCESS',
items
};
}
export function wordsAreFetching(bool){
return{
type: 'WORDS_ARE_FETCHING',
areFetching: bool
}
}
export function wordsHasErrored(bool) {
return {
type: 'WORDS_HAS_ERRORED',
hasErrored: bool
};
}
export function wordsFetchData(parsed) {
return (dispatch) => {
dispatch(wordsAreFetching(true));
fetch('URL', {
method: "POST",
headers: {
"Content-type": "application/json"
},body: JSON.stringify({
words: parsed
})
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(wordsAreFetching(false));
return response;
})
.then((response) => response.json())
.then((items) => dispatch(wordsFetchDataSuccess(items)))
.catch(() => dispatch(wordsHasErrored(true)));
};
}
Here are my reducers:
export function word(state = [], action) {
switch (action.type) {
case 'WORDS_FETCH_DATA_SUCCESS':
return action.items;
default:
return state;
}
}
export function wordsAreFetching(state = false, action) {
switch (action.type) {
case 'WORDS_ARE_FETCHING':
return action.areFetching;
default:
return state;
}
}
export function wordsFetchHasErrored(state = false, action) {
switch (action.type) {
case 'WORDS_HAS_ERRORED':
return action.hasErrored;
default:
return state;
}
}
This is my componentDidMount function:
componentDidMount = (state) => {
this.props.fetchData(state);
};
This is the function after terminating which the action should be called:
parseInput = async () => {
console.log(this.state.textInput);
let tempArray = this.state.textInput.split(" "); // `convert
string into array`
let newArray = tempArray.filter(word => word.endsWith("*"));
let filterArray = newArray.map(word => word.replace('*', ''));
await this.setState({filterArray: filterArray});
await this.props.updateData(this.state.filterArray);
if (this.state.projectID === "" && this.state.entity === "")
this.dialog.current.handleClickOpen();
else
if (this.state.filterArray.length !== 0)
this.componentDidMount(this.state.filterArray);
};
These are the mapStateToProps and mapDispatchToProps functions.
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.wordsFetchHasErrored,
areFetching: state.wordsAreFetching
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: wordsFetchData
};
};
You only need one action for executing fetching (i.e WORDS_ARE_FETCHING), the rest of the cases (i.e WORDS_HAS_ERRORED & WORDS_FETCH_DATA_SUCCESS) can be handled inside your reducer.
Your action:
export function wordsAreFetching(){
return{
type: 'WORDS_ARE_FETCHING',
}
}
Your new reducer:
export function word(state = [], action) {
switch (action.type) {
case 'WORDS_ARE_FETCHING':
return {...state, error: false, areFetching: true};
case 'WORDS_FETCH_DATA_SUCCESS':
return {...state, items: action.payload , areFetching: false};
case 'WORDS_HAS_ERRORED':
return {...state, error: true, areFetching: false};
default:
return state;
}
Then you can trigger WORDS_FETCH_DATA_SUCCESS after you get the data from here:
export function wordsFetchData() {
try {
const response = await axios.get(YOUR_URL);
return dispatch({ type: WORDS_FETCH_DATA_SUCCESS, payload: response.data });
} catch (err) {
return dispatch({ type: WORDS_HAS_ERRORED });
}
}
Take a look at this example, it uses axios that can help you with async calls.
A couple of things:
No need to pass state into your componentDidMount, your mapDispatchToProps is not using it.
Here is a suggestion to structure those functions. They are a bit more concise and readable.
const mapStateToProps = ({items, wordsAreFetching, wordsFetchHasError}) => ({
items,
hasErrored: wordsFetchHasErrored,
areFetching: wordsAreFetching,
});
const mapDispatchToProps = () => ({
fetchData: wordsFetchData(),
});
Other notes and helpful things:
If you're using thunk, you'll have access to your entire redux store in here as a second argument. For example:
return (dispatch, getState) => {
dispatch(wordsAreFetching(true));
console.log('getState', getState());
const { words } = getState().items;
// This is a great place to do some checks to see if you _need_ to fetch any data!
// Maybe you already have it in your state?
if (!words.length) {
fetch('URL', {
method: "POST",
headers: {
......
}
})
I hope this helps, if you need anything else feel free to ask.