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.
Related
I'd like to know if it's possible to make 2 API calls inside a loader function if I am using react-router 6. My ideas was to create an object based on these 2 calls and destruct the object in the rendering component like this:
function MainComponent (){
const {data , reservation} = useRouteLoaderData('room-details');
..
..
}
export default MainComponent;
export async function loader({request, params}) {
const id = params.roomId;
const response = await fetch ('http://localhost:8080/rooms/' + id);
const response2 = await fetch('http://localhost:8080/rooms/reservation/' + id)
const megaResponse = {
data: response, //i tried data:{respose} It ain't work
reservation: response2,
};
if (!response.ok) {
throw json({message: 'Something Wrong'}, {status: 500});
}
else {
return megaResponse;
}
}
But i have no success output.
I'd really want to make these 2 call in one place, otherwise I will have to use useEffect in a child component. Not a good Idea I think.
Thanks
I suspect you are not returning the unpacked response, i.e. JSON. I suggest surrounding the asynchronous code in a try/catch and simply try to process the requests/responses. Unpack the JSON value from the response objects. Since it doesn't appear the requests are dependent on one another I recommend loading them into an array of Promises that can be run concurrently and awaited as a whole. If during any part of the processing a Promise is rejected or an exception thrown, the catch block will return the JSON error response to the UI, otherwise, the { data, reservation } object is returned.
const loader = async ({ request, params }) => {
const { roomId } = params;
try {
const [data, reservation] = await Promise.all([
fetch("http://localhost:8080/rooms/" + roomId),
fetch("http://localhost:8080/rooms/reservaton/" + roomId)
]).then((responses) => responses.map((response) => response.json()));
return { data, reservation };
} catch {
throw json({ message: "Something Wrong" }, { status: 500 });
}
};
I found the solution, I tried it and it worked. It is as follow:
function MainComponent (){
const [data , reservation] = useRouteLoaderData('room-details');
..
..
}
export default MainComponent;
export async function loader({request, params}) {
const id = params.roomId;
return Promise.all([
fetch ('http://localhost:8080/rooms/' + id),
fetch('http://localhost:8080/rooms/reservation/' + id)
])
.then(
([data, reservation]) =>
Promise.all([data.json(), reservation.json()]),
error => {throw json({message: 'Something Wrong'}, {status: 500});}
)
.then(([data, reservation]) => {
return [data, reservation];
});
}
Thanks
playing with react native and API I've faced this kind of issue:
Do not use setState in componentDidMount (react/no-did-mount-set-state)
Who knows the reason behind?
Here my piece of code
Home.js
state = {
users: []
}
async componentDidMount() {
const users = await ajax.fetchUsers();
this.setState({users});
}
Here FetchData.js
async fetchUsers() {
try {
let response = await fetch(URI + '/users');
let responseJsonData = await response.json();
return responseJsonData;
}
catch(e) {
console.log(e)
}
}
My guess is that you're getting that error because the tool in question doesn't realize you've added async to componentDidMount, so it thinks you're synchronously setting state in componentDidMount.
You shouldn't do that. componentDidMount is not defined as an async method in React.Component. Don't make methods async when they aren't defined that way by the class you're extending. Nothing handles the promise that you'd be returning if you made componentDidMount async.
Instead, use a wrapper async function (and handle errors!!):
componentDidMount() {
(async() => {
try {
const users = await ajax.fetchUsers();
this.setState({users});
} catch (e) {
// ...handle/report error here...
}
})();
}
Or even break it out as its own method:
componentDidMount() {
this.loadInitialUsers(); // Note: No `await`
}
async loadInitialUsers() {
// Important: This must not throw any errors, because nothing handles
// the promise rejection that would create
try {
const users = await ajax.fetchUsers();
this.setState({users});
} catch (e) {
// ...handle/report error here...
}
}
Within my React component, I have an async request which dispatches an action to my Redux store which is called within the useEffect hook:
const loadFields = async () => {
setIsLoading(true);
try {
await dispatch(fieldsActions.fetchFields(user.client.id));
} catch (error) {
setHasError(true);
}
setIsLoading(false);
}
useEffect(() => { if(isOnline) { loadFields() } }, [dispatch, isOnline]);
The action requests data via a fetch request:
export const fetchFields = clientId => {
return async dispatch => {
try {
const response = await fetch(
Api.baseUrl + clientId + '/fields',
{ headers: { 'Apiauthorization': Api.token } }
);
if (!response.ok) {
throw new Error('Something went wrong!');
}
const resData = await response.json();
dispatch({ type: SET_FIELDS, payload: resData.data });
} catch (error) {
throw error;
}
}
};
export const setFields = fields => ({
type : SET_FIELDS,
payload : fields
});
When this is rendered within the React app it results in the following warning:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a useEffect cleanup function
I believe this occurs because the promise doesn't have a "clean-up" function. But I am unsure where to place this? Should I have some logic within LoadFields()? Or must this be done within the useEffect hook?
This tutorial which will help you to resolve your issue.
Quick example: with Promises
function BananaComponent() {
const [bananas, setBananas] = React.useState([])
React.useEffect(() => {
let isSubscribed = true
fetchBananas().then( bananas => {
if (isSubscribed) {
setBananas(bananas)
}
})
return () => isSubscribed = false
}, []);
return (
<ul>
{bananas.map(banana => <li>{banana}</li>)}
</ul>
)
}
Quick example: with async/await (Not the best one but that should work with an anonymous function)
function BananaComponent() {
const [bananas, setBananas] = React.useState([])
React.useEffect(() => {
let isSubscribed = true
async () => {
const bananas = await fetchBananas();
if (isSubscribed) {
setBananas(bananas)
}
})();
return () => isSubscribed = false
}, []);
return (
<ul>
{bananas.map(banana => <li>{banana}</li>)}
</ul>
)
}
First issue
If your useEffect() fetches data acynchronously then it would be a very good idea to have a cleanup function to cancel the non-completed fetch. Otherwise what could happen is like that: fetch takes longer than expected, meantime the component is re-rendered for whatever reason. Maybe because its parent is re-rendered. The cleanup of useEffect runs before re-render and the useEffect itself runs after re-render. To avoid having another fetch inflight it's better to cancel the previous one. Sample code:
const [data, setData] = useState();
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const apiData = await fetch("https://<yourdomain>/<api-path>",
{ signal: controller.signal });
setData(apiData);
} catch (err) {
if (err.name === 'AbortError') {
console.log("Request aborted");
return;
}
}
};
fetchData();
return () => {
controller.abort();
}
});
Second issue
This code
return async dispatch => {
will not work because neither dispatch nor Redux store support async actions. The most flexible and powerful way to handle this issue is to use middleware like redux-saga. The middleware lets you:
dispatch 'usual' sync actions to Redux store.
intercept those sync actions and in response make one or several async calls doing whatever you want.
wait until async call(s) finish and in response dispatch one or several sync actions to Redux store, either the original ones which you intercepted or different ones.
I have a component which does some SQLite loads in it's componentDidMount() function
async componentDidMount() {
try {
const user = await this.userDao.getUserData();
const setupNeeded = user === null;
if( setupNeeded ) {
this.setState({
status : 'setup'
});
}
else {
this.setState({
status : 'ready',
seed: user.seed
})
}
} catch (e) {
logger.error("Error during db query", e);
this.setState({
status: 'corrupted'
});
}
}
And I would like to test the rendered result after the call to getUserData() has been resolved, and the state has been set accordingly.
Now in my test, I have the actual call to the database mocked away, so the Promise should be resolved immediately, yet testing like this does not work as expected:
Initially, I tried it like this:
test('Should render SetNewPasswordScreen', async () => {
const tree = await renderer.create(<IndexScreen/>);
const json = tree.toJSON();
// expect stuff
});
However the json in this case contains the data of the initial render() call.`
Doing it like this will work:
test('Should render SetNewPasswordScreen', (done) => {
const tree = renderer.create(<IndexScreen/>);
setTimeout(() => {
const json = tree.toJSON();
// expect stuff
}, 5000);
});
But this is not ideal, because I am just guessing that after 5 seconds everything will be done, but I don't know. Also, it's less then suitable if a test takes 5 seconds to finish. (I used 5 seconds arbitrarily, probably it will also work with much less since the async call is mocked anyway, but I can never really know)
My question is if anybody has a better idea of how to solve this issue?
it('should render setNewPassWordScreen', async () => {
const tree = await renderer.create(<IndexScreen/>);
const instance = tree.getInstance();
await instance.componentDidMount();
// expect other stuff.
});
Little late for the party but faced the issues as well. Here is a handy shortcut function I use with the adviced usage of act:
import renderer from "react-test-renderer"
import { ReactElement } from "react"
import { act } from "#testing-library/react-native"
export const expectToMatchSnapshot = async (component: ReactElement) => {
let tree
await act(async () => {
tree = renderer.create(component)
})
expect(tree.toJSON()).toMatchSnapshot()
}
In your case you should only then add:
test("should render ***", async () => {
await expectToMatchSnapshot(<IndexScreen/>)
}
you could use the wait for expect package:
https://www.npmjs.com/package/wait-for-expect
test('Should render SetNewPasswordScreen', async (done) => {
const tree = renderer.create(<IndexScreen/>);
await waitForExpect(() => {
const json = tree.toJSON();
// expect stuff
});
});
mind the function passed into the test('...', fn) is now an async function.
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))
}
}