Home > Net >  How to better type a React Context to assume that the value will always be defined and not have to g
How to better type a React Context to assume that the value will always be defined and not have to g

Time:11-03

I have a AuthProvider at in my application that will automatically redirect to the login page if the user is not logged in

interface IAuthContext {
  token: string | undefined;
}

export const AuthContext = createContext<IAuthContext>({ token: undefined });

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [token, setToken] = useState<string | undefined>();
  const navigate = useNavigate();
  useEffect(() => {
    const authToken = localStorage.getItem('authToken');

    if (!authToken) {
      navigate('/login');
    }
    setToken(token);
  }, []);

  return <AuthContext.Provider value={{ token }}>{children}</AuthContext.Provider>;
};

MyComp is a child of AuthProvider. Therefore, token should always be defined, because if it doesn't exist, then AuthProvider will redirect to the login page and MyComp will never be rendered.

export const MyComp = () => {
  const { token } = useContext(AuthContext);

  if (!token) {
    throw new Error('missing token');
  }

  const data = useMemo(() => fetchData(token), token);

  return <div>{data}</div>;
};

It's annoying having assert that token is not undefined every time I need to use it. I have to type token as string | undefined, because I need to pass a default value to createContext

Is there a way to better type this so I don't need to assert that the token is defined and to not have to give a default value to createContext?

CodePudding user response:

interface IAuthContext {
  token: string;
}

export const AuthContext = createContext<IAuthContext>({ token: '' });

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [token, setToken] = useState('');
  const navigate = useNavigate();

  useEffect(() => {
    const authToken = localStorage.getItem('authToken');

    if (!authToken) {
      navigate('/login');
    } else {
      setToken(authToken);
    }
  }, [token]);
  
  if (!token) return null;

  return <AuthContext.Provider value={{ token }}>{children}</AuthContext.Provider>;
};

CodePudding user response:

Found a way using typescript function overloading and a custom react hook

interface IAuthContext {
  token: string;
}

const AuthContext = createContext<IAuthContext | undefined>(undefined);

export function useAuthContext(acceptUndefined: true): IAuthContext | undefined;
export function useAuthContext(): IAuthContext;
export function useAuthContext(acceptUndefined?: true) {
  const authContext = useContext(AuthContext);
  if (!authContext && !acceptUndefined) {
    throw 'The component is not a child of AuthProvider';
  }
  return authContext;
}

Then, I can use the useAuthContext hook to get a defined context.

const MyComp = () => {
  const { token } = useAuthContext();

  const data = useMemo(() => fetchData(token), token);

  return <div>{data}</div>;
};

I can also pass true to useAuthContext if I want to handle the default context value myself

const MyComp = () => {
  const authContext = useAuthContext(true);
  if (!authContext) {
    throw 'AuthContext undefined';
  }
  const { token } = authContext;
  // ... //
};

  • Related