Events handled by another saga (Redux-Saga) - javascript

I'm trying to create an error interceptor for all my sagas, after analyzing the redux-saga eventChannel I tried to create a saga like this:
export function interceptor() {
return eventChannel(() => {
api.interceptors.response.use(
(response) => response,
(error) => {
const { response } = error;
if ([401, 403].includes(response.status)) {
emit(AuthCreators.logoutRequest());
}
return Promise.reject(error);
}
);
return () => null;
});
}
In rootSaga it is being called this way:
export default function* rootSaga() {
return yield all([fork(interceptor), anotherSaga, anotherSaga2]);
}
This way, every time one of my other sagas has a catch the interceptor is triggered, however my emit that should trigger the logoutRequest that is in other saga is not being triggered.
How can the emit call other saga?
Is this the best way to create an error interceptor?
Already grateful

Maybe I don't understand you question exactly but this might help you
const saga = [
anotherSaga, anotherSaga2
]
yield all(sagas.map((saga) => spawn(function* () {
try {
yield call(saga);
} catch (e) {
// console.log(e);
// here we should store all errors in some service...
}
})));

Related

Axios interceptor repeats previous loop and incrementally adds new loops per new request

In my react app I use an axios interceptor for my global error handling. Because I use React Context to show and handle my notifications I had to move the interceptor part to a functional component where I can use the React useContext hook.
const Interceptor = ({ children }) => {
const { addNotification } = useContext(NotificationContext);
apiClient.interceptors.request.use(
(config) => {
return config;
},
(error) => {
return Promise.reject(error);
}
);
apiClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.message === "Network Error") {
if (error?.response?.status === 504) {
addNotification("error", "Oops!", "gateway_timeout");
} else {
addNotification("error", "Oops!", "server_down");
}
} else {
if (error?.response?.config?.url !== "/me") {
addNotification("error", "Oops!", error.response.data.message);
}
}
return Promise.reject(error);
}
);
return <>{children}</>;
};
This works - errors are being caught and a notification is shown - for the first response with an error. The second time the first notification is shown again and two new notification are being made. The third time the previous three notifications are shown again and three new ones are being made and so on. The problem seems to come from the interceptor which is being ran incrementally (1,3,6,...)
Demo (hit the login button twice or more to see)
Problem
You are adding a new interceptor every render, but you only want to do it once.
Solution
Wrap your code in useEffect
useEffect(() => {
apiClient.interceptors.request.use(
// ...
)
apiClient.interceptors.response.use(
// ...
)
}, [])

React Custom Hooks - Handling errors

I am displaying all my api requests errors in a toast.
In my code, I have separated concepts, moving the components logic to business/ui hooks.
In order to render a toast (an imperative component), I just do the following inside a functional component:
const toast = useToast(); // UI hook
toast.display(message, { type: "error", duration: 500 });
and, in order to connect to my api, I can use custom business hooks, for example:
const useRequestSomething() {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const isRequesting = useRef(false);
const requestSomething = async (someParam, onSuccess = undefined, onError = undefined) => {
if (isRequesting.current) return;
isRequesting.current = true;
setIsLoading(true);
try {
const data = await api.requestSomething(someParam);
setData(data);
onSuccess?.();
} catch(err) {
onError?.(err);
}
setIsLoading(false);
isRequesting.current = false;
}
return {
data,
isLoading,
requestSomething
}
}
My main concern is the separation of concepts... I don't think it is a good idea to use the useToast() inside the this hook that is a container of my business logic... although it may be a good idea.
So, in order to handle errors, inside any component, I can do something like:
function MyComponent() {
const toast = useToast();
const { t } = useTranslation(); // i18n.js hook
const { data, isLoading, requestSomething } = useRequestSomething();
const handleOnPress = () => {
requestSomething("x", undefined, handleOnRequestSomethingError);
}
const handleOnRequestSomethingError = (err) => {
toast.display(t(err), { type: "error", duration: 500 });
}
... JSX
}
It seems that I have defined some kind of callback-based api with the business hook... what do you think about my implementation?
Is it an anti-pattern to handle errors this way (with callbacks) inside hooks?
What is the typical approach to handle this situations? (I cannot use useQuery, because of my backend)
I think your solution is good, but, IMHO, instead of prematurely handling the error, I like to let the error propagate to where we actually know how to handle it. For example, I would do this.
const requestSomething = async (params) = {
...
try {
await api.doRequest(params);
} catch (err) {
... do some common clean up ...
throw err;
}
}
const handleOnPress = async () => {
try {
await requestSomething("x");
} catch (err) {
toast.display(t(err), { type: "error", duration: 500 });
}
}
Actually, I would wrap it in a general error handler like this.
const handleOnPress = async () => {
await withGeneralErrorHandling(async () => {
try {
await requestSomething("x");
} catch (err) {
if (err.errorCode === 'SOME_KNOWN_CASE') {
toast.display(t(err), { type: "error", duration: 500 });
} else {
throw err;
}
}
});
}
async function withGeneralErrorHandling(callback: () => Promise<void>) {
try {
await callback()
} catch (err) {
if (err.errorCode === 'GENERAL_CASE1') { ...}
else if (err.errorCode === 'GENERAL_CASE2') { ... }
else {
if (isProduction) { reportError(err); }
else { throw err; }
}
}
}
This is because I usually cannot list out all the error cases at the first implementation. Each error case will be discovered incrementally. I have to let it fail fast by letting it propagate to as closest to the outermost controller as possible.
By utilizing this built-in error propagation, you retain the stack trace information and can know exactly where the error occurs.
Yeah, your Component knows about the Toast, every future component which handles some errors will know about the Toast.
This makes your error handling logic a little rigid, if you need to use another way of handling errors in the future, you'll have to edit every component.
I'd use some state management system (redux, mobx, whatever).
The idea is that in order to show an error you need to update the state of your application. Your toast component will be subscribed to the state change and react accordingly.
This way you depend on the state, not some actual component/way of displaying errors, which is more abstract and flexible.

How to make other function gets called after first is executed?

I am working on a react app where I am using redux for state management and I have 2 functions to call and I want them to run only after 1st function is executed.
Here's a snippet of whats I am doing:
if (this.props.page_num < this.props.numPages) {
this.props.fetchCode(params, isFiltered, isSearched).then(() => {
this.props.setPageNumber(this.props.page_num + 1);
});
}
Here I am getting a error stating:
CodeTable.jsx?2468:132 Uncaught TypeError: this.props.fetchCode(...).then is not a function
fetchCode function:
export function* fetchCode(action) {
try {
const response = yield call(Services.fetchCode, action.params);
const { dtoList } = response.data.pagedList;
const num_pages = response.data.pagedList.numPages;
const total_records = response.data.pagedList.totalRecords;
const page_number = response.data.pagedList.pageNumber;
const postCodeSetsData = dtoList.map(({
}) => ({
}));
yield put(ActionCreator.setCodes(dtoList, num_pages, total_records, postCodeData, page_number, action.isFiltered, action.isSearched));
} catch (error) {
sagaException(error);
}
}
Since you are using redux saga, I believe the most appropriate thing to do is compose another saga.
export function* fetchCodeSetsAndSetPage(action) {
try {
yield put (ActionCreator.fetchCodes(...));
yield put (ActionCreator.setPageNumber(...));
} catch (error) {
sagaException(error);
}
}
And then call that one instead in your component.
Some docs.
make fetchCodeSets function async. You can write promise like then only with an async functions. also use a return statement inside the function. The declaration of the fetchCodeSets function should be like this
const fetchCodeSets = async (params) => {
//codes here
return;
}

How to migrate api call wrapper from a redux thunk to redux saga

I have recently started using redux-saga and I'm really liking it.
I have the following wrapper which I was using for my api calls which would take a promise (my api call), and display a preloader and handle errors.
export const callApi = (promise: Promise<any>, errorMsg: string = 'Api error') => (dispatch: Dispatch) => {
dispatch(setLoading(true));
return promise.then(
(response) => {
dispatch(setLoading(false));
return response.body;
},
(error) => {
dispatch(setLoading(false));
dispatch(apiError(errorMsg, error));
return error;
});
};
I'm unsure how I would replicate behaviour like this in redux saga. I couldnt find any example of doing anything like this?
So far I've come up with
const camelizeKeysPromise = (obj) => Promise.resolve(camelizeKeys(obj));
export function* sagaCallApi(promise: Promise<any>, errorMsg: string = 'Api error') {
yield put(setLoading(true));
try {
const response = yield call(promise);
try {
const result = yield call(camelizeKeysPromise(response.body));
return result;
} catch (e) {
return response.body;
}
} catch (exception) {
yield put(setLoading(false));
yield put(apiError(errorMsg, error));
};
}
Yielding a call to promise will not return the desired response. You can use eventChannel from redux-saga to create a channel that emits the response on success or the error object on failure and then subscribe to the channel in your saga.
const promiseEmitter = promise => {
return eventChannel(emit => {
promise.then(
response => emit({response}),
error => emit({error})
);
});
};
Modify your new saga by replacing the call to the promise with this:
const channel = yield call(promiseEmitter, promise);
const {response, error} = yield take(channel);
if(response){
// handle success
return response;
}else if(error){
// handle failure
yield put(setLoading(false));
yield put(apiError(errorMsg, error));
}
Be aware that there might be syntactical errors in my code as I wrote this without an editor, but you can get the general approach.

How to avoid duplicate API requests with Redux-Saga?

So far I like Redux better than other Flux implementations, and I'm using it to re-write our front end application.
The main struggling points that I'm facing:
Maintaining the status of API calls to avoid sending duplicate requests.
Maintaining relationships between records.
The first issue could be solved by keeping a status field in the sub-state of each type of data. E.g.:
function postsReducer(state, action) {
switch(action.type) {
case "FETCH_POSTS":
return {
...state,
status: "loading",
};
case "LOADED_POSTS":
return {
status: "complete",
posts: action.posts,
};
}
}
function commentsReducer(state, action) {
const { type, postId } = action;
switch(type) {
case "FETCH_COMMENTS_OF_POST":
return {
...state,
status: { ...state.status, [postId]: "loading" },
};
case "LOADED_COMMENTS_OF_POST":
return {
status: { ...state.status, [postId]: "complete" },
posts: { ...state.posts, [postId]: action.posts },
};
}
}
Now I can make a Saga for Posts and another one for Comments. Each of the Sagas knows how to get the status of requests. But that would lead to a lot of duplicate code soon (e.g. Posts, Comments, Likes, Reactions, Authors, etc).
I'm wondering if there is a good way to avoid all that duplicate code.
The 2nd issue comes to existence when I need to get a comment by ID from the redux store. Are there best practices for handling relationships between data?
Thanks!
redux-saga now has takeLeading(pattern, saga, ...args)
Version 1.0+ of redux-saga has takeLeading that spawns a saga on each action dispatched to the Store that matches pattern. After spawning a task once, it blocks until the spawned saga completes and then starts to listen for a pattern again.
Previously I implemented this solution from the owner of Redux Saga and it worked really well - I was getting errors from API calls sometimes being fired twice:
You could create a higher order saga for this, which would look something like this:
function* takeOneAndBlock(pattern, worker, ...args) {
const task = yield fork(function* () {
while (true) {
const action = yield take(pattern)
yield call(worker, ...args, action)
}
})
return task
}
and use it like this:
function* fetchRequest() {
try {
yield put({type: 'FETCH_START'});
const res = yield call(api.fetch);
yield put({type: 'FETCH_SUCCESS'});
} catch (err) {
yield put({type: 'FETCH_FAILURE'});
}
}
yield takeOneAndBlock('FETCH_REQUEST', fetchRequest)
In my opinion this way is far way more elegant and also its behaviour can be easily customized depending on your needs.
I had the exact same issue in my project.
I have tried redux-saga, it seems that it's really a sensible tool to control the data flow with redux on side effects. However, it's a little complex to deal with the real world problem such as duplicate requests and handling relationships between data.
So I created a small library 'redux-dataloader' to solve this problem.
Action Creators
import { load } from 'redux-dataloader'
function fetchPostsRequest() {
// Wrap the original action with load(), it returns a Promise of this action.
return load({
type: 'FETCH_POSTS'
});
}
function fetchPostsSuccess(posts) {
return {
type: 'LOADED_POSTS',
posts: posts
};
}
function fetchCommentsRequest(postId) {
return load({
type: 'FETCH_COMMENTS',
postId: postId
});
}
function fetchCommentsSuccess(postId, comments) {
return {
type: 'LOADED_COMMENTS_OF_POST',
postId: postId,
comments: comments
}
}
Create side loaders for request actions
Then create data loaders for 'FETCH_POSTS' and 'FETCH_COMMENTS':
import { createLoader, fixedWait } from 'redux-dataloader';
const postsLoader = createLoader('FETCH_POSTS', {
success: (ctx, data) => {
// You can get dispatch(), getState() and request action from ctx basically.
const { postId } = ctx.action;
return fetchPostsSuccess(data);
},
error: (ctx, errData) => {
// return an error action
},
shouldFetch: (ctx) => {
// (optional) this method prevent fetch()
},
fetch: async (ctx) => {
// Start fetching posts, use async/await or return a Promise
// ...
}
});
const commentsLoader = createLoader('FETCH_COMMENTS', {
success: (ctx, data) => {
const { postId } = ctx.action;
return fetchCommentsSuccess(postId, data);
},
error: (ctx, errData) => {
// return an error action
},
shouldFetch: (ctx) => {
const { postId } = ctx.action;
return !!ctx.getState().comments.comments[postId];
},
fetch: async (ctx) => {
const { postId } = ctx.action;
// Start fetching comments by postId, use async/await or return a Promise
// ...
},
}, {
// You can also customize ttl, and retry strategies
ttl: 10000, // Don't fetch data with same request action within 10s
retryTimes: 3, // Try 3 times in total when error occurs
retryWait: fixedWait(1000), // sleeps 1s before retrying
});
export default [
postsLoader,
commentsLoader
];
Apply redux-dataloader to redux store
import { createDataLoaderMiddleware } from 'redux-dataloader';
import loaders from './dataloaders';
import rootReducer from './reducers/index';
import { createStore, applyMiddleware } from 'redux';
function configureStore() {
const dataLoaderMiddleware = createDataLoaderMiddleware(loaders, {
// (optional) add some helpers to ctx that can be used in loader
});
return createStore(
rootReducer,
applyMiddleware(dataLoaderMiddleware)
);
}
Handle data chain
OK, then just use dispatch(requestAction) to handle relationships between data.
class PostContainer extends React.Component {
componentDidMount() {
const dispatch = this.props.dispatch;
const getState = this.props.getState;
dispatch(fetchPostsRequest()).then(() => {
// Always get data from store!
const postPromises = getState().posts.posts.map(post => {
return dispatch(fetchCommentsRequest(post.id));
});
return Promise.all(postPromises);
}).then() => {
// ...
});
}
render() {
// ...
}
}
export default connect(
state => ()
)(PostContainer);
NOTICE The promised of request action with be cached within ttl, and prevent duplicated requests.
BTW, if you are using async/await, you can handle data fetching with redux-dataloader like this:
async function fetchData(props, store) {
try {
const { dispatch, getState } = store;
await dispatch(fetchUserRequest(props.userId));
const userId = getState().users.user.id;
await dispatch(fetchPostsRequest(userId));
const posts = getState().posts.userPosts[userId];
const commentRequests = posts.map(post => fetchCommentsRequest(post.id))
await Promise.all(commentRequests);
} catch (err) {
// error handler
}
}
First, you can create a generic action creator for fetching post.
function fetchPost(id) {
return {
type: 'FETCH_POST_REQUEST',
payload: id,
};
}
function fetchPostSuccess(post, likes, comments) {
return {
type: 'FETCH_POST_SUCCESS',
payload: {
post,
likes,
comments,
},
};
}
When you call this fetch post action, it'll trigger onFetchPost saga.
function* watchFetchPost() {
yield* takeLatest('FETCH_POST_REQUEST', onFetchPost);
}
function* onFetchPost(action) {
const id = action.payload;
try {
// This will do the trick for you.
const [ post, likes, comments ] = yield [
call(Api.getPost, id),
call(Api.getLikesOfPost, id),
call(Api.getCommentsOfPost, id),
];
// Instead of dispatching three different actions, heres just one!
yield put(fetchPostSuccess(post, likes, comments));
} catch(error) {
yield put(fetchPostFailure(error))
}
}

Categories

Resources