Axios calls with React : best practises - reactjs

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.

Related

Axios get in stock items

I am trying to filter products whether it is available or not.
Not sure how to pass an axios request with ">" logic.
I've started to create an Action
export const listProductAvailable = () => async (dispatch) => {
dispatch({
type: PRODUCT_AVAILABLE_LIST_REQUEST,
});
try {
const { data } = await Axios.get(`/api/products?countInStock>0`);
dispatch({ type: PRODUCT_AVAILABLE_LIST_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: PRODUCT_AVAILABLE_LIST_FAIL, payload: error.message });
}
};
But I don't think that such a request is possible.
const { data } = await Axios.get(/api/products?countInStock>0);
Also, I don't see myself changing my Product Model creating an isAvailable boolean field as it would be redundant with countInStock =0 or not.

Creating Components based on multiple axios requests

I'm working on redux-thunk project which i need to reach out endpoint-1 to get which & how many components i need to show in a page. Then i'm required to use this return to reach out endpoint-2 and get the data with the parameters from the enpoint-1.
I'm having hard time to create the correct logic. Also i'm sharing a diagram which i hope gives you the idea and requirements.
Flow Diagram
Thanks
export const fetchByPage = () => async dispatch => {
const response = await streams.get("byPage?", {
params: parameters.byPageParams
});
console.log(response.data);
dispatch({ type: FETCH_BY_PAGE, payload: response.data });
};
export const fetchPersons = () => async dispatch => {
const response = await streams.get(URL, {
params: parameters.byCriteriaParams
});
dispatch({ type: FETCH_PERSONS, payload: response.data });
};
Here are my actions. I'm trying to update byCriteriaParams with the data returns from fetchByPage call.

Fetching data from store if exists or call API otherwise in React

Let's assume I have a component called BookOverview that displays details of a book.
I'm getting data with an action:
componentDidMount() {
this.props.getBook(areaId);
}
And then I get the data with axios:
export const getBook = () => async dispatch => {
const res = await axios.get(
`${API}/${ENDPOINT}`
);
dispatch({
type: GET_BOOK,
payload: res.data
});
};
How shall I change this code to:
if redux store already have the book loaded - return it
if no book is present in the store - call the relevant API?
What is the best practise to achieve that please?
You can have the getState inside your async action creator like this:
export const getBook = () => async (dispatch, getState) => {
if(!getState().book /* check if book not present */) {
const res = await axios.get(
`${API}/${ENDPOINT}`
);
dispatch({
type: GET_BOOK,
payload: res.data
});
} else {
dispatch({
type: GET_BOOK,
payload: getState().book
});
}
};
For More Async Actions-Redux
You can try it this way:
componentDidMount() {
if(this.props.book==null){
this.props.getBook(areaId);
}
}
I assumed that you have a property called book in your props. that populates from the particular reducer.
You have to subscribe the particular reducer to get the this.props.book - This gives the value that you have in your store.

React Redux: How to call multiple dependent actions in sequence

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.

Redux saga, axios and progress event

Is there clean/short/right way to using together axios promise and uploading progress event?
Suppose I have next upload function:
function upload(payload, onProgress) {
const url = '/sources/upload';
const data = new FormData();
data.append('source', payload.file, payload.file.name);
const config = {
onUploadProgress: onProgress,
withCredentials: true
};
return axios.post(url, data, config);
}
This function returned the promise.
Also I have a saga:
function* uploadSaga(action) {
try {
const response = yield call(upload, payload, [?? anyProgressFunction ??]);
yield put({ type: UPLOADING_SUCCESS, payload: response });
} catch (err) {
yield put({ type: UPLOADING_FAIL, payload: err });
}
}
I want to receive progress events and put it by saga. Also I want to catch success (or failed) result of the axios request. Is it possible?
Thanks.
So I found the answer, thanks Mateusz BurzyƄski for the clarification.
We need use eventChannel, but a bit canningly.
Suppose we have api function for uploading file:
function upload(payload, onProgress) {
const url = '/sources/upload';
const data = new FormData();
data.append('source', payload.file, payload.file.name);
const config = {
onUploadProgress: onProgress,
withCredentials: true
};
return axios.post(url, data, config);
}
In saga we need to create eventChannel but put emit outside.
function createUploader(payload) {
let emit;
const chan = eventEmitter(emitter => {
emit = emitter;
return () => {}; // it's necessarily. event channel should
// return unsubscribe function. In our case
// it's empty function
});
const uploadPromise = upload(payload, (event) => {
if (event.loaded.total === 1) {
emit(END);
}
emit(event.loaded.total);
});
return [ uploadPromise, chan ];
}
function* watchOnProgress(chan) {
while (true) {
const data = yield take(chan);
yield put({ type: 'PROGRESS', payload: data });
}
}
function* uploadSource(action) {
const [ uploadPromise, chan ] = createUploader(action.payload);
yield fork(watchOnProgress, chan);
try {
const result = yield call(() => uploadPromise);
put({ type: 'SUCCESS', payload: result });
} catch (err) {
put({ type: 'ERROR', payload: err });
}
}
I personally found the accepted answer to be very convoluted, and I was having a hard time implementing it. Other google / SO searches all led to similar type answers. If it worked for you, great, but I found another way using an EventEmitter that I personally find much simpler.
Create an event emitter somewhere in your code:
// emitter.js
import { EventEmitter } from "eventemitter3";
export default new EventEmitter();
In your saga to make the api call, use this emitter to emit an event within the onUploadProgress callback:
// mysagas.js
import eventEmitter from '../wherever/emitter';
function upload(payload) {
// ...
const config = {
onUploadProgress: (progressEvent) = {
eventEmitter.emit(
"UPLOAD_PROGRESS",
Math.floor(100 * (progressEvent.loaded / progressEvent.total))
);
}
};
return axios.post(url, data, config);
}
Then in your component that needs this upload progress number, you can listen for this event on mount:
// ProgressComponent.jsx
import eventEmitter from '../wherever/emitter';
const ProgressComponent = () => {
const. [uploadProgress, setUploadProgress] = useState(0);
useEffect(() => {
eventEmitter.on(
"UPLOAD_PROGRESS",
percent => {
// latest percent available here, and will fire every time its updated
// do with it what you need, i.e. update local state, store state, etc
setUploadProgress(percent)
}
);
// stop listening on unmount
return function cleanup() {
eventEmitter.off("UPLOAD_PROGRESS")
}
}, [])
return <SomeLoadingBar value={percent} />
}
This worked for me as my application was already making use of a global eventEmitter for other reasons. I found this easier to implement, maybe someone else will too.

Resources