Cancel requests on route change (React/Redux/Axios) - reactjs

Is there convenient way to cancel all sending request on any route changes using axios, redux-thunk, redux? I know that axios has cancellation token which should be added to every request and I can call source.cancel(/* message */) to cancel it.
P.S. Currently I handle this in componentWillUnmount. Maybe there is something better?

The easiest way which I found is to store source in state, and use source.cancel if request is sending.
componentWillUnmount() { if(isLoading) { source.cancel() } }

Very simple solution would be to declare isMounted variable and set it to false when component unmounts.
Other way of handling this issue is, (I'm not sure about axios but) XMLHttpRequest has abort method on it. You could call xhr.abort() on componentWillUnmount.
Check it here: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort
And finally there is an industry solution :) from Netflix UI engineers. They have written a redux middleware for RxJS and using RxJS operators to fire up and cancel the requests.
The library is called redux-observable. I recommend to take a look at examples: https://redux-observable.js.org/docs/recipes/Cancellation.html
You can watch the talk about it here: https://youtu.be/AslncyG8whg

I found a simple solution (without redux) to do that. All you need is a cancelToken in your axios requests. After, use the useHook to detect route changes. Then, cancel the requests with the TOKEN when the route is unmounted and to generate a new TOKEN to make a new request. See the Axios Doc to more details (https://github.com/axios/axios).
Route.tsx file
import React, { useEffect } from 'react';
import { Route, RouteProps, useLocation } from 'react-router-dom';
import API from 'src/services/service';
const CustomRoute = (props: RouteProps) => {
const location = useLocation();
// Detect Route Change
useEffect(() => {
handleRouteChange();
return () => {
handleRouteComponentUnmount();
};
}, [location?.pathname]);
function handleRouteChange() {
// ...
}
function handleRouteComponentUnmount() {
API.finishPendingRequests('RouteChange');
}
return <Route {...props} />;
};
export default CustomRoute;
Service.ts file
import { Response } from 'src/models/request';
import axios, {AxiosInstance, AxiosResponse } from 'axios';
const ORIGIN_URL = 'https://myserver.com'
const BASE_URL = ORIGIN_URL + '/api';
let CANCEL_TOKEN_SOURCE = axios.CancelToken.source();
function generateNewCancelTokenSource() {
CANCEL_TOKEN_SOURCE = axios.CancelToken.source();
}
export const axiosInstance: AxiosInstance = axios.create({
baseURL: BASE_URL,
});
const API = {
get<DataResponseType = any>(
endpoint: string,
): Promise<AxiosResponse<Response<DataResponseType>>> {
return axiosInstance.get<Response<DataResponseType>>(endpoint, {
cancelToken: CANCEL_TOKEN_SOURCE.token,
});
},
// ...Another Functions
finishPendingRequests(cancellationReason: string) {
CANCEL_TOKEN_SOURCE.cancel(cancellationReason);
generateNewCancelTokenSource();
},
};
export default API;

Related

How to render [slug].js page after fetching data Next.js

I am trying to create a logic for my blog/:post page in Next.js but I cannot seem to figure out how.
The idea is to:
Fetch the url (using useRouter)
Call API (it is a headless CMS) to get the info of the post
Render the post
What I have right now is:
[other imports ...]
import { useEffect, useRef, useState } from "react";
import { useRouter } from 'next/router'
const apikey = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY;
const butter = require('buttercms')(apikey);
function BlogPost(props) {
const router = useRouter()
const { slug } = router.query
const [blogPost, setBlogPost] = useState({})
// Function to the blog post
function fetchBlogPost() {
butter.post.retrieve(slug)
.then(response => {
const blogPostData = response.data.data
setBlogPost(blogPostData)
})
}
useEffect(() => {
// We need to add this if condition because the router wont grab the query in the first render
if(!router.isReady) return;
fetchBlogPost()
}, [router.isReady])
return (
<>
# Render post with the data fetched
</>
)
}
export default BlogPost;
But this is not rendering everything (the image is not being rendered for example). I believe it is because of the pre-render functionality that Next.js has. Also I have been reading about the getStaticProps and getStaticPaths but I am unsure on how to use them properly.
Any guidance will be welcome. Thanks!
If you're using next.js then you are on track with getStaticProps being your friend here!
Essentially getStaticProps allows you to take advantage of ISR to fetch data on the server and create a static file of your page with all of the content returned from the fetch.
To do this you'll need to make an adjustment to your current architecture which will mean that instead of the slug coming in from a query param it will be a path parameter like this: /blogs/:slug
Also this file will need to be called [slug].js and live in (most likely) a blogs directory in your pages folder.
Then the file will look something like this:
import { useEffect, useRef, useState } from "react";
import { useRouter } from 'next/router'
const apikey = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY;
const butter = require('buttercms')(apikey);
export const getStaticPaths = async () => {
try {
// You can query for all blog posts here to build out the cached files during application build
return {
paths:[], // this would be all of the paths returned from your query above
fallback: true, // allows the component to render with a fallback (loading) state while the app creates a static file if there isn't one available.
}
} catch (err) {
return {
paths: [],
fallback: false,
}
}
}
export const getStaticProps = async ctx => {
try {
const { slug } = ctx.params || {}
const response = await butter.post.retrieve(slug)
if(!response.data?.data) throw new Error('No post data found') // This will cause a 404 for this slug
return {
notFound: false,
props: {
postData: response.data.data,
slug,
},
revalidate: 5, // determines how long till the cached static file is invalidated.
}
} catch (err) {
return {
notFound: true,
revalidate: 5,
}
}
}
function BlogPost(props) {
const {isFallback} = useRouter() // We can render a loading state while the server creates a new page (or returns a 404).
const {postData} = props
// NOTE: postData might be undefined if isFallback is true
return (
<>
# Render post with the data fetched
</>
)
}
export default BlogPost;
In any case, though if you decide to continue with rendering on the client instead then you might want to consider moving your fetch logic inside of the useEffect.

fetching data using redux

I'm new to redux and I'm trying to fetch some data in my slice file, then put it in my state to use it across my app.
so I read the documentation in redux website. it says:
"Let's start by adding a thunk that will make an AJAX call to retrieve a list of posts. We'll import the client utility from the src/api folder, and use that to make a request to '/fakeApi/posts'."
and the code is:
import { createSlice, nanoid, createAsyncThunk } from '#reduxjs/toolkit'
import { client } from '../../api/client'
const initialState = {
posts: [],
status: 'idle',
error: null
}
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
const response = await client.get('/fakeApi/posts')
return response.data
})
so now I'm confused. How can I create the client file to use it?
and then, how can I save it in my state to re-use it?
it would be a huge help if you guide me!
Oh yeah now i understand what you want, client is just like fetch, i assume they are using axios , then in client.js file they are exporting axios at the end.
An example, client.js file:
import axios from "axios";
export const client = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
Then import it whereever you want:
import { client } from '../../api/client'
But you can also use axios directly without creating any instances .
As i said before you may use fetch instead, or any other http request package, but actually with axios you have more power and you can easily find a lot of documentations
You can get your reducer state with the use of useSelector and make sure
you write correct reducer state name instead of counter.
const count = useSelector((state) => state.counter.value);
You can dispatch your action by useDispatch hook and make sure you write correct action name instead of decrement.
const dispatch = useDispatch();
dispatch(decrement())
import of this two hooks
import { useSelector, useDispatch } from 'react-redux';
You can save your api response in posts state like this:
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async ()
=> {
const response = await client.get('/fakeApi/posts')
state.posts = response.data
})
Full demo example of redux-toolkit: https://github.com/priyen27/redux-toolkit-demo
thanks for these answers. so I used it and now I get this error:
XHR failed loading: GET "https://excuser.herokuapp.com/v1/excuse"
that's the api link I want.
I used fetch as well and it worked correctly, but I don't know how to store it's data in my state. I used this function:
export async function fetchMyAPI() {
let response = await fetch(`https://excuser.herokuapp.com/v1/excuse`)
let data = await response.json()
return data[0].excuse
}
when I use it in my component and at the end I set is to some const it works perfect. but when I use it directly (like setData(fetchMyAPI())) it returns a promiss and I can't access data. what should I do? how can I store it in my state?
note that I fetch data in my slice component.
my final get api function:
const fetchExcuses = createAsyncThunk('excuses/fetchExcuses', async () => {
const response = await client.get('excuser.herokuapp.com/v1/excuse')
let data = await response.json()
})

React Axios Interceptors jumping in late

I'm working on a small react project and using axios interceptors to catch whether I'm in a localhost development environment or on the production deployed website.
What's happening is that when people sign up to my site, they click on the confirmation email link, and land on a certain "state" or whatever you call it or the application where the axios interceptor doesn't know what environment I'm on, and for a split second the wrong api call is made, to the right after it calling the right api uri.
Let me show this with some code:
export const App = () => {
useEffect(() => {
axios.interceptors.request.use((req) => {return { ...req, url: getBaseUri() + req.url
};})}, []);
return (
<div className="App">
<Routes />
</div> 
)}
And then the methods:
const devUriBase = "http://localhost:8080";
const prodUriBase = "https://my-website.herokuapp.com";export function getBaseUri() {
return window.location.host.includes("localhost") ? devUriBase : prodUriBase;
}
Then on the verification page component, where I make the api call itself, for a moment the api call is made to the incorrect url so for a split second the component is shown, then it seems the useEffect jumps in and the api call is made again. None of the combinations I tried worked. I tried to make a config component and through children have the axios interceptor, putting this in the index instead, and I don't know what else. I've been struggling with this for 3 days, I thought it was time to ask.
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import axios from 'axios';
import { useToken } from '../../auth/useToken';
import { EmailVerificationSuccess } from './EmailVerificationSuccess';
import { EmailVerificationFail } from './EmailVerificationFail';
export const EmailVerificationLandingPage = () => {
const { verificationString } = useParams();
const [, setToken] = useToken();
const [state, setState] = useState('loading');
useEffect(() => {
const loadVerification = async () => {
try {
const response = await axios.put('/api/verify-email', { verificationString });
const { token } = response.data;
setToken(token);
setState('success');
} catch (e) {
setState('error');
}
}
loadVerification();
}, [setToken, verificationString]);
if (state === 'loading') return <p>Cargando...</p>;
if (state === 'error') return <EmailVerificationFail />
return <EmailVerificationSuccess />
I appreciate your help.
This did it.
When you add request interceptors, they are presumed to be asynchronous by default. This can cause a delay in the execution of your axios request when the main thread is blocked (a promise is created under the hood for the interceptor and your request gets put on the bottom of the call stack). If your request interceptors are synchronous you can add a flag to the options object that will tell axios to run the code synchronously and avoid any delays in request execution.
axios.interceptors.request.use(function (config) {
config.headers.test = 'I am only a header!';
return config;
}, null, { synchronous: true });

Why does my Axios Response return a 404 even though I am setting the mock response?

I am trying to mock my REST requests for a react/ts project when testing in Storybook using Axios. Even though I am setting the response to an array object, it still seems to be responding with a "Request failed with status code 404" status.
Here is my component making the REST call: TestPrompt.tsx
const onClickHandler = () => {
requestOrMock("http://localhost:9002/projectPlan/projectTasks?project=FAKEWID")
}
Here is the method my TestPrompt component is using to make the request: UtilityFunctions.ts
import axios from 'axios';
export const axiosMock = axios.create();
export const requestOrMock = async (uri: string) => {
const response = axiosMock.get(uri);
return response;
}
Here is my test that is mocking the response: Prompt.stories.tsx
import * as React from "react";
import {storiesOf} from '#storybook/react';
import TestPrompt from "../components/common/Prompt";
import MockAdapter from 'axios-mock-adapter';
import { axiosMock } from "../utils/utilityFunctions";
const mock = new MockAdapter(axiosMock);
const blankPromptRequestUri = "http://localhost:9002/projectPlan/projectTasks?project=FAKEWID";
const footballTeams = [
{
"descriptor": "New England Patriots",
"id": "NewEnglandPatriots"
},
{
"descriptor": "Seattle Seahawks",
"id": "SeattleSeahawks"
}
];
storiesOf('Components/MultiSelect', module)
.add('Prompt1', () => {
mock.onGet(blankPromptRequestUri).reply(200, footballTeams);
return (
<TestPrompt/>
);
})
When I click on this component in storybook, it sends out the request to the designated url, but it gets the 404 response rather than the footballTeams object I have specified. Any idea what I have done wrong? Thanks for your help.
If I get your problem correctly, you need to call onGet() of mock to setup the mock end point and then send a request to that end point.
mock.onGet("/teams").reply(200, footballTeams);
storiesOf('Components/MultiSelect', module)
.add('Prompt1', () => {
axios.get("/teams")
.then(res => console.log(res.data))
return (
<TestPrompt/>
);
})
The requests that were being made were being made relative to the host, so rather than "http://localhost:9002/projectPlan/projectTasks?project=FAKEWID" being sent, it was actually "/projectPlan/projectTasks?project=FAKEWID". It is likely that you will only need to pass in the routes here.

How to dispatch data to redux from the common api request file?

I have created a common js file to call the service requests. I want to store the fetched data in my redux managed store. But I am getting this error saying Invalid hook call. Hooks can only be called inside of the body of a function component.I think this is because I am not using react-native boilerplate for this file. But the problem is I don't want to I just want to make service requests and responses.
import { useDispatch } from "react-redux";
import { addToken } from "../redux/actions/actions";
const { default: Axios } = require("axios");
const dispatch = useDispatch();
const handleResponse=(response, jsonResponse)=> {
// const dispatch = useDispatch(); //-----also tried using dispatch here
const jsonRes = jsonResponse;
const { status } = response;
const { errors } = Object.assign({}, jsonRes);
const resp = {
status,
body: jsonResponse,
errors,
headers: response.headers,
};
console.log(resp, 'handle response');
return await dispatch(addToken(resp.body.token))
};
const API = {
makePostRequest(token) {
Axios({
url: URL,
...req,
timeout: 30000
}).then(res =>
console.log('going to handle');
await handleResponse(res, res.data)
})
}
export default API
I know there would be some easy way around but I don't know it
Do not use useDispatch from react-redux, but dispatch from redux.
You need to use redux-thunk in your application.
Look at the example in this article Redux Thunk Explained with Examples
The article has also an example of how to use redux with asynchronous calls (axios requests in your case).
I suggest to refactored your api to differentiate two things:
fetcher - it will call your api, e.g. by axios and return data in Promise.
redux action creator (thunk, see the example in the article) - it will (optionally) dispatch REQUEST_STARTED then will call your fetcher and finally will dispatch (REQUEST_SUCCESS/REQUEST_FAILURE) actions.
The latter redux action creator you will call in your react component, where you will dispatch it (e.g. with use of useDispatch)

Resources