I have a test where if /endpoint1 fails, an error message should be displayed. The test passes however I get the following error from React Testing Library:
Warning: An update to HomePage inside a test was not wrapped in act(...).
Here's the snipper for clarity. You can see I'm using SWR to send requests. I'm mocking requests using Mock Service Worker.
export default function App() {
const { data: data1, error: error1 } = useSwr("/endpoint1")
const { data: data2, error: error2 } = useSwr("/endpoint2")
if (error1 || error2) {
return <Error />
}
return <Success />
}
The error is present because /endpoint2 resolves after /endpoint1, causing the component to rerender. I believed that cancelling the second request once the first one errors would work. I made a wrapper around useSwr() that used AbortController() and exposed an cancel() function however the same error would occur with this snippet:
export default function App() {
const { data: data1, error: error1, cancel: cancel1 } = useCancellableSwr(
"/endpoint1"
)
const { data: data2, error: error2, cancel: cancel2 } = useCancellableSwr(
"/endpoint2"
)
if (error1 || error2) {
if (!data1 || !error1) cancel2()
if (!data2 || !error2) cancel1()
return <p>Error</p>
}
return <p>Success</p>
}
For some reason, even after cancelling one endpoint, it still resolves and causes a rerender. Am I missing something here? Thanks
Edit:
Here's the failing test:
test("An error shows if unable to fetch /endpoint1", async () => {
mockServer.use(
rest.get("/endpoint1", (_, res, ctx) => res(ctx.status(500)))
)
mockServer.use(
rest.get("/endpoint2", (_, res, ctx) => res(ctx.status(200)))
)
render(<App />)
const error = await screen.findByText("Error")
expect(error).toBeInTheDocument()
})
And my custom fetcher function:
async function serverFetch(endpoint) {
const response = await fetch("http://localhost:8080" + endpoint)
const body = response.json()
if (!response.ok) {
throw new Error("An error occurred while fetching the data.")
}
return body
}
Related
I have a redux action called academyRedirect.js which is invoked whenever an user is redirected to the path /user/academy when he pushes the 'academy' button in my main page.
export const getAcademyAutoLogin = () => {
axios.get(`/user/academy`)
.then((response) => {
window.location.replace(response.data); // this is replaced by an URL coming from backend
})
.catch((error) => {
reject(error.response);
});
return null;
};
Whenever the user is not allowed to access the academy (for not having credentials) or whenever i get an error 500 or 404, i need to display a modal or something to inform the user that an error occurred while trying to log into the academy. Right now im not being able to do it, the page just stays blank and the console output is the error.response.
Any help is appreciated
Redux Store
export const messageSlice = createSlice({
name: 'message',
initialState: { isDisplayed: false, errorMessage: ''},
reducers: {
displayError(state, action) {
state.isDisplayed = true
state.errorMessage = action.message
},
resetErrorState() {
state.isDisplayed = false
state.errorMessage = ''
},
}
})
export const messageActions = messageSlice.actions;
Inside the component:-
const Login = () => {
const errorState = useSelector(globalState => globalState.message)
const onClickHandler = async => {
axios.get(`/user/academy`)
.then((response) => { window.location.replace(response.data) })
.catch((error) => {
dispatch(messageActions.displayError(error))
});
}
return (
{errorState.isDisplayed && <div>{errorState.errorMessage}</div>}
{!errorState.isDisplayed &&<button onClick={onClickHandler}>Fetch Data </button>}
)
}
Maybe this is of help to you
You can try to add interceptor to your axios.
Find a place where you create your axios instance and apply an interceptor to it like this
const instance = axios.create({
baseURL: *YOUR API URL*,
headers: { "Content-Type": "application/json" },
});
instance.interceptors.request.use(requestResolveInterceptor, requestRejectInterceptor);
And then, in your requestRejectInterceptor you can configure default behavior for the case when you get an error from your request. You can show user an error using toast for example, or call an action to add your error to redux.
For second case, when you want to put your error to redux, its better to use some tools that created to make async calls to api and work with redux, for example it can be redux-saga, redux-thunk, redux-axios-middleware etc.
With their docs you would be able to configure your app and handle all cases easily.
I'm using React, and just wanted some advice on error handling.
I have my fetch request in an async function, this function is in another folder and is being imported in my App.js file. Im doing this because I want to try out testing with mock service worker, and have read its easier with the requests in a separate file.
From looking at my code below, is this best practice for error handling? Is there a better way thats more concise?
Here is my fetch async function, at the moment i've purposely gave the wrong env variable name so it will give me a 401 unauthorised error.
require('dotenv').config()
export const collect = async () => {
const key = process.env.REACT_APP_API_KE
try{
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=london&appid=${key}`)
if(res.status !== 200){
throw new Error(res.status)
}
const data = await res.json()
return data
} catch (error){
let err = {
error: true,
status: error.message,
}
return err
}
}
This is being called in my App.js file (not rendering much at the moment)
import { useState } from 'react'
import { collect } from './utilities/collect'
require('dotenv').config()
function App() {
const [data, setData] = useState("")
const [error, setError] = useState({ error: false, status: "" })
const handleFetch = async () => {
let newData = await collect()
if(newData.error){
setError({ error: newData.error, status: newData.status })
}else {
setData(newData)
}
}
return (
<div className="App">
<h1>weather</h1>
<button onClick={handleFetch}>fetch</button>
</div>
);
}
export default App;
Any help or advise would be great.
When writing an abstraction around Promises or async and await one should make sure it is used appropriately, that is a good Promse must allow it consumer to use it then and catch method or should allow it consumer use try and catch to consume it and provide appropriate information on Errors
From Your code, The abstraction doesnt gives back an appropriate response and doesnt follow the standard behavior of a promise it always resolve and never reject and though the code works its implementation of the collect is different from a standard Promise and wont be nice for a standard code base, for example a good abstraction will provide error information returned from the third party api
Appropriate way to amend code
The third party api returns this response
{
"cod":401,
"message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."}
This should be your implementation
// Your fetch functon abstraction modified
require('dotenv').config()
const collect = async () => {
const key = process.env.REACT_APP_API_KE;
const res = await fetch(
`http://api.openweathermap.org/data/2.5/weather?q=london&appid=${key}`,
);
if (res.status !== 200) {
const error = await res.json();
throw {message: error.message,status:error.cod};
}
const data = await res.json();
return data;
};
Your app component should now be like this
import { useState } from 'react'
import { collect } from './utilities/collect'
require('dotenv').config()
function App() {
const [data, setData] = useState("")
const [error, setError] = useState({ error: false, status: "" })
const handleFetch = async () => {
try {
let newData = await collect()
setData(newData)
} catch(e){
setError({ error: e.message, status: e.status })
}
}
return (
<div className="App">
<h1>weather</h1>
<button onClick={handleFetch}>fetch</button>
</div>
);
}
export default App;
Since Nuxt's fetch hooks cannot run in parallel, I needed a way to cancel requests done in fetch hook when navigating to some other route so users don't have to wait for the first fetch to complete when landed on the homepage navigated to some other. So I found this approach: How to cancel all Axios requests on route change
So I've created these plugin files for Next:
router.js
export default ({ app, store }) => {
// Every time the route changes (fired on initialization too)
app.router.beforeEach((to, from, next) => {
store.dispatch('cancel/cancel_pending_requests')
next()
})
}
axios.js
export default function ({ $axios, redirect, store }) {
$axios.onRequest((config) => {
const source = $axios.CancelToken.source()
config.cancelToken = source.token
store.commit('cancel/ADD_CANCEL_TOKEN', source)
return config
}, function (error) {
return Promise.reject(error)
})
}
and a small vuex store for the cancel tokens:
export const state = () => ({
cancelTokens: []
})
export const mutations = {
ADD_CANCEL_TOKEN (state, token) {
state.cancelTokens.push(token)
},
CLEAR_CANCEL_TOKENS (state) {
state.cancelTokens = []
}
}
export const actions = {
cancel_pending_requests ({ state, commit }) {
state.cancelTokens.forEach((request, i) => {
if (request.cancel) {
request.cancel('Request canceled')
}
})
commit('CLEAR_CANCEL_TOKENS')
}
}
Now this approach works fine and I can see requests get canceled with 499 on route change, however, it is flooding my devtools console with "Error in fetch()" error. Is there some preferred/better way to do this?
Example of fetch hook here:
async fetch () {
await this.$store.dispatch('runs/getRunsOverview')
}
Example of dispatched action:
export const actions = {
async getRunsOverview ({ commit }) {
const data = await this.$axios.$get('api/frontend/runs')
commit('SET_RUNS', data)
}
}
Edit: I forgot to mention that I'm using fetch here with fetchOnServer set to False to display some loading placeholder to users.
The main problem is the flooded console with error, but I can also see that it also enters the $fetchState.error branch in my template, which displays div with "Something went wrong" text before route switches.
Edit 2:
Looked closer where this error comes from and it's mixin file fetch.client.js in .nuxt/mixins directory. Pasting the fetch function code below:
async function $_fetch() {
this.$nuxt.nbFetching++
this.$fetchState.pending = true
this.$fetchState.error = null
this._hydrated = false
let error = null
const startTime = Date.now()
try {
await this.$options.fetch.call(this)
} catch (err) {
if (process.dev) {
console.error('Error in fetch():', err)
}
error = normalizeError(err)
}
const delayLeft = this._fetchDelay - (Date.now() - startTime)
if (delayLeft > 0) {
await new Promise(resolve => setTimeout(resolve, delayLeft))
}
this.$fetchState.error = error
this.$fetchState.pending = false
this.$fetchState.timestamp = Date.now()
this.$nextTick(() => this.$nuxt.nbFetching--)
}
Have also tried to have everything using async/await as #kissu suggested in comments but with no luck :/
This hook for sending API calls works perfectly fine. It has been working fine for last 6 months.
I have no clue why it is failing for a single route.
All the errors returned from the API have the same structure :
[{ message: "Invalid username" }, { message: "Invalid password" }]
Error
Error: Objects are not valid as a React child (found: Error: [object Object]).
If you meant to render a collection of children, use an array instead.
Hook
export const useRequest = ({ url, method, body, onSuccess }) => {
const [errors, setErrors] = useState(null);
const makeRequest = async (e) => {
try {
setErrors(null);
const { data } = await axios[method](url, { ...body });
if (onSuccess) {
// will invoke callback function that is passed
onSuccess(data);
}
return data;
} catch (err) {
if (err && err.response?.data && err.response?.data.errors) {
// figuring out what is going wrong!
err.response?.data.errors
.map((each) => console.log(typeof each.message)); // returns string
setErrors(
// <== SHOWS ERROR HERE !!!!! WHY??!!
<ul className="errors-list">
{err.response?.data.errors.map((each, index) => (
<li key={index}>{each.message}</li>
))}
</ul>
);
}
}
};
return { makeRequest, errors };
};
This is the first time it is showing error, it has never in last 6 months behaved like this.
Please help!
I'm trying to apply the ajax method posted here: https://github.com/redux-observable/redux-observable/blob/master/docs/basics/Epics.md
import { ajax } from 'rxjs/ajax';
// action creators
const fetchUser = username => ({ type: FETCH_USER, payload: username });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
// epic
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`).pipe(
map(response => fetchUserFulfilled(response))
)
)
);
// later...
dispatch(fetchUser('torvalds'));
When trying this method, I get the message:
TypeError: Object(...)(...).pipe is not a function
So pipe doesn't appear to exist. (It concerns the second pipe after the ajax call).
How do I fix this?
I installed the following dependencies:
"rxjs": "^6.5.2",
"rxjs-compat": "^6.5.2",
Edit:
I changed my code to ajax.get and the calling code:
export const fetchTrendingEpic = action$ => action$.pipe(
ofType(FETCH_TRENDING_REQUEST),
mergeMap(async(action) => {
const res = await fetchPostStats(action.payload);
console.log(res);
res.pipe(
map(response => {
console.log('response', response);
setTrendingPlaces({trendingPlaces: response});
})
)
})
);
The res was properly printed (showing an observable), but now I get an error:
TypeError: Cannot read property 'type' of undefined
This is how I create my store in dev:
const createEnhancer = (epicMiddleware) => {
const middleware = [ epicMiddleware, createLogger() ];
let enhancer;
if (getEnvironment() === 'development') {
enhancer = composeWithDevTools(
applyMiddleware(...middleware),
// other store enhancers if any
);
};
export default (initialState) => {
const epicMiddleware = createEpicMiddleware();
const enhancer = createEnhancer(epicMiddleware);
const store = createStore(rootReducer, initialState, enhancer);
epicMiddleware.run(rootEpic);
return store;
}
Edit note:
This is code executed in NodeJS (SSR).
I'm struggling with this, don't really understand how this can be so hard to get working without error.
Can't quite see how the example code will ever work when ajax.getJSON returns a promise, not an Observable...
My service that called the ajax request was marked with async because the previous implementation used Axios before the refactoring.
This was the reason why my console logged a promise as return value of the function.
Removing async returns the Observable as expected.
As #Dez mentioned in the comments, there is also need to add return values.
And last but not least, rxjs ajax does not work in a NodeJS environment.
Therefore, based on this thread:
https://github.com/ReactiveX/rxjs/issues/2099
I have found that someone created the following npm package that I will try out shortly: https://github.com/mcmunder/universal-rxjs-ajax
Axios works fine with rxjs in nodejs, just doing something like this...
const { from } = require('rxjs');
const { map } = require('rxjs/operators');
const axios = require('axios');
const responsePromise = axios.get('https://jsonplaceholder.typicode.com/todos/1');
const response$ = from(responsePromise);
response$
.pipe(
map(response => ({ type: 'RESPONSE_RECEIVED', payload: response.data}))
)
.subscribe(console.log);