1

I have created a hook useToken which tracks and updates the state of my access and refresh token.

When the app is started, the tokens are pulled from localStorage and are refreshed updating the values of the hook's state as well as the values in local storage.

The values within local storage update as expected but the values of the state do not reflect the change. I understand setState is asynchronous but even when awaiting or using .then() the state does not update.

This can be noticed further down where the token is used and the request fails because the access token does not match.

/hooks/useTokens.tsx:

import { useState } from "react";

export default function useTokens() {
  const [tokens, setTokensState] = useState({
    apiToken: localStorage.getItem("apiToken"),
    refreshToken: localStorage.getItem("refreshToken"),
  });

  const setTokens = (tokenParams: {
    apiToken: string | null;
    refreshToken: string | null;
  }) => {
    const newTokens = { ...tokens, ...tokenParams };
    setTokensState(newTokens);
    newTokens.apiToken
      ? localStorage.setItem("apiToken", newTokens.apiToken)
      : localStorage.removeItem("apiToken");
    newTokens.refreshToken
      ? localStorage.setItem("refreshToken", newTokens.refreshToken)
      : localStorage.removeItem("refreshToken");
  };

  return { tokens, setTokens };
};

home.tsx

const { tokens, setTokens } = useTokens();
...
      refreshToken(tokens.refreshToken!).then(
        ({ isSuccessful, data, status }) => {
          if (isSuccessful) {
            // data.access_token contains new token
            // tokens.xyz contains old access/refresh token even after setTokens
            setTokens({
              apiToken: data.access_token,
              refreshToken: data.refresh_token,
            });
            accountsAndAuthCheck();
          } else {
            console.log("refresh", data, status);
            setTokens({ apiToken: null, refreshToken: null });
            navigate("/link", { replace: true });
          }
        }
      );

home.tsx

...
const accountsAndAuthCheck = () => {

    // tokens.apiToken has not updated
    getAccounts(tokens.apiToken!).then(({ isSuccessful, data, status }) => {
      if (isSuccessful) {
        setIsAuthorised(1);
        const _accountId = data.accounts[0].id;
        setAccountId(_accountId);
      } else {
        if (status == 403) {
          setIsAuthorised(-1);
        }
      }
    });
  };
...
Joosh
  • 33
  • 5
  • 1
    your `refreshToken` executes the `setTokens` and at the same time `accountsAndAuthCheck`. The thing about that is that setState values will only be new AFTER the component re-renders. and that will happen only after the entire logic flow will finish. so even tho u set the state - the `accountsAndAuthCheck` is called with old values since the state is still old. only after `accountsAndAuthCheck` is done - state is updated. To fix it - either pass token to the `accountsAndAuthCheck` or call it after the state update – Lith Jul 29 '23 at 19:52

1 Answers1

1

If you want to fix this issue without using custom hook state, I would pass token as arguments to accountsAndAuthCheck function and set just local storage in hook

/hooks/useTokens.tsx:

export default function useTokens() {

  const setTokens = (tokenParams: {
    apiToken: string | null;
    refreshToken: string | null;
  }) => {
    const newTokens = {...tokenParams };
    newTokens.apiToken
      ? localStorage.setItem("apiToken", newTokens.apiToken)
      : localStorage.removeItem("apiToken");
    newTokens.refreshToken
      ? localStorage.setItem("refreshToken", newTokens.refreshToken)
      : localStorage.removeItem("refreshToken");
  };

  return { setTokens };
};



home.tsx

const { tokens, setTokens } = useTokens();

      refreshToken(tokens.refreshToken!).then(
        ({ isSuccessful, data, status }) => {
          if (isSuccessful) {
            setTokens({
              apiToken: data.access_token,
              refreshToken: data.refresh_token,
            });
            accountsAndAuthCheck(data.access_token, data.refresh_token);
          } else {
            console.log("refresh", data, status);
            setTokens({ apiToken: null, refreshToken: null });
            navigate("/link", { replace: true });
          }
        }
      );


const accountsAndAuthCheck = (apiToken, refreshToken) => {

    getAccounts(apiToken!).then(({ isSuccessful, data, status }) => {
      if (isSuccessful) {
        setIsAuthorised(1);
        const _accountId = data.accounts[0].id;
        setAccountId(_accountId);
      } else {
        if (status == 403) {
          setIsAuthorised(-1);
        }
      }
    });
  };

If you want to keep state in your custom hook then I would suggest you to try triggering accountsAndAuthCheck function with change of token state in useEffect

Evren
  • 4,147
  • 1
  • 9
  • 16
  • 1
    I read an article saying that hooks shouldn't be used to keep track of state between components so I have pivoted from that idea. I have it working now after substituting in useContext instead of a hook and then used the useEffect change that you mentioned to run the authCheck function only when tokens are updated. This way I can keep the token available between components as well as have one source of truth rather than prop drilling. – Joosh Jul 29 '23 at 20:10