Juzzbott
Juzzbott

Reputation: 1769

Creating hook in Typescript with useContext and empty context object

I'm trying to create a useAuth custom hook within React 17 and Typescript. The solution that I have is kind of working, but it requires the following when accessing the hook methods:

    const auth = useAuth();
    // Do other stuff
    const result = await auth?.login(data);

The issue is that I need to use the ? operator when calling the login method, as the auth object can be null.

The reason for this I know is how I have implemented in the context, however I can't figure out how in TypeScript to create the context with a null context object by default (i.e. no currentUser).

This is how I've currently implemented the hook:

    interface AuthProvider {
        children: ReactNode;
    }
    
    interface ProvideAuthContext {
      currentUser: AppUser | null,
      login: (loginInfo: LoginInfo) => Promise<LoginResponse>,
      register: (registerInfo: RegisterInfo) => Promise<RegisterResponse>
    }
    
    const authContext = createContext<ProvideAuthContext | null>(null);
    
    export function ProvideAuth({ children }: AuthProvider) {
      const auth = useProvideAuth();
      return <authContext.Provider value={auth}>{children}</authContext.Provider>
    }
    
    // Hook for child components to get the auth object and re-render when it changes.
    export const useAuth = () => {
      return useContext(authContext);
    };
    
    // Provider hook that creates auth object and handles state
    function useProvideAuth() {
      // This is where I implement the ProvideAuthContext interface
    
      // Return the user object and auth methods
      return {
        currentUser,
        login,
        register
      };
    
    }

Any help with this would be amazing. I'm 99% sure it's got to do with this line: const authContext = createContext<ProvideAuthContext | null>(null);.

I can't do it this way: const authContext = createContext<ProvideAuthContext | null>({ currentUser: null }); as I get the following error:

Argument of type '{ currentUser: null; }' is not assignable to parameter of type 'ProvideAuthContext'. Type '{ currentUser: null; }' is missing the following properties from type 'ProvideAuthContext': login, register

I can't change the ProvideAuthContext login and register methods to be login?:... or register?:... as I can't then use them in components that use the useAuth hook.

Thanks, Justin

Upvotes: 1

Views: 1741

Answers (2)

asla03
asla03

Reputation: 103

I see that previous answers tell you to set an initial value other than null in your context, but I don't think it really makes sense to do that in your case. Initial values for context shouldn't be used unless you really need this context to have an initial value if used outside a provider, which doesn't fit your case because you're saying that your ProvideAuth should always be wrapped around your app. Setting initial values can actually make it harder to debug later on because there's no way for you to know if the values that you got are actually from context or from your provider. Instead, I'd advise you to introduce an if check in your hook to make sure that the context is not null, and in case it is, it throws an error:

const authContext = createContext<ProvideAuthContext | null>(null);

export const useAuth = () => {
  const auth = useContext(authContext);
  if (!auth) {
    throw new Error('useAuth can\'t be used outside a ProvideAuth.')
  } 
  return auth
};

This can make it easier to debug because if you use your useAuth hook but you forgot to wrap your app around provider you immediately get an error message telling you exactly what's wrong, with the benefit that the auth returned by this hook will never be null and typescript won't complain anymore. I learned this from Kent C. Dodds' blog.

Upvotes: 1

jantimon
jantimon

Reputation: 38140

You can make use of the never type which is inferred from throw new Error:


    interface ProvideAuthContext {
      currentUser: AppUser | null,
      login: (loginInfo: LoginInfo) => Promise<LoginResponse>,
      register: (registerInfo: RegisterInfo) => Promise<RegisterResponse>
    }
    
    const authContext = createContext<ProvideAuthContext>({
      currentUser: null,
      login: () => { throw new Error("context is missing") },
      register: () => { throw new Error("context is missing") }
    });

Upvotes: 1

Related Questions