Using axios inside custom react hook - reactjs

I want to create a global axios instance with an interceptor which adds a header with access token.
The access token is stored in a cookie, but I can't figure out how to access the cookie inside the interceptor as it is not a react component.
I have tried creating the axios instance in a custom hook like this:
import React, { useEffect, useState } from "react";
import { useCookies } from "react-cookie";
import axios from "axios";
function useApiClient() {
const [cookies, setCookie] = useCookies(["token"]);
const [client, setClient] = useState(null);
useEffect(() => {
setClient(
axios
.create({
baseURL: "http://localhost:8080/api/",
responseType: "json",
})
.interceptors.request.use(
(config) => {
console.log("setting up");
if (cookies["token"] != null) {
config.headers.authorization = cookies["token"];
}
},
(error) => Promise.reject(error)
)
);
}, [client]);
return client;
}
export default useApiClient;
But when I try to call this using:
const client = useApiClient();
function attemptLogin() {
const credentials = {
username: username,
password: password,
};
client
.post("/authenticate", JSON.stringify(credentials), {
headers: {
"Content-Type": "application/json",
},
}).then(....)
I get below error:
TypeError: client.post is not a function
Can anyone help?

The problem is that you are trying to use react-cookie that provides an api for working with cookies in components. It may be fine for some cases, but for the case when you need to check cookies on every request it forces putting an axios instance somewhere at the top of components hierarchy and also makes it harder to use the instance out of components (most likely you'll pass the instance to outer functions).
What I suggest is to make an instance in a separate module, and use a different tool for getting cookies like universal-cookie.
// apiClient.js
import axios from "axios";
import Cookies from "universal-cookie";
const cookies = new Cookies();
export const apiClient = axios
.create({
baseURL: "http://localhost:8080/api/",
responseType: "json"
})
.interceptors.request.use(
config => {
console.log("setting up");
const token = cookies.get("token");
if (token != null) {
config.headers.authorization = token;
}
},
error => Promise.reject(error)
);
After that, you can use this instance everywhere in the app
import React from 'react';
import { apiClient } from "./apiClient";
export default ({ name }) => {
useEffect(() => {
apiClient.get(/*...*/).then(/*...*/);
});
return <h1>Hello {name}!</h1>;
}
If you prefer to stick to hooks you could wrap a client into one easily:
// apiClient.js
export function useApiClient() { return apiClient; }
Playgroung with full example

That is maybe because useEffect in useApiClient.js is async method.
Personally I see no harm in directly assigning headers to axios object. However I see you are using axios.create so I would assume you may be making calls to multiple Service APIs/endpoints and all do not need same headers. Anyways...
Suggestions
Make useApiClient a simple class which returns instance of axios with required headers. If not, I am seriously missing the point of using useEffect here.
Behavior
In each file you file call this and all will have separate instances of axios without polluting the global axios object.
useApiClient.js
function useApiClient {
return axios
.create({
baseURL: "http://localhost:8080/api/",
responseType: "json"
})
.interceptors.request.use(
config => {
console.log("setting up");
const token = cookies.get("token");
if (token != null) {
config.headers.authorization = token;
}
},
error => Promise.reject(error)
);
}
}
export default useApiClient;
Make a Factory Class which returns axios instances based on the input parameter (to assign different headers based on parameter passed and return the instance). You may also have it as Singleton if feasible.
Hope it helps.

Related

Redux-Saga axios api call get request with access_token does not work. Why is that?

This is a react project using axios, redux, and redux-sagas.
I am getting the following exception when trying to fetch all records from a table that are guarded. I am using JWT on my laravel backend. Login token is properly set inside local storage, I am guessing, it does not get passed properly.
TypeError: Cannot read properties of null (reading 'authService')
at getAll (PostService.js:11:1)
at runCallEffect (redux-saga-core.esm.js:524:1)
at runEffect (redux-saga-core.esm.js:1204:1)
at digestEffect (redux-saga-core.esm.js:1271:1)
at next (redux-saga-core.esm.js:1161:1)
at proc (redux-saga-core.esm.js:1108:1)
at redux-saga-core.esm.js:585:1
at immediately (redux-saga-core.esm.js:56:1)
at runForkEffect (redux-saga-core.esm.js:584:1)
at runEffect (redux-saga-core.esm.js:1204:1)
This is my page where I want to fetch every record from a table:
import { useEffect } from "react";
import { getAllPostsAction } from "../../store/posts/slice";
import { useSelector, useDispatch } from "react-redux";
import { makeSelectPosts } from "../../store/posts/selector";
export const PostsPage = () => {
const posts = useSelector(makeSelectPosts);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getAllPostsAction());
}, [dispatch]);
console.log(posts);
return <h1>PostsPage</h1>;
};
Saga:
import { call, put, all, fork, takeEvery } from "redux-saga/effects";
import { postService } from "../../services/PostService";
import { setAllPostsAction } from "./slice";
import { setSinglePostAction } from "./slice";
function* getPosts() {
try {
const response = yield call(postService.getAll);
yield put(setAllPostsAction(response.data));
} catch (err) {
console.error(err);
}
}
function* getPostsSagaWatcher() {
yield takeEvery("posts/getAllPostsAction", getPosts);
}
...
// I forked every watcher from a file down bellow in a file
This is how I fetch everything with axios:
import { authService } from "./AuthService";
import axios from "axios";
class PostService {
constructor() {
this.authService = authService;
}
async getAll() {
return await axios.get("http://127.0.0.1:8000/api/posts", {
headers: this.authService.getHeaders(),
});
}
...
getHeaders() looks like this:
getHeaders() {
return {
Authorization: `Bearer ${window.localStorage.getItem("loginToken")}`,
};
}
I've tried to fetch every record in a table and setting it to component state (useState hook) on component mount which worked like a charm. So the issue is most likely the way I dispatch sagas.
yield call(postService.getAll);
Since you havn't specified what the value of this should be, postService.getAll gets called using undefined for this. So when the function tries to access this.authService, it throws an error.
call has several alternate ways you can use it to specify the value of this. All of the following will work:
yield call([postService, postService.getAll])
// or
yield call([postService, 'getAll'])
// or
yield call({ context: postService, fn: postService.getAll })
See also: https://redux-saga.js.org/docs/api/#callfn-args

Use react hook inside a axios config file

i have a axios config file, and i call react hook {Auth Context} in that file with the purpose to fetch the token in react context api. but i got an error like this "React Hook 'useAuth' cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function"
AuthContext.js
import React, { useContext, createContext, useState } from "react";
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentToken, setCurrentToken] = useState("");
const [isAuth, setIsAuth] = useState(false);
function login(token) {
setCurrentToken(token);
setIsAuth(true);
}
const value = {
login,
currentToken,
isAuth,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
and my axios config file something like this
AxiosConfig.js
import axios from "axios";
import { useAuth } from "./AuthContext";
const { currentToken } = useAuth(); //my Reeact Context
export default axios.create({
baseURL: "http://127.0.0.1:8000",
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${currentToken}`,
},
});
what is the best way to achieve that goal ?
Thank you in advance
you can create an api and set the token when it's needed, since your api is the same throughout the code, this will work.
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_END_POINT,
});
export const setApiToken = (token: string) => {
api.defaults.headers.common["Authorization"] = `bearer ${token}`;
};
As several users have pointed out you cannot use a hook outside the React component. I have very similar set up in my project and store my API key in the local storage, this also adds a benefit of persisting API key.
// src/api/index.js
import axios from "axios";
import { API_URL, LOCALSTORAGE_API_KEY } from "../utils/constants";
export const signedRequest = () => {
const apiKey = localStorage.getItem(LOCALSTORAGE_API_KEY);
return axios.create({
baseURL: API_URL,
headers: apiKey ? { "Authorization": `Bearer: ${apiKey}` } : {},
});
};
export const unsignedRequest = () => {
const request = getSignedRequest();
request.defaults.headers = {};
return request;
};
Usage:
signedRequest().get<AdminDashboard.UsersResponse>("/dashboard");
And if you need to perform a request w/o Authorization you can do it:
unsignedRequest().get<AdminDashboard.UsersResponse>("/public");
Hooks were not meant to be used outside of a component, and useContext and useAuth(which uses useContext) is a hook. Here's a quote from freecodecamp:
You can not use hooks outside a component function, it is simply how they work. But, you can make a composition of hooks. React relies on an amount and order of how hooks appear in the component function. So don't even think of wrapping those calls with conditional logic of some sort.
As you read above, you are not supposed to do this in react applications.

Duplicate axios calls in react

I am working on a create-react-app generated app and I have an API call to a local JSON file.
Somehow this code generates 8 API calls in a roll.
import '#css/request.scoped.css';
import { useState } from 'react';
import { getAllData } from '#api/api';
function Request() {
let [user, setUser] = useState('');
function changeUser(data) {
setUser(data);
}
getAllData().then((reply) => {
changeUser(reply.data[0].item_name);
});
return <>{user}</>;
}
export default Request;
and here is my Axios instance
import axios from "axios";
const request = axios.create({
baseURL:'./example.json',
timeout: 20000,
});
export const getAllData = () => {
return request({
method: 'get',
url: '',
});
};
Can someone tell me why this happens?
I suspect the most likely cause is that the component is being re-rendered, and currently the logic is to make the API call on every render.
You can set the logic to occur only on the first render of the component with useEffect:
import { useState, useEffect } from 'react';
// then...
useEffect(() => {
getAllData().then((reply) => {
changeUser(reply.data[0].item_name);
});
}, []);
The empty dependency array passed to useEffect means it would open happen once when the component is first loaded, not on subsequent renders. Any dependency you add to that array would cause the operation to be invoked again any time that dependency changes between renders.

How to use axios to make requests in react

I started learning React about 3 or 4 months ago. Initially went through Codecademy's course, and have since polished up a lot with other courses and tutorials, the main of which used Axios for get requests without really explaining Axios. I decided to go back to the "Ravenous" yelp clone project at Codecademy, the instructions of which are for class components, and thought it would be a nice challenge to complete that same project but with functional components. Everything was perfect until it was time to wire up the Yelp api, and given my lack of knowledge with axios and requests in general, I can't pinpoint what I'm doing wrong. I'm almost certain that I'm doing something wrong in Yelp.js, but I can't be sure because I'm getting a type error that "fetchRestaurants" is not a function in App.js, no matter which way I call it. I think it involves Promises, and I tried using async/await on searchYelp, but this part is all new to me.
Here's Yelp.js:
import axios from "axios";
const KEY = "RwNYGeKdhbfM1bo6O8X04nLNR7sKjIXyAQ-Fo-nz-UdNHtSKDAmHZCc9MSiMAVmNhwKrfcukZ0qlHF1TdVIRSrgak3-oHVNmGD5JPR4ysxhfGd1hgOllt83H1i44X3Yx";
const fetchRestaurants = (term, location, sortBy) => { const corsApiUrl = "https://cors-anywhere.herokuapp.com/"; return () => {
axios
.get(
`${corsApiUrl}https://api.yelp.com/v3/businesses/search?categories=restaurants&location=${location}&term=${term}&sort_by=${sortBy}`,
{
headers: {
Authorization: `Bearer ${KEY}`,
},
}
)
.then(res => res.data.businesses)
.catch(error => console.log(error.response)); }; };
export default fetchRestaurants;
And here's App.js:
import React, { useState } from "react";
import BusinessList from "./BusinessList";
import SearchBar from "./SearchBar";
import Yelp from "./Yelp";
import "./App.css";
const App = props => {
const [businesses, setBusinesses] = useState([]);
const searchYelp = (term, location, sortBy) => {
const businessList = Yelp.fetchRestaurants(term, location, sortBy);
setBusinesses(businessList);
};
return (
<div className="App">
<h1>Ravenous</h1>
<SearchBar searchYelp={searchYelp} />
<BusinessList businesses={businesses} />
</div>
);
};
export default App;
Apologies in advance if this was supposed to be posted somewhere else. Noob probs.
you did not save the response anywhere. .then(res => res.data.businesses) here the response should've been saved somewhere, either a varibale or state. also your function responseble for the axios request doesn't return anything.
It's not an issue with axios itself, but how you use it in your React application.
Yelp.js: fetchRestaurants() should return a proper value (promise in this case) :
const fetchRestaurants = (term, location, sortBy) => {
return axios
.get(url, options)
.then(res => res.data.businesses)
.catch(...)
}
App.js: All API calls should be done using useEffect hook:
useEffect(() => {
Yelp.fetchRestaurants(term, location, sortBy)
.then(businessList => setBusinesses(businessList));
}, [term, location, sortBy]);

Standalone APIService needs access to redux or NextJS ctx for getting token

I have a standalone APIService.js which is used across my NextJS Application, it does a few things, but the main thing is setting the BASE_URL and if the user has logged in (should have a token in a cookie, or in a header cookie - this then dispatches into redux).
Basically I think I need this APIService to either; detect when there is a cookie on the machine (it does) or if its server-side rendered and get it from the header; or what I think is better - is to listen for changes to redux, and take the token from the redux store.
This is my APIService here;
import axios from 'axios';
import { Cookies } from 'react-cookie';
import { API_URL } from './constants';
const config = {
baseURL: API_URL,
};
const cookies = new Cookies();
const APIService = axios.create(config);
APIService.interceptors.request.use((request) => {
const token = cookies.get('token');
request.headers.Authorization = token ? `Bearer ${token}` : '';
return request;
});
export default APIService;
to give an example of the NextJS page usage;
I have this function in my _app.js
static async getInitialProps({ Component, ctx }) {
if (ctx.isServer) {
if (ctx.req.headers.cookie) {
const token = getCookie('token', ctx.req);
console.log('WHOAMI ', token);
ctx.store.dispatch(reauthenticate(token));
}
} else {
const { token } = ctx.store.getState().authentication;
console.log('WHOAMI2 ', token);
ctx.store.dispatch(reauthenticate(token));
}
return {
pageProps: Component.getInitialProps
? await Component.getInitialProps(ctx)
: {},
};
}
Do I need to connect() the APIService to Redux - if so how can I? I generally import the APIService into other components where I need to use my API, so i dont want to make drastic changes to it unless absolutely necessary.
I did add a context hook to the page itself to call the API client side, but this only works as a workaround for the fact I cannot get access to the header or token in the APIService as it has no context access.
So I could add a function like this to the pages, but I know its a horrid solution.
useEffect(() => {
fetchData();
}, []);
I find that if you want something to always be up-to-date with Redux, it's best to write a reselect selector for it. Something like:
import { createSelector } from 'reselect';
const getAuth = (state:AppState) => state.auth;
const getToken = createSelector(getAuth, (auth) => auth.token);
// Easy syntax for class-based services
const getSimpleService = createSelector(getToken, (token) => {
return new Service(token);
})
// Your sample APIService
const getApiService = createSelector(getToken, (token) => {
const config = {
baseURL: API_URL,
};
const APIService = axios.create(config);
APIService.interceptors.request.use((request) => {
request.headers.Authorization = token ? `Bearer ${token}` : '';
return request;
});
return APIService;
})
Now your components can get the API through connect()! My experience is with React rather than Next, but the Redux code ought to be pretty much the same. Refactoring your API service into a class might also be convenient, as you could keep all the axios setup in its constructor.
In our application, what we did is we kept the services, particularly the Authentication Service, decoupled from Redux.
What I would suggest is to keep using it in the getInitialProps instead of connecting it to redux directly. You can still use it on redux without any problems too as you only need access to the Cookies to make the login check work. It will do the job and will keep things simple.
I recommend context with hooks
Controllers
import React from 'react';
import { NextPage, NextPageContext } from 'next';
import Layout from '#layouts/index';
import { AuthStore, AuthStoreProps } from '#src/stores';
interface HomeProps {
cookie?: string;
}
const HomeController: NextPage<HomeProps> = ({ cookie }) => {
const authStore = React.useContext<AuthStoreProps>(AuthStore);
const isAuthorized = authStore.getAuthentication(cookie);
return <Layout>Home</Layout>;
};
HomeController.getInitialProps = async ({ req }: NextPageContext) => {
const cookie = req ? req.headers.cookie : '';
return { cookie };
};
export default HomeController;
Hooks
Your API Services
Opinions
You can control store values using class and context.
and then This could be reused in react or js projects.
React was concentrated on functional programming with hooks. you know.

Resources