I have a created useAxiosPrivate hook and I want to use it in a service function I have created using axios which I used to export diffrent methods. But since its not a functional or class component I get an error react hooks must be called in a react function component or a custom react hook function
useAxiosPrivate.tsx
import { axiosPrivate } from '../api/axios'
import { useEffect } from 'react'
import useRefreshToken from './useRefreshToken'
import useAuth from './useAuth'
const useAxiosPrivate = () => {
const refresh = useRefreshToken()
const { auth }: any = useAuth()
useEffect(() => {
const requestIntercept = axiosPrivate.interceptors.request.use(
(config) => {
config.headers = config.headers ?? {}
if (!config.headers['Authorization']) {
config.headers['Authorization'] = `Bearer ${auth?.accessToken}`
}
return config
},
(error) => Promise.reject(error),
)
const responseIntercept = axiosPrivate.interceptors.response.use(
(response) => response,
async (error) => {
const prevRequest = error?.config
if (
(error?.response?.status === 403 || error?.response?.status === 401) &&
!prevRequest?.sent
) {
prevRequest.sent = true
const newAccessToken = await refresh()
prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`
return axiosPrivate(prevRequest)
}
return Promise.reject(error)
},
)
return () => {
axiosPrivate.interceptors.request.eject(requestIntercept)
axiosPrivate.interceptors.response.eject(responseIntercept)
}
}, [auth, refresh])
return axiosPrivate
}
export default useAxiosPrivate
I want to use this in auth.service.tsx
import useAxiosPrivate from "../hooks/useAxiosPrivate"
const axiosPrivate = useAxiosPrivate(); <-- 'I want to use this in this'
export const SharedService {
UpdateProfile: async (firstName:string, lastName:string) => {
const response = await axiosPrivate.put('/user/me',{
firstName,
lastName,
})
}
I get error that hooks should be used at top level or inside functional component or class how do I fix it ?
Your service must be a hook as well so it can use other hooks
import useAxiosPrivate from "../hooks/useAxiosPrivate";
export const useSharedService = () => {
const axiosPrivate = useAxiosPrivate();
return {
UpdateProfile: async (firstName: string, lastName: string) => {
const response = await axiosPrivate.put("/user/me", {
firstName,
lastName,
});
},
};
};
Related
In a react app, when creating a component, I use useEffect to handle a HTTP request via a custom hook (which fetch via a useCallback). Then, to parse the parameters for the request, I have a layer for services which return the expected values.
As a result, this workflow keeps re-rendering in a loop and the apps gets stacked.
Component:
import React, { Fragment, useContext, useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { getProject } from '../../../services/Project.service';
import { AuthContext } from '../../../shared/context/auth.context';
import { NavOptions } from '../../../shared/constants/NavOptions';
import { useHttpClient } from '../../../shared/hooks/http.hook';
import SideNavigation from '../../../shared/components/Navigation/SideNavigation/SideNavigation';
import NavLinks from '../../../shared/components/Navigation/NavLinks/NavLinks';
import './Dashboard.css';
const Dashboard = (props) => {
console.log('Dashboard...');
const { isLoading, error, sendRequest, clearError } = useHttpClient();
const [project, setProject] = useState();
const auth = useContext(AuthContext);
const projectId = useParams().projectId;
const getProject = async () => {
console.log('getProject...');
console.log('auth', auth.token);
const response = await sendRequest(getProject(projectId, auth.token));
if (response.status === 201) {
const responseData = await response.json();
console.log('project:', responseData);
setProject(responseData);
} else {
console.log('getting buildings failed!');
const error = await response.json();
}
};
useEffect(() => {
projectId && getProject();
}, []);
const { t, i18n } = useTranslation();
let content = (
<div className="bim-y-dashboard">
.
.
.
</div>
);
return (
<Fragment>
<SideNavigation>
<NavLinks options={NavOptions.PROJECT} projectId />
</SideNavigation>
<MainContent>{content}</MainContent>
</Fragment>
);
};
export default Dashboard;
Custom hook:
export const useHttpClient = () => {
const auth = useContext(AuthContext);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const activeHttpRequests = useRef([]);
const sendRequest = useCallback(
async (url, method = 'GET', body = null, headers = {}) => {
console.log('sendRequest...');
console.log('url', url);
console.log('method', method);
console.log('body', body);
console.log('headers', headers);
},[]);
const clearError = () => {
setError(null);
};
useEffect(() => {
return () => {
activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort());
};
}, []);
return { isLoading, error, sendRequest, clearError };
};
Service:
export const getProject = (projectId, token) => {
console.log('getProject...');
return (`/projects/id/${projectId}`, 'GET', null, {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token,
});
}
What am I missing to avoid this constant re-rendering?
Thanks in advance.
I have the feeling your issue is in code you have not shared, as the above looks legit.
Things you can try:
Comment out const auth = useContext(AuthContext); in your custom hook to make sure the culprit is not in your context
Make sure const response = await sendRequest(..) returns what you are expecting
Add a new hook, something like const [data, setData] = useState(null), then after you get a response, set it in your hook setData(response)
Finally, in your useEffect, update your condition as projectId && !data && getProject();
That will ensure your re rendering issue is neither on your auth context nor in your fetch request.
Hope that helps; if not, please update the question with the full code of your component and I'll take a second look :)
RESOLVED:
Found this post: Link
It explains that because of sendRequest method, it keeps re-rendering.
In order to receive a value from the server and pass it to the subcomponent, useLayoutEffect is used. By the way, useLayoutEffect is executed after the screen is all drawn.
Passing values is difficult because of the order of execution.
Q1. I want to run useLayoutEffect before rendering, or know another way.
Is there any way you can recommend?
Q2. At the bottom of the ApiServiece.js code, there is json as the return value for the response. But when I output json, promise[{pending}] is output. What is wrong?
I'm studying on my own, so I'm desperate for help. Any comments are welcome, so please help.🥲
Code Structure
// AdminContainer.js
import React, { useLayoutEffect, useEffect, useState, useRef } from "react";
import { useResolvedPath } from "react-router";
import { getAllUserList } from "../service/ApiService";
import DisplayUser from "./DisplayUser";
const AdminContainer = () => {
const [allUsers, setUsers] = useState([]);
useLayoutEffect (() => {
setUsers(getAllUserList);
// console.log(getAllUserList()); // Promise {<pending>}
}, []);
return (
<div>
{/* {JSON.stringify(allUserList[0])} */}
{/* {allUserList.map((user, i) => {
return <DisplayUser inputUser={user} key={i} />
})} */}
<DisplayUser inputUser={allUsers} />
</div>
)
}
export default AdminContainer;
// ApiServiece.js
import { connect } from 'react-redux';
import { login } from '../modules/authentication';
import store from '../modules/store';
import { API_BASE_URL } from "./app-config";
import { resolvePath } from 'react-router';
import { Sync } from '#material-ui/icons';
const ACCESS_TOKEN = "ACCESS_TOKEN";
export function signin(userDTO) {
console.log(store.getState());
return call("/auth/signin", "POST", userDTO).then((response) => {
if (response.token) {
localStorage.setItem(ACCESS_TOKEN, response.token);
localStorage.setItem("SequenceEmail", response.email);
window.location.href = "/";
}
});
}
export function signup(userDTO) {
return call("/auth/signup", "POST", userDTO);
}
export function call(api, method, request) {
let headers = new Headers({
"Content-Type": "application/json",
});
const accessToken = localStorage.getItem("ACCESS_TOKEN");
if (accessToken && accessToken !== null) {
headers.append("Authorization", "Bearer " + accessToken);
}
let options = {
headers: headers,
url: API_BASE_URL + api,
method: method,
};
if (request) {
options.body = JSON.stringify(request);
}
return fetch(options.url, options)
.then((response) =>
response.json().then((json) => {
if (!response.ok) {
return Promise.reject(json);
}
return json;
})
)
.catch((error) => {
console.log(error.status);
if (error.status === 403) {
window.location.href = "/login";
}
return Promise.reject(error);
});
}
export function getAllUserList() {
let result = call("/auth/getAllUerList", "GET");
return result;
}
// DisplayUser.js
const DisplayUser = (inputUser) => {
const userRef = useRef(inputUser);
const [user, setUser] = useState();
useEffect(() => {
setUser(userRef.current.inputUser);
}, [])
console.log('setUser isArray:' + Array.isArray(setUser(userRef.current.inputUser)));
return (
// <div> {user.username} </div>
<div> test </div>
)
}
export default DisplayUser;
CodeSandbox order of execution Link
So first thing first you are assigning to the allUsers variable (which should be a list) a function, which doesn't make sense. You have to first CALL the function (and using the useLayoutEffect you WILL call it before rendering, but the request will resolve asyncronously, so and you will have to populate allUsers only when the request is resolved). One solution would be to show a spinner in the meanwhile for example:
// AdminContainer.js
import React, { useLayoutEffect, useEffect, useState, useRef } from "react";
import { useResolvedPath } from "react-router";
import { getAllUserList } from "../service/ApiService";
import DisplayUser from "./DisplayUser";
const AdminContainer = () => {
const [allUsers, setUsers] = useState([]);
const [showSpinner, setSpinnerIsShowing] = useState(false)
useLayoutEffect (() => {
setSpinnerIsShowing(true)
getAllUserList().
then(users => setUsers(users))
.catch(err => /*Handle error*/)
.finally(()=> setSpinnerIsShowing(false))
}, []);
return (
showSpinner ? <SomeSpinner /> : <div>
{/* {JSON.stringify(allUsers[0])} */}
{/* {allUsers.map((user, i) => {
return <DisplayUser inputUser={user} key={i} />
})} */}
<DisplayUser inputUser={allUsers} />
</div>
)
}
export default AdminContainer;
Then in ApiService.js I would use an async await syntax to make everything more readable.
The difference is that async functions will always return a promise, so you don't need to use the Promise class directly (throw will be equivalent to reject, while return will be the same as resolve).
export async function call(api, method, request) {
let headers = new Headers({
"Content-Type": "application/json",
});
const accessToken = localStorage.getItem("ACCESS_TOKEN");
if (accessToken && accessToken !== null) {
headers.append("Authorization", "Bearer " + accessToken);
}
let options = {
headers: headers,
url: API_BASE_URL + api,
method: method,
};
if (request) {
options.body = JSON.stringify(request);
}
const response = await fetch(options.url, options)
const json = await response.json()
if (!response.ok) {
const error = new CustomError("Request not OK :C");
error.customData = json;
throw error
}
return json;
//console.log(err.status);
//if (err.status === 403) {
// window.location.href = "/login"; <-- Nope: handle this when calling the function
//}
}
Note the Custom Error that you can create to define additional data extending the default javascript Error class:
class CustomError extends Error {
constructor(message, customData = {}){
super(message)
this.customData = customData
}
}
Also the getUserList function is an asyncronous one:
And you can create an async function in two ways:
with async syntax
export async function getAllUserList() {
return await call("/auth/getAllUserList", "GET");
}
with promise sintax:
export function getAllUserList() {
return new Promise((reject,resolve)=>{
call("/auth/getAllUserList", "GET")
.then(res => resolve(res))
.catch(err => reject(err))
})
}
And those two are equivalent.
Let me know in the comments if you need some clarifications :)
I'm learning React and am having trouble with a value defined in a custom context provider. I access the value in a component under the provider with a custom hook but it's reported as being undefined. I've gone through the questions on SO and have verified my syntax with the lesson in my book but can't find the problem.
This is my custom provider and custom hook:
import React, { createContext, useState, useEffect, useContext } from 'react';
const ApiContext = createContext();
export const useApi = () => useContext(ApiContext);
export const ApiProvider = ({ children }) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [baseImageUrl, setBaseImageUrl] = useState();
const apiKey = 'api_key=SECRET';
const baseUrl = 'https://api.themoviedb.org/3';
const objToParams = (obj) => {
let params = '';
if(obj) {
const keys = Object.keys(obj);
for(let key of keys) {
params += `&${key}=${encodeURIComponent(obj[key])}`;
}
}
return params;
}
const api = {
get: async (path, params) => {
const resp = await fetch(baseUrl + path + '?' + apiKey + objToParams(params));
return await resp.json();
}
}
useEffect( () => {
try {
setLoading(true);
const config = api.get('/configuration');
console.log(config);
config.images && setBaseImageUrl(config.images.secure_base_url);
}
catch (error) {
console.error(error);
setError(error);
}
finally {
setLoading(false);
}
}, []);
if( loading ) {
return <p>Loading...</p>;
}
if( error ) {
return <pre>{JSON.stringify(error, null, 2)}</pre>;
}
return (
<ApiContext.Provider value={{ api, baseImageUrl }}>
{ children }
</ApiContext.Provider>
);
}
and this is the component where I access the value through the custom hook:
import React, { useState } from 'react';
import { ApiProvider, useApi } from './components/context/ApiProvider';
import Header from './components/Header';
import Main from './components/Main';
import Footer from './components/Footer';
import './App.css';
const App = () => {
const [searching, setSearching] = useState(false);
const [searchResults, setSearchResults] = useState([])
const [searchError, setSearchError] = useState();
const {api} = useApi();
const onSearch = (query) => {
try {
setSearching(true);
setSearchResults(api.get('/search/multi', {query: encodeURIComponent(query)} ));
console.log(searchResults);
}
catch (error) {
console.error(error);
setSearchError(error);
}
finally {
setSearching(false);
}
}
return (
<ApiProvider>
<div className="main-layout">
<Header onSearch={ onSearch }/>
<Main
searching={ searching }
searchError={ searchError }
searchResults={ searchResults }
/>
<Footer />
</div>
</ApiProvider>
);
}
export default App;
You can't consume the context in the component where you apply it.
<ComponentA>
<Context.Provider value={"somethong"} >
<ComponentB/>
</Context.Provider>
</ComponentA>
In the above example, only ComponentB can consume the value. ComponentA can't.
If you wan't to consume the value in your App component, it has to be the child (or grandchild ...) of the ContextProvider.
<Context.Provider value={"somethong"} >
<App/>
</Context.Provider>
If I understand your code correctly than you are trying to consume the context in your App, while also returning the provider for the same context.
Well, I have a component that requires a token in order to fetch data from DB. The user usually gets the token after a successfull login:
const loginAuth = (loginData, history) => async () => {
const res = await api.post(`/auth/login`, loginData);
if (res.statusText === `OK`) {
if (res.data.data) {
history.push(`/auth/validatetwofactorauth/${res.data.data._id}`);
return res.data;
}
setAuthToken(res.data.token);
history.push('/posts');
} else {
console.log('error');
// setError(res.data.error);
}
};
As you guys can see I'm calling a method with the name of sethAuthToken, said function looks like this:
export const setAuthToken = (token) => {
if (token) {
if (typeof window !== 'undefined') {
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
api.defaults.headers['Authorization'] = `Bearer ${token}`;
localStorage.setItem('xAuthToken', token);
}
} else {
delete api.defaults.headers.common['Authorization'];
delete api.defaults.headers['Authorization'];
localStorage.removeItem('xAuthToken');
deleteCookie('xAuthToken', '/');
}
};
After the token is received, the function above passes it to the api.default.headers.common; this is my api:
import axios from 'axios';
import { API_URL } from '../config';
const api = axios.create({
baseURL: `${API_URL}/api/v1`,
headers: {
'Content-Type': `application/json`
}
});
console.log('API', api.defaults);
// So far everything works great since I can actually see the token being
// retrieved in the previous console log()
api.interceptors.response.use(
(res) => res,
(err) => {
return Promise.reject(err);
}
);
export default api;
So far, everything works great!.
Now, the problem comes on this following request and component. This component fetches data from a request that do requires the a Bearer token(which by now should be available)
import { useEffect, useState, useContext } from 'react';
import { withRouter } from 'next/router';
// ACTIONS
import { getTimelineFromServer } from '#/actions/post';
// HELPERS
import NothingFoundAlert from '#/layout/NothingFoundAlert';
import privateRoutes from '#/routing/privateRoutes';
import AuthContext from '#/routing/authContext';
// REACSTRAP
import CardColumns from 'react-bootstrap/CardColumns';
// NESTED COMPONENTS
import Single from './single';
export const getServerSideProps = async (context) => {
const params = `?page=${context.query.page}&limit=${context.query.limit}&sort=${context.query.sort}&status=published`;
const data = (await getTimelineFromServer(params)()) || []; // Get videos
if (!data) {
return { notFound: true };
}
return {
props: {
params: params,
serverPosts: data.data,
}
};
};
const Timeline = ({
params,
serverPosts,
router
}) => {
const [posts, setPosts] = useState([]);
useEffect(() => {
setPosts(serverPosts);
}, [params]);
const { auth } = useContext(AuthContext);
return posts?.length > 0 ? (
<CardColumns>
{posts.map((post, index) => (
<Single
key={post._id}
post={post}
objects={posts}
auth={auth}
/>
))}
</CardColumns>
) : (
<NothingFoundAlert />
)
};
export default withRouter(privateRoutes(Timeline));
The function that requires it, is the getTimelineFromserver(params)() || []; this is what it looks like:
export const getTimelineFromServer = (params) => async (dispatch) => {
console.log(api.defaults.headers.common);
try {
const res = await api.get(`/posts/timeline${params}`);
console.log('Get timeline', api.defaults.headers.common);
return res.data;
} catch (err) {
return { msg: err?.response?.statusText, status: err?.response?.status };
}
};
Now, I'm assuming the error is in the api.defaults.headers.common because when I console.log-it, it only returns { Accept: 'application/json, text/plain, */*' }.
In summary: the token gets deleted for some reason?
Thank you and I know this post is long but I would not be asking if I had not tried anything before; now, I'm out of ideas.
NOTE: I have a privateRoutes HOC that I use with the component above but I do not think that has anything to do with the fetching since the only thing it does is verify if X token is found, otherwise, redirect user to /aut/login
My goal is to get the autoPagination function to run when this.props.userSaves initially updates in state. In my program it starts out as an empty array, and on initialization 100 objects are stored in the array. The problem is that autoPagination is running before the objects get stored, and thus the while loop isn't running. I've fixed this using setTimeout but I don't really see that as a long-term solution. Any ideas?
The below code is nested in a class based component.
autoPagination = async token => {
while (this.props.userSaves.length > 0) {
const { userSaves } = this.props
const lastPage = userSaves[userSaves.length-1].data.name
const userSavesObject = await axios.get (`https://oauth.reddit.com/user/${this.props.username}/saved/.json?limit=100&after=${lastPage}`, {
headers: { 'Authorization': `bearer ${token}` }
})
const currentPageSaves = userSavesObject.data.data.children
this.props.storeUserHistory(currentPageSaves)
this.props.appendUserHistory(currentPageSaves)
}
}
Full component (since requested):
import axios from 'axios';
import React from 'react';
import { connect } from 'react-redux';
import { storeUserHistory, appendUserHistory, storeInitialData } from '../actions/index.js'
class ListSaved extends React.Component {
componentDidMount (props) {
const params = new URLSearchParams(this.props.location.hash);
const token = params.get('#access_token')
this.props.storeInitialData(token)
setTimeout(() => {
this.autoPagination(token);
}, 3000)
}
autoPagination = async token => {
while (this.props.userSaves.length > 0) {
const { userSaves } = this.props
const lastPage = userSaves[userSaves.length-1].data.name
const userSavesObject = await axios.get (`https://oauth.reddit.com/user/${this.props.username}/saved/.json?limit=100&after=${lastPage}`, {
headers: { 'Authorization': `bearer ${token}` }
})
const currentPageSaves = userSavesObject.data.data.children
this.props.storeUserHistory(currentPageSaves)
this.props.appendUserHistory(currentPageSaves)
}
}
renderPostTitles = () => {
return this.props.totalSaves.map((saved) => {
return (
<div key={saved.data.id}>
<div>{saved.data.title}</div>
</div>
)
})
}
render () {
return <div>{this.renderPostTitles()}</div>
}
}
const mapStateToProps = state => {
console.log(state)
return {
username: state.username,
userSaves: state.userHistory,
totalSaves: state.totalUserHistory
}
}
export default connect(mapStateToProps, { storeUserHistory, appendUserHistory, storeInitialData })(ListSaved);
Take a variable and set it true initially.. Run the function when you get data in your props and make the variable false so that it don't run again..
constructor (props)
{
super(props)
this.myvar = true
}
componentWillRecieveProps(nextProps)
{
if(this.myvar)
{
if(check if get your data)
{
// run your function
this.myvar= false
}
}
}
Corrected Component. Every-time the component updates the function is run. Component is updated a first time right after mounting
import axios from 'axios';
import React from 'react';
import { connect } from 'react-redux';
import { storeUserHistory, appendUserHistory, storeInitialData } from '../actions/index.js'
class ListSaved extends React.Component {
componentDidMount (props) {
const params = new URLSearchParams(this.props.location.hash);
const token = params.get('#access_token')
this.props.storeInitialData(token)
}
componentDidUpdate (props) {
this.autoPagination(token);
}
autoPagination = async token => {
while (this.props.userSaves.length > 0) {
const { userSaves } = this.props
const lastPage = userSaves[userSaves.length-1].data.name
const userSavesObject = await axios.get (`https://oauth.reddit.com/user/${this.props.username}/saved/.json?limit=100&after=${lastPage}`, {
headers: { 'Authorization': `bearer ${token}` }
})
const currentPageSaves = userSavesObject.data.data.children
this.props.storeUserHistory(currentPageSaves)
this.props.appendUserHistory(currentPageSaves)
}
}
renderPostTitles = () => {
return this.props.totalSaves.map((saved) => {
return (
<div key={saved.data.id}>
<div>{saved.data.title}</div>
</div>
)
})
}
render () {
return <div>{this.renderPostTitles()}</div>
}
}
const mapStateToProps = state => {
console.log(state)
return {
username: state.username,
userSaves: state.userHistory,
totalSaves: state.totalUserHistory
}
}
export default connect(mapStateToProps, { storeUserHistory, appendUserHistory, storeInitialData })(ListSaved);