Search code examples
react-nativejwtreact-hooksfetchrefresh-token

react native fetch hook and refresh jwt token


i have made a custom hook to fetch data from api, but the token only valid for 5 second. so i made this hook the problem is when i call the hooks from my page it called many time and the refresh token already expired

when i access the api i will check the response first if the token invalid i tried to refresh my token using handleRefreshToken

nb : im using useContext for my state management

import React, {useEffect, useState, useContext} from 'react';
import {View, StyleSheet} from 'react-native';
import {AuthContext} from '../Auth/Context';

import AsyncStorage from '@react-native-community/async-storage';
import {urlLogin, URLREFRESHTOKEN} from '../Configs/GlobaUrl';

const FetchData = () => {
  const {loginState, authContext} = useContext(AuthContext);
  const [data, setData] = useState([]);
  const [message, setMessage] = useState('');
  const [loading, setIsLoading] = useState(false);
  const {dispatchRefreshToken} = authContext;

  const handleRefreshToken = async (callbackUrl, callbackBody) => {
    const refBody = {
      client_id: loginState.ipAddress,
      ipAddress: loginState.ipAddress,
      employee_id: loginState.userData.Pegawai_Id,
      jwttoken: loginState.userToken,
      refresh_tokenn: loginState.refreshToken,
    };
    console.log('======refreshtokencalled==========');
    console.log(refBody.refresh_tokenn, '<=refresh token');
    console.log(refBody.jwttoken, '<=jwt token');
    let response = await fetch(URLREFRESHTOKEN, {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(refBody),
      redirect: 'follow',
    });
    let result = await response.json();
    console.log(result, ' ini result');
    if (
      result.item3 !== 'refresh token gagal' &&
      result.item3 !== 'refresh token sudah tidak berlaku'
    ) {
      let refresh = result.item2;
      let token = result.item1;
      // the backend doesnt send any succes / error code only item1 for token,       //item2 refresh token and item3 for error
      dispatchRefreshToken(token, refresh);
      await AsyncStorage.setItem('refreshToken', refresh);
      await AsyncStorage.setItem('token', token);
      return getData(callbackUrl, callbackBody);
    } else {
      return null;
    }
  };

  const getData = async (url, body) => {
    setIsLoading(true);
    let result;
    try {
      let response = await fetch(url, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          Authorization: `Bearer ${loginState.userToken}`,
        },
        body: JSON.stringify(body),
        redirect: 'follow',
      });
      if (response.status == '401') {
        let refreshResult = await handleRefreshToken(url, body);
        console.log(refreshResult);
      } else {
        result = await response.json();
        console.log(result);
        console.log(loginState.refreshToken);
        if (result.code == '1') {
          setData(result.data);
          setIsLoading(false);
        } else {
          throw result;
        }
      }
    } catch (err) {
      setData([]);
      console.log(err, 'masuk error usefetchbybutton');
      console.log(err.message, err.code);
      setIsLoading(false);
      setMessage(err);
    }
  };
  return {
    data: data,
    message: message,
    loading: loading,
    getData: getData,
  };
};

export default FetchData;

this is my dispatch refresh token

const authContext = useMemo(
    () => ({
      logIn: async (token, userData, refreshToken) => {
        console.log(token, '<>', refreshToken, 'ini memoisa');
        dispatch({
          type: 'LOGIN',
          token: token,
          userData: userData,
          refreshToken: refreshToken,
        });
      },
      logOut: () => {
        AsyncStorage.clear((error) => {
          console.log(error);
        });
        dispatch({type: 'LOGOUT'});
      },
      dispatchRefreshToken: (userToken, refreshToken) => {
        console.log(refreshToken, '=refresh dispatch=');
        console.log(userToken, '=userToken dispatch=');
        dispatch({
          type: 'REFRESHTOKEN',
          userToken: userToken,
          refreshToken: refreshToken,
        });
      },
    }),
    [],
  );

my reducer function

  const loginReducer = (prevState, action) => {
    switch (action.type) {
    some case ...
     case 'REFRESHTOKEN':
        return {
          ...prevState,
          userToken: action.userToken,
          refreshToken: action.refreshToken,
        };
          }
  };


Solution

  • Use recursion. The pseudo code is as follows

    const getData = async (args, times) => {
        // try to fetch data
        const data = await Api.fetch(args);
        // if token need to be refreshed.
        if (check401(data)) {
            // Use variable times to prevent stack overflow.
            if (times > 0) {
               // refresh the token
               await refreshToken()
               // try again
               return getData(args, times - 1);
            } else {
               throw new Error("The appropriate error message")
            }
        }
        return dealWith(data)
    }
    
    

    The logical above can be encapsulated to all your api. Like this

    const wrapApi = (api) => {
        const wrappedApi = async (args, times) => {
            const data = await api(args);
            // if token need to be refreshed.
            if (check401(data)) {
                // Use variable times to prevent stack overflow.
                if (times > 0) {
                   // refresh the token
                   await refreshToken()
                   // try again
                   return wrappedApi(args, times - 1);
                } else {
                   throw new Error("The appropriate error message")
                }
            }
            return dealWith(data)
        }
        return wrappedApi;
    }