Search code examples
reactjstypescriptreact-hooksreact-statereact-custom-hooks

Updating the state within a custom hook is not reflected in subscribing components


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);
        }
      }
    });
  };
...

Solution

  • 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