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 });
Related
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.
I've been working on a Next.JS web application for the past couple of days but I've reached a problem. The app has an API call (/api/settings) which returns some settings about the application from the database. Currently, I have a function which returns these settings and access to the first component:
App.getInitialProps = async () => {
const settingsRequest = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/settings`
);
const settingsResponse = await settingsRequest.json();
return { settings: settingsResponse };
};
This does work and I am able to pass in settings to components but there are two problems with this:
I need to nest the prop through many components to reach the components that I need
This request runs every time a page is reloaded/changed
Essentially, I need to create a system that does this:
runs a function in the _app.tsx getInitialProps to check if the data is already in localStorage, if not make the API request and update localStorage
have the localStorage value accessible from a custom hook.
Right now the problem with this is that I do not have access to localStorage from the app.tsx getInitialProps. So if anyone has an alternative to run this function before any of the page loads, please let me know.
Thanks!
I found a solution, it might be a janky solution but I managed to get it working and it might be useful for people trying to achieve something similar:
First we need to create a "manager" for the settings:
export const checkIfSettingsArePresent = () => {
const settings = localStorage.getItem("app_settings");
if (settings) return true;
return false;
};
export const getDataAndUpdateLocalStorage = async () => {
const r = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/settings`);
const response = await r.json();
localStorage.setItem("app_settings", JSON.stringify(response));
};
With that created we can add a UseEffect hook combined with a useState hook that runs our function.
const [doneFirst, setDoneFirst] = useState<boolean>(false);
useEffect(() => {
const settingsPreset = checkIfSettingsArePresent();
if (performance.navigation.type != 1)
if (settingsPreset) return setDoneFirst(true);
const getData = async () => {
await getDataAndUpdateLocalStorage();
setDoneFirst(true);
};
getData();
}, []);
//any other logic
if (!doneFirst) {
return null;
}
The final if statement makes sure to not run anything else before the function.
Now, whenever you hot-reload the page, you will see that the localStorage app_settings is updated/created with the values from the API.
However, to access this more simply from other parts of the app, I created a hook:
import { SettingsType } from "#sharex-server/common";
export default function useSettings() {
const settings = localStorage.getItem("app_settings") || {
name: "ShareX Media Server",
};
//#ts-ignore
return JSON.parse(settings) as SettingsType;
}
Now I can import useSettings from any function and have access to my settings.
const [category, setCategory] = useState("general")
const news = await axios.get(`https://newsapi.org/v2/top-headlines?country=in&apiKey=64968be4903a4a979fe05c58a3355a73
&category=${category}`);
**As I am fetching API but its not fetching as is shows an empty array can anyone tell me where I am I going wrong **
You can not use the response of an async function directly in your React functional component. You have to use a state which holds your news. If you call setNews React automatically rerenders your component with the new news data.
export function News() {
const [category, setCategory] = useState("general");
const [news, setNews] = useState([]);
// fetch news everytime the category changes
useEffect(() => {
async function fetchNews() {
try {
const url = `https://newsapi.org/v2/top-headlines?country=in&apiKey=64968be4903a4a979fe05c58a3355a73&category=${category}`;
const response = await axios.get(url);
console.log(response);
setNews(response.data.articles);
} catch (errorWhileFetchingNews) {
console.log("error while fetching news", errorWhileFetchingNews);
}
}
fetchNews();
}, [category]);
// render the news
return (
<div>
{
news.map((article, i) => {
return <div key={i}>{article.title}</div>;
})
}
</div>
);
}
EDIT:
CAUTION: The CORS issues seem to appear only in my codesandbox example. If the example above runs on localhost:3000 in a normal React app (create-react-app) it works like it should. So you might ignore the following description.
Unfortunately the server newsapi.org doesn't send CORS headers. So you are not allowed to call this service directly via AJAX requests (axios, fetch, ...). You either find a way to enable CORS on this site (because you have an API key you may be able to administrate something?) or you find an other service that supports CORS or you have to send your request through a proxy. The proxy/backend then have to run on the same domain (host + port) like your frontend or the proxy must handle all the CORS header stuff. There are also questions on stackoverflow that have the same issue with newsapi.org but I am afraid that there is no easy solution/workaround for this.
I have setup a working example with jsonplaceholder.typicode.com (supports CORS) instead of newsapi.org.
See here: https://codesandbox.io/s/white-wildflower-su5vd?file=/src/News.js
Just in case the example is not reachable anymore, here is the code:
import { useState, useEffect } from "react";
import axios from "axios";
export function News(props) {
const [category, setCategory] = useState("general");
const [news, setNews] = useState([]);
// fetch news everytime the category changes
useEffect(() => {
async function fetchNews() {
try {
const url = "https://jsonplaceholder.typicode.com/comments";
const response = await axios.get(url);
console.log(response);
setNews(response.data);
} catch (errorWhileFetchingNews) {
console.log("error while fetching news", errorWhileFetchingNews);
}
}
fetchNews();
}, [category]);
// render the news
return (
<div>
{
news.map((article) => {
return <div key={article.id}>{article.name}</div>;
})
}
</div>
);
}
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.
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;