React Redux: How to call multiple dependent actions in sequence - reactjs

I'm trying to chain two calls in a single action using a thunk, but it doesn't seem to work as expected. I need the ID value from the first action to call the second one.
Actions look like this:
export const getParentRecords = filterId => {
return (dispatch, getState) => {
let headers = {
filter_id: filterId
};
const request = axios({
method: "GET",
url: `https://myapi.com/v1/parent-records`,
headers: headers
});
dispatch({
type: GET_PARENT_RECORDS,
payload: request
});
};
};
export const getChildRecords = (parentId = null) => {
let url = `https://myapi.com/v1/child-records`;
if (parentId) {
url = `https://myapi.com/v1/child-records/?parent_id=${parentId}`;
}
return (dispatch, getState) => {
let headers = {
//etc...
};
const request = axios({
method: "GET",
url: url,
headers: headers
});
dispatch({
type: GET_CHILD_RECORDS,
payload: request
});
};
};
export const getAllRecords = filterId => {
return (dispatch, getState) => {
dispatch(getParentRecords(filterId);
let { parentRecords } = getState();
let defaultParent = parentRecords.filter(p => p.is_default === true)[0];
dispatch(getChildRecords(defaultParent.parent_id));
};
};
In calling component:
const mapStateToProps = state => {
return {
parentRecords: state.parentRecords,
childRecords: state.childRecords
};
};
export default connect(mapStateToProps, { getAllRecords })(MyComponent);
Problem is; dispatching the first action doesn't seem to be doing anything. When I call getState() afterwards, the data isn't there. The parentRecords variable in getAllRecords is always empty.
I'm really not sure what to do with this. Pretty common scenario but haven't found a way through it.

I suggest you to use another library for side-effects handling, like redux-saga or redux-observable, since redux-thunk is very primitive.
Redux-saga is generator-based and imperative.
Redux-observable is RxJS-based and declarative.
So choose whatever you like more.
https://redux-saga.js.org/
https://redux-observable.js.org/
Each asynchronous action should have three action types, eg: GET_CHILD_RECORDS, GET_CHILD_RECORDS_SUCCESS and GET_CHILD_RECORDS_FAILURE.
Using redux-saga it will look like this:
Action creators:
const getChildRecords = (parentId = null) => ({
type: GET_PARENT_RECORDS,
payload: parentId
});
Then you can handle this action in saga generator:
function rootSaga*() {
yield takeLatest(GET_PARENT_RECORDS, onGetParentRecords);
yield takeLatest(GET_PARENT_RECORDS_SUCCESS, onGetChildRecords);
}
function onGetParentRecords*({ payload: parentId }) {
try {
const parentRecords = yield call(apiCallFunctionHere, parentId);
yield put({
type: GET_PARENT_RECORDS_SUCCESS,
payload: parentRecords
});
} catch(error) {
yield put({
type: GET_PARENT_RECORDS_FAILURE,
error
});
}
}
function onGetChildRecords*({ payload: parentRecords }) {
const defaultParent = parentRecords.filter(p => p.is_default === true)[0];
try {
const childRecords = call(apiFunctionToFetchChildRecords, defaultParent);
yield put({
type: GET_CHILDREN_RECORDS_SUCCESS,
payload: parentRecords
});
} catch(error) {
yield put({
type: GET_CHILDREN_RECORDS_FAILURE,
error
});
}
}

I'm not interested in introducing yet another framework for something so simple. After the commute home, an idea struck me. Please let me know the pros/cons.
A new getAllRecords function:
export const getAllRecords = filterId => {
return (dispatch, getState) => {
let headers = {
// etc...
};
const request = axios({
method: "GET",
url: `https://myapi.com/v1/parent-records`,
headers: headers
});
request.then(result => {
if (result.status === 200) {
let parentRecords = result.data.payload;
let defaultParent = parentRecords.filter(p => p.is_default === true)[0];
dispatch({
type: GET_PARENT_RECORDS,
payload: parentRecords
});
dispatch(getChildRecords(defaultParent.parent_id));
}
});
};
};
This seems to get me everything I need. Gets parent record(s) by executing the promise, dispatches parent and child results.

Related

React - useReducer with asynchronous CRUD operations

I am trying to figure out how to use useReducer with asynchronous CRUD operations (using the fetch API).
In my mind the reducer function would look like this:
async function reducer(action, state) {
const newState = {...state};
switch (action.type) {
case types.ADD_ITEM:
try {
const {item} = await addItem(action.payload.item);
newState.items.push(item);
}
catch (e) {
newState.error = e.message;
}
break;
case types.REMOVE_ITEM:
try {
await removeItem(action.payload.itemId);
newState.items = newState.items.filter(value.id !== action.payload);
}
catch (e) {
newState.error = e.message;
}
break;
case types.EDIT_ITEM:
try {
const {item} = await editItem(action.payload.itemId, action.payload.item);
newState.items[newState.items.findIndex(value => value.id === action.payload.itemId)] = item;
}
catch (e) {
newState.error = e.message;
}
break;
}
return newState;
}
These would be the fetch functions:
async function addItem(item) {
const response = await fetch('addItemRoute', {
method: "POST",
body: JSON.stringify({
item
})
});
return response.json();
}
async function removeItem(itemId) {
const response = await fetch('removeItemRoute/' + itemId, {
method: "DELETE"
});
return response.json();
}
async function editItem(itemId, item) {
const response = await fetch('editItemRoute/'+ itemId, {
method: "PUT",
body: JSON.stringify({
item
})
});
return response.json();
}
But the reducer function cannot be an async function.
What would be the standard way to handle concepts like this?
Any help/reference is truly appreciated.
I think you misunderstood the role of reducer. In React world, there is a thing call global state (a way to pass values down to children without having to pass as props), which traditionally being handled by another package called Redux. The reducer only handle taking whatever you dispatch, decide what action to take to update the global state based on the type of action which is not asynchronous. The action is what you use to decide what to dispatch and also the way for you to get the data to dispatch so usually all the HTTP calls occurs here. Since useReducer will returns for you the current state and the dispatch function as well, you can basically pass this dispatch to your action. You can take a look at my example below based on your example for clearer image of what you might want to do:
You may want to put all your action in a action file called action.js like this:
async function addItem(item, dispatch) {
const response = await fetch('addItemRoute', {
method: "POST",
body: JSON.stringify({
item
})
});
return dispatch({
type: "ADD_ITEM",
payload: response.json()});
}
async function removeItem(itemId, dispatch) {
const response = await fetch('removeItemRoute/' + itemId, {
method: "DELETE"
});
return dispatch({
type: "ADD_ITEM",
payload: response.json()
});
}
async function editItem(itemId, item, dispatch) {
const response = await fetch('editItemRoute/'+ itemId, {
method: "PUT",
body: JSON.stringify({
item
})
});
return dispatch({
type: "ADD_ITEM",
payload: response.json()
});
}
Then in your reducer, you can do the regular without having to call the fetch or async calls like this:
async function reducer(action, state) {
const newState = {...state};
switch (action.type) {
case types.ADD_ITEM:
try {
const {item} = action.payload;
newState.items.push(item);
}
catch (e) {
newState.error = e.message;
}
break;
case types.REMOVE_ITEM:
try {
newState.items = newState.items.filter(value.id !== action.payload);
}
catch (e) {
newState.error = e.message;
}
break;
case types.EDIT_ITEM:
try {
const {item} = action.payload;
newState.items[newState.items.findIndex(value => value.id === action.payload.itemId)] = item;
}
catch (e) {
newState.error = e.message;
}
break;
}
return newState;
}
Then in your component with the button that you want to execute this, you can do something like this:
const MyComponent = ()=> {
...
const [state, dispatch] = useReducer(reducer, initialState);
...
return (
...
<button onClick={addItem(item, dispatch)}/>
...
}
You can reference the core concept of redux here which IMO explains very clearly the functionality of reducer, dispatch, actions and global state. If you want you can also tried out Redux as well, here's their tutorial.

Axios calls with React : best practises

i want to know if there is some clean code or update to make it on my code, because i think i repeat the same code on every actions on my redux, my question is how can I avoid calling axios on my actions files ?
Please take a look on my code here :
export const SignInType = (host, lang) => async (dispatch) => {
try {
dispatch({
type: USER_LOGIN_SIGNINTYPE_REQUEST,
});
const { data } = await axios.get(
`/${lang}/data?host=${host}`
);
console.log({ data });
dispatch({
type: USER_LOGIN_SIGNINTYPE_SUCCESS,
payload: data,
});
dispatch({
type: USER_LOGIN_CLEAR_ERROR,
});
} catch (err) {
dispatch({
type: USER_LOGIN_SIGNINTYPE_FAIL,
payload: err,
});
}
};
I Really want to delete the Axios name from my actions file and make it on a separate file, but how can i do this ?
Thank you
We can suggest but there's no correct answer to this, initially any redundant lines of code can be abstracted, so in order to make things a little bit easier, we need to abstract the obvious and add the meaningful, e.g:
abstract the way you write action creators:
const actionComposer = (options) => (...args) => async dispatch => {
const modifiedDispatch = (type, payload) => dispatch({ type, payload });
const { action, onSuccess, onFailed } = options(modifiedDispatch);
try {
if (action) {
const res = await action(...args)
onSuccess(res);
}
} catch (err) {
onFailed(err)
}
}
then your code can look like this:
export const SignInType = actionComposer((dispatch)=> {
return {
action: async (host, lang) => {
dispatch(USER_LOGIN_SIGNINTYPE_REQUEST);
const { data } = await axios.get(`/${lang}/data?host=${host}`);
return data;
},
onSuccess: (res) => {
dispatch(USER_LOGIN_SIGNINTYPE_SUCCESS, data);
dispatch(USER_LOGIN_CLEAR_ERROR);
},
onFailed: (err) => {
dispatch(USER_LOGIN_CLEAR_ERROR, err.message)
}
}
})
Redux Toolkit already has a createAsyncThunk API that does all the work of defining the action types and dispatching them for you. You should use that.
Alternately, you can use our RTK Query data fetching and caching library, which will eliminate the need to write any data fetching logic yourself.

The function call inside the redux action doesnot get executed

I'm trying my first react application with redux, along with Thunk as middle ware. When calling the action from one of the components, the action is hit but the code inside the action return is not executed. I have no clue what I'm missing here. I'm also using Firestore to get the data.
export const getBikeTypes = () => {
console.log('outside return')
return (dispatch, getState, { getFireBase, getFireStore }) => {
console.log('inside return')
const firestore = getFireStore();
firestore.collection('BikeTypes').get()
.then((response) => {
console.log(response)
return response
}).then(() => {
dispatch({ type: 'GET_BIKETYPES' });
}).catch((err) => {
dispatch({ type: 'GET_BIKETYPES_FAIL', err });
})
}
};
I think you should dispatch action with the payload once you get the response.
const firestore = getFireStore();
firestore.collection('BikeTypes').get()
.then((response) => {
dispatch({ type: 'GET_BIKETYPES', payload: response })
})

Should I handle errors in my action creators

In the following context how should I handle possible errors:
export async function testAction(data) {
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
return {
type: 'TEST_ACTION',
payload: response
}
} catch(err) {
// ???
}
}
// Somewhere in a component:
<Button onClick={ () => dispatch( testAction() ) }>
Test Stuff
</Button>
Or is better to actually dispatch from the component, eg:
refactor action creator:
export async function testAction(data) {
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
return response
} catch(err) {
return err
}
}
Somewhere in a component:
const handleTestAction = () => {
testAction().then(r => dispatch( { type: 'TEST_ACTION', payload: r } ) ).catch( // hadnle errors)
}
<Button onClick={ handleTestAction }>
Test Stuff
</Button>
I know the redux style guide recommends using Action Creators for dispatching actions but in this particular case I am calling the action first and then use dispatch. How should I approach it?
You can create another reducer to handle errors.
export async function testAction(data) {
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
return {
type: 'TEST_ACTION',
payload: response
}
} catch(err) {
return {
type: 'ERROR',
payload: err
}
}
}
But you cannot do it like above. because the process is asynchronous
You have to use a 'redux-thunk' for that. Once you add it as a middle-ware to your store, you can get the dispatcher in to your action creater, so you can dispatch anything in the test action after you complete.
So your reducer should change to the below one,
export async function testAction(data) {
return (dispatch) => {
try {
let response = await
axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
dispatch({
type: 'TEST_ACTION',
payload: response
})
} catch(err) {
dispatch({
type: 'ERR',
payload: response
})
}
}
}
UPDATE
Once you connect the middleware, you can use dispatch in the action creater,
const store = createStore(
reducers,
{},
applyMiddleware(thunk)
);
You need to only add the thunk to the store just like above.
You can make it more clear by refactor your code like below
export const testAction = () => async (dispatch) => {
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
dispatch({
type: 'TEST_ACTION',
payload: response
})
} catch(err) {
dispatch({
type: 'ERR',
payload: response
})
}
}
If your API is going to change in dev and prod modes, You can use below way,
Somewhere in your application,
const axiosInstatnce = axios.create({
baseURL: "https://jsonplaceholder.typicode.com",
headers: {/* you can set any header here */}
});
Now when you create store,
const store = createStore(
reducers,
{},
applyMiddleware(thunk.withExtraArgument(axiosInstatnce))
);
Now you can get the axiosInstance as the third argument of the function you return from the testAction. 2nd argument gives the current state.
export const testAction = () => async (dispatch, state, api) => {
try {
let response = await api.get(`/todos/1`);
dispatch({
type: 'TEST_ACTION',
payload: response
})
} catch(err) {
dispatch({
type: 'ERR',
payload: response
})
}
}
Now in your component,
import {testAction} from '../path/to/actions'
const dispatch = useDispatch()
dispatch(testAction())
If you want to write async code in an action creator, you need to write an async action creator. Regular action creators return an object whereas async action creators return a function instead of an object.
export function testAction(data) {
return async function(dispatch) {
// async code
}
}
Inside the function returned by an async action creator, you have access to dispatch which can be used to dispatch any success action in case of successful response from server and in case of error, you can dispatch an action indicating that an error has occurred.
export function testAction(data) {
return async function (dispatch) {
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
dispatch({
type: 'TEST_ACTION',
payload: response
});
} catch(err) {
dispatch({type: 'TEST_ACTION_ERROR', message: 'error occurred'});
}
}
}
You also need to use redux-thunk middleware if you have async action creators in your code. This middleware allows action creators to return a function.
For complete details about how to create async action creators and how to setup redux-thunk middleware to make async creators work, take a look at Async Actions

render view after a post request in react/redux

I have post method helper where I'm making the rest calls to the server which is basically running but the view/container is not rerendering after the call.
export function postData(action, errorType, isAuthReq, url, dispatch, data) {
const requestUrl = API_URL + url;
let headers = {};
if (isAuthReq) {
headers = {headers: {'Authorization': cookie.load('token')}};
}
axios.post(requestUrl, data, headers)
.then((response) => {
dispatch({
type: action,
payload: response.data
});
})
.catch((error) => {
errorHandler(dispatch, error.response, errorType)
});
}
I'm getting the the following error: dispatch is not defined in the browser when I'm calling this method
my call from the container is as followed:
handleFavorite(buildingId) {
const url = `/building/${buildingId}/toogle-favorite`;
postData(FETCH_All_BUILDING, AUTH_ERROR, true, url, this.props.dispatch, {});
}
This is how my connect method is looks like:
function mapStateToProps(state) {
return {
buildings: state.building.buildings,
error: state.building.error,
userId: state.auth.userId
}
}
export default connect(mapStateToProps, {buildingsAll})(BuildingAll);
My Question is...
How can I re render my view? This dispatch that I want to give to the method is not available. Is there a possibility to bind that rest to the state perhaps with mapDispatchToProps. Any idea how I can solve that problem, I'm fairly new to react/redux - it's my first side project in that lib.
Thanks
Update 1
I have updated the code but getting the next error and my view is now not rendering (nothing showing).
mapDispatchToProps() in Connect(BuildingAll) must return a plain object. Instead received function
bundle.js:26 Uncaught TypeError: finalMergeProps is not a function
const mapDispatchToProps = (dispatch) => bindActionCreators(postDataThunk, dispatch);
export default connect(mapStateToProps, mapDispatchToProps, {buildingsAll})(BuildungAll);
You need to bind your action creators in your container
const { bindActionCreators } = require("redux");
const mapStateToProps = (state) => {
return {
buildings: state.building.buildings,
error: state.building.error,
userId: state.auth.userId
}
}
const mapDispatchToProps = (dispatch) => bindActionCreators(YourActions, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(BuildingAll);
And then your action becomes something like this:
import thunk from 'redux-thunk';
const postData = (action, errorType, isAuthReq, url, data) => {
return (dispatch) => {
const requestUrl = API_URL + url;
let headers = {};
if (isAuthReq) {
headers = { headers: { 'Authorization': cookie.load('token') } };
}
axios.post(requestUrl, data, headers)
.then((response) => {
dispatch({
type: action,
payload: response.data
});
})
.catch((error) => {
errorHandler(dispatch, error.response, errorType)
});
};
};
Because your postData might have a few side effects because it's fetching something asynchronously, you'll need a thunk
Read this article on it: http://redux.js.org/docs/advanced/AsyncActions.html

Resources