Reputation: 214
I have the following issue: Currently I am trying to write a customHook, with which I can make all my backend operatation. The intention of it is, to make my code look cleaner, easier understandable and to wirte less.
To understand the problem, I have to explain a few things first. First of all I am working with JWT Tokens and to execute a few action you need to authentificate with a token in the authentification header. And if I am getting an error when trying to post something, my function should refresh the JWT Token and should call the function again, if the auth error occurred the first time. Because I am working with functional Components and a customHook (I think) can only use hooks in the way functional components would do, I have to call the previous function again after I set the state to the current error value. Therefore I am saving my previous function and its parameters in useRefs, but when the useEffect is triggered the ref.current values dont have the values they should have.
In the following code only the operations that are needed for this issue are included:
useBackend:
import axios from "axios";
import { useEffect, useRef, useState } from "react";
const BASE_URL = 'https://localhost:44372/api/';
const R = 'Requests/'; const M = 'Movies/'; const U = 'Users/';
const defaultOperations = (controller) => {
return {
post: (object) => buildParams((`${controller}/Post${controller.substring(0, controller.length-1)}`), 'post', true, object),
put: (object, id) => buildParams((`${controller}/Put${controller.substring(0, controller.length-1)}/${id}`), 'put', true, object),
delete: (id) => buildParams((`${controller}/Delete${controller.substring(0, controller.length-1)}/${id}`), 'delete', true),
getAll: () => buildParams((`${controller}/Get${controller}`), 'get', false),
getByID: (id) => buildParams((`${controller}/Get${controller.substring(0, controller.length-1)}/${id}`), 'get', false),
}
}
const buildParams = (url, type, header, param) => {
return {url: url, type: type, header: header, param: param};
}
export const CONTROLLERS = {
REQUESTS: {
post: (object) => defaultOperations('Requests').post(object),
put: (object, id) => defaultOperations('Requests').put(object, id),
delete: (id) => defaultOperations('Requests').delete(id),
getAll: () => defaultOperations('Requests').getAll(),
getByID: (id) => defaultOperations('Requests').getByID(id),
getRequestsByUser: () => buildParams(`${R}GetRequestsByUser`, 'post', true, {accessToken: localStorage.accessToken}),
},
USERS: {
refreshToken: () => buildParams(`${U}RefreshToken`, 'post', false, {accessToken: localStorage.accessToken, refreshToken: localStorage.refreshToken}),
login: (mail, password) => buildParams(`${U}Login`, 'post', false, {mail: mail, password: password}),
register: (object) => buildParams(`${U}Register`, 'post', false, object),
getUserByAccessToken: () => buildParams(`${U}GetUserByAccessToken`, 'post', true, {accessToken: localStorage.accessToken}),
authentificate: () => buildParams('authentificate', 'special', true, undefined),
getAll: () => defaultOperations('Users').getAll(),
getByID: (id) => defaultOperations('Users').getByID(id),
post: (object) => defaultOperations('Users').post(object),
put: (object, id) => defaultOperations('Users').put(object, id),
delete: (id) => defaultOperations('Users').delete(id),
},
}
export const useBackend = (error, setError, initValue) => {
const [values, setValues] = useState([]);
const lastFunction = useRef();
const lastParameter = useRef();
function selectFunction(objc) {
switch(objc.type) {
case 'get': buildGetAndFetch(objc.url, objc.param, objc.header); break;
case 'post': buildPostAndFetch(objc.url, objc.param, objc.header);break;
case 'put': buildPutAndFetch(objc.url, objc.param, objc.header);break;
case 'delete': buildDeleteAndFetch(objc.url, objc.param, objc.header);break;
case 'special': authentificate();break;
default: console.log("Error in Switch");
}
}
if(initValue!==undefined) setValues(initValue);
function buildPostAndFetch(url, param, header) {
let _param = param;
if(param?.accessToken !== undefined) {
_param = {accessToken: localStorage.accessToken};
if(param?.refreshToken) {
_param = {...param, refreshToken: localStorage.refreshToken}
}
}'GetUserByAccessToken'));
const finalurl = `${BASE_URL}${url}`;
if(header) {
axios.post(finalurl, _param, {headers: {
'Authorization': `Bearer ${(localStorage.accessToken)}`
}})
.then(res => {
response(res);
})
.catch(err => {
authError(buildPostAndFetch, url, param, header);
})
}
else {
axios.post(finalurl, param)
.then(res => {
response(res);
})
.catch(err => {
setError(true);
})
}
}
useEffect(() => {
}, [values])
function buildDeleteAndFetch(url, param, header) {
const finalurl = `${BASE_URL}${url}`;
if(header) {
axios.delete(finalurl, param, {headers: {'Authorization': `Bearer ${(localStorage.accessToken)}`}})
.then(res => {
setValues(values.filter(item => item.requestID !== param));
setError(false);
})
.catch(err => {
authError(buildDeleteAndFetch, url, param, header);
})
}
else {
axios.delete(finalurl, param)
.then(res => {
setValues(values.filter(item => item.requestID !== param));
setError(false);
})
.catch(err => {
setError(true);
})
}
}
function response(res) {
setValues(res.data)
setError(false);
}
function refreshToken(err) {
const finalurl= `${BASE_URL}${U}RefreshToken`;
axios.post(finalurl, {accessToken: localStorage.accessToken, refreshToken: localStorage.refreshToken})
.then(res => {
localStorage.accessToken = res.data.accessToken;
setError(err);
})
.catch(err => {
localStorage.accessToken = undefined;
localStorage.refreshToken = undefined;
setError(err);
});
}
function authError(funktion223, url, param, header) {
if(error!==true) {
lastFunction.current = funktion223;
lastParameter.current = [url, param, header];
refreshToken(true);
}
}
useEffect(() => {
if(error===true) {
if(localStorage.accessToken !== undefined && localStorage.refreshToken !== undefined) {
const lastfunc = lastFunction.current;
const lastparams = lastParameter.current;
if(lastfunc !== undefined && lastparams.length > 0 ) {
console.log(lastfunc);
console.log(lastparams[0]);
lastfunc(lastparams[0], lastparams[1], lastparams[2]);
}
}
}
}, [error])
return [values,
(objc) => selectFunction(objc)];
}
Explanation to the components: I the userSettings component I am calling the hook when the componentsDidMount to check if the user is logged in, if not it wouldnt make any sense to try to fetch other data from the backend. If he is logged in or at least his token is expired it will be refreshed. In the UserRequests component the the requests of the user will be fetched, when no error is there.
(I dont know if you need this information, maybe if you think that the custom hook is correct and I only made mistakes in the components that are using them)
userSettings:
import React, { useEffect, useState } from 'react';
import {Row, Col} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import '../../App.css';
import ProfileSettings from './profileSettings';
import SettingsChooser from './settingsChooser';
// import SettingRoutings from '../settingRoutings';
import {BrowserRouter as Router, useLocation, useParams} from 'react-router-dom';
// import Routings from '../Routings.js';
import UserRequests from './userRequests';
import useAuth from '../../API/useAuthentification';
import { CONTROLLERS, useBackend } from '../../hooks/useBackend';
function UserSettings({user}) {
const {title: path} = useParams();
const [authError, setAuthError] = useState(false);
const [userValues, authentificate] = useBackend(authError, setAuthError, user);
let component;
useEffect(() => {
console.log('render');
authentificate(CONTROLLERS.USERS.getUserByAccessToken());
}, [])
useEffect(() => {
console.log(userValues);
}, [userValues])
if(path ==='logout') {
// localStorage.accessToken = undefined;
// localStorage.refreshToken = undefined
}
else if(path === 'deleteaccount') {
//TODO
//Implement
}
component = <UserRequests user={userValues} setAuthError={setAuthError} authError={authError}/>
return (
<div classname="">
<Row className="">
<Col className="formsettings2" md={ {span: 3, offset: 1}}>
<SettingsChooser/>
</Col>
<Col className="ml-5 formsettings2"md={ {span: 6}}>
{authError ? <p>No Access, please Login first</p> : component}
</Col>
</Row>
</div>
);
}
export default UserSettings;
Requests:
import React, { useEffect, useState } from 'react';
import {Table} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import createAPIEndpoint from '../../API/callBackendAPI';
import {toast } from 'react-toastify';
import Loading from '../Alerts/loading';
import notify from '../Alerts/toasts';
import { ToastProvider } from 'react-toast-notifications';
import useAuth from '../../API/useAuthentification';
import { useHistory } from 'react-router';
import { CONTROLLERS, useBackend } from '../../hooks/useBackend';
toast.configure({
});
function UserRequests({user, authError, setAuthError}) {
const headers = ['ID', 'Title', 'Release Date', 'Producer', 'Director', 'Status', 'UTC Time', '#', '#'];
const history = useHistory();
const [requests, requestActions] = useBackend(authError, setAuthError);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if(requests.length > 0) setLoaded(true);
}, [requests])
useEffect(() => {
if(authError === false) requestActions(CONTROLLERS.REQUESTS.getRequestsByUser());
}, []);
const handleDelete = (e) => {
requestActions(CONTROLLERS.REQUESTS.delete(e));
}
const handleUpdate = (e) => {
console.log(e);
sessionStorage.movie = JSON.stringify(e);
history.push('/updateRequest');
}
if(loaded===false) return <Loading/>
return(
<ToastProvider>
<Table bordered hover responsive="md">
<thead>
<tr>
{headers.map((item, index) => {
return( <th className="text-center" key={index}>{item}</th> );
})}
</tr>
</thead>
<tbody>
{requests.map((item, index) =>{
return(
<tr>
<td>{index + 1}</td>
<td>{item.movie.movieTitle}</td>
<td>{item.movie.movieReleaseDate}</td>
<td>{item.movie.movieProducer}</td>
<td>{item.movie.movieDirector}</td>
<td>{(item.requestStatus === 1 ? 'Success' : item.requestStatus ===2 ? 'Pending' : 'Denied')}</td>
<td className="col-md-3">{item.requestDate}</td>
<td><span onClick={() => handleDelete(item.requestID)}><i className="fas fa-times"></i></span></td>
<td><span onClick={() => handleUpdate(item.movie)}><i className="fa fa-pencil-alt"></i></span></td>
</tr>);
})}
</tbody>
</Table>
</ToastProvider>
);
}
export default UserRequests
Upvotes: 3
Views: 1115
Reputation: 31
useEffect
runs onmount
,
When you use customHook useBackend
with error === true
and you have tokens in localStorage
it will try to use lastFunction
and lastParameter
refs, but you haven't initiated them yet.
useEffect(() => {
if(error===true) { //could use if(error) if error is boolean
if(localStorage.accessToken !== undefined && localStorage.refreshToken !== undefined) {
const lastfunc = lastFunction.current;
const lastparams = lastParameter.current;
if(lastfunc !== undefined && lastparams.length > 0 ) {
console.log(lastfunc);
console.log(lastparams[0]);
lastfunc(lastparams[0], lastparams[1], lastparams[2]);
}
}
}
}, [error])
if you don't want useEffect
to run onmount
you can use isMounted
flag as answered here:
Stop useEffect from running on mount
Upvotes: 2