Duplicate axios calls in react - reactjs

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.

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

How to avoid error : Invalid hook call. Hooks can only be called inside of the body of a function component?

I have separate file where I do my get and post axios calls . So in this file I have this code
import { useSelector, useDispatch } from "react-redux";
import __ from 'lodash'
import axios from "axios";
import { getPersistedToken } from "src";
import { PROCESS_STATUS } from "src/model";
export const getAxiosQuery = (endpoint) => async () => {
const dispatch = useDispatch()
console.log(dispatch)
return await axios.get(endpoint, {
headers: {
'Authorization': 'Bearer ' + getPersistedToken(),
}
}).then((res) => {
dispatch(processStatusAction(PROCESS_STATUS.DONE))
return res
},
).catch(error => {
dispatch(processStatusAction(PROCESS_STATUS.FAIL))
});
}
Is there any way please to allow using react hooks in my function ? because I won't pass dispatch variable in parametres of this function in all my calls
Is there any way please to allow using react hooks in my function ? because I won't pass dispatch variable in parametres of this function in all my calls
No, there's no way that you can use a hook in your function. You can only use hooks in (functional) React components. Information here.
What you can do in your case, is create a custom hook (say useAxiosQuery) which retrieves the dispatch function and automatically injects it, such as:
const useAxiosQuery = (endpoint) => {
const dispatch = useDispatch();
return getAxiosQuery(endpoint, dispatch);
}

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.

Invalid hook call. Hooks can only be called inside of the body of a function component when i call useQuery in useEffect

I am using apollo-graphql in my react project and i am getting error of
Invalid hook call. Hooks can only be called inside of the body of a function component
Here is my code for this
import React, { useEffect, useState, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
// **************** COMPONENTS ****************
import { GET_MORTGAGE_JOURNEY } from "../../../../Graphql/Journeys/query";
export default function index() {
const insuranceId = useSelector((state) => state.mortgage.insuranceId);
// Panels Heading to show on all panels
useEffect(() => {
if (insuranceId) {
getMortgageData(insuranceId);
}
}, [insuranceId]);
function getMortgageData(insuranceId) {
const { loading, error, data } = useQuery(GET_MORTGAGE_JOURNEY, {
variables: { id: insuranceId },
});
console.log(data);
}
return <section className="mortage-journey"></section>;
}
Once i run this i get the error, I know that useQuery itself is a hook and i cant call it from inside useEffect, but then what should be the workaround for this as i need insuranceId from my redux state first and send it to the query.
Thanks !
You are breaking the rule of hooks when you call it from any place other than the top level of a React component.
useEffect takes a callback function, and you are calling your hook from that. It is a problem.
I found this skip option in useQuery which helps you call useQuery conditionally.
useEffect(() => {
const { loading, error, data } =
useQuery(GET_MORTGAGE_JOURNEY, {
variables: { id: insuranceId },
skip : (!insuranceId)
});
}, [insuranceId]);
Any time insuranceId changes your callback runs, so it is run after mount once and then on subsequent changes.
Try using refetch. Something like this:
const { data, refetch } = useQuery(MY_QUERY);
useEffect(() => {
refetch();
}, id);
You can use refetch wherever you like, in useEffect or a button click onclick handler.
Alternatively, you could use useLazyQuery if you don't want it to run the first time:
const [goFetch, { data }] = useLazyQuery(MY_QUERY);
Now you can use goFetch or whatever you want to call it wherever you like.
Your whole example might look like:
import React, { useEffect, useState, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { GET_MORTGAGE_JOURNEY } from "../../../../Graphql/Journeys/query";
export default function index() {
const insuranceId = useSelector((state) => state.mortgage.insuranceId);
const { loading, error, data, refetch } = useQuery(GET_MORTGAGE_JOURNEY, {
variables: { id: insuranceId },
});
useEffect(() => {
if (insuranceId) {
refetch({id: insuranceId});
}
}, [insuranceId]);
return <section className="mortage-journey"></section>;
}

Mocking external class method inside React component with jest

I'm trying to test component method, which inside performing network call to external resources. After reading docs I still can't figure out how to do so. Can anyone help? Here is my code(some parts hidden for brevity):
My component:
import React from 'react'
import ResourceService from '../../modules/resource-service'
export default class SliderComponent extends React.Component {
setActiveSlide = (activeSlide) => {
ResourceService.getData({
id: activeSlide,
}).then((data) => {
if (data) {
this.setState({
data,
})
}
})
}
}
Resource service:
import axios from 'axios'
export default class ResourceService {
static getData(params) {
return axios.post('/api/get_my_data', params)
.then((resp) => resp.data)
}
}
Desired test (as I understand it):
import React from 'react'
import { mount, configure } from 'enzyme'
import SliderComponent from '../../../app/components/slider'
test('SliderComponent changes active slide when setActiveSlide is
called', () => {
const wrapper = mount(
<SliderComponent />
);
wrapper.instance().setActiveSlide(1);
// some state checks here
});
I need mock ResourceService.getData call inside SliderComponent, and I really can't understand ho to do it...
You can import your ResourceService in your test and mock the method getData with jest.fn(() => ...). Here is an example:
import React from 'react'
import { mount, configure } from 'enzyme'
import ResourceService from '../../../modules/resource-service'
import SliderComponent from '../../../app/components/slider'
test('SliderComponent changes active slide when setActiveSlide is
called', () => {
// you can set up the return value, you can also resolve/reject the promise
// to test different scnarios
ResourceService.getData = jest.fn(() => (
new Promise((resolve, reject) => { resolve({ data: "testData" }); }));
const wrapper = mount(<SliderComponent />);
wrapper.instance().setActiveSlide(1);
// you can for example check if you service has been called
expect(ResourceService.getData).toHaveBeenCalled();
// some state checks here
});
try using axios-mock-adapter to mock the postreq in your test.
It should look something like this (may need a few more tweaks):
import React from 'react'
import { mount, configure } from 'enzyme'
import SliderComponent from '../../../app/components/slider'
import axios from'axios';
import MockAdapter = from'axios-mock-adapter';
test('SliderComponent changes active slide when setActiveSlide is
called', () => {
let mock = new MockAdapter(axios)
//you can define the response you like
//but your params need to be accordingly to when the post req gets called
mock.onPost('/api/get_my_data', params).reply(200, response)
const wrapper = mount(
<SliderComponent />
);
wrapper.instance().setActiveSlide(1);
// some state checks here
});
make sure to check the docs of axios-mock-adapter

Resources