3

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
vazun
  • 194
  • 2
  • 15

1 Answers1

2

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

Melo
  • 31
  • 4
  • Thank you very much for your answer, but I dont really understand why they are not initalized yet. I know that useEffects run on mount, but this explicit action only runs when my error is true and its only set to true after the lastfunction and lastparams were set. – vazun Apr 07 '21 at 13:02
  • I tried this solution but it still doesnt work. In the authError the ref attributes are changed correctly, but as you mentioned the refs arent initialized, when the useEffect is called, but as mentioned I dont know why. In my opinion the isMounted flag only provides that the code in the useEffect isnt called on the initial render, but I dont really know how it should avoid the useEffect to use the values after the refs were initialized and set correctly. Probably I just dont understand it right and have a mistake in thinking but I would appreciate it if you would explain it to me more detailed – vazun Apr 07 '21 at 13:25
  • before you send you request, verify that your token is valid, if it'sexpired you can handle it by refreshing your token and send your request after that. you can do something similar to this: https://stackoverflow.com/questions/44982412/how-do-i-check-for-token-expiration-and-logout-user then you won't need to store your last request, handle the token before you send your request. – Melo Apr 07 '21 at 18:25