Skip to content

How do I get a token outside of useAuth with @clerk/expo #1511

Closed

Description

@nvsd

I'm trying to add an auth header to my fetch function but I'm finding it difficult.

import {Clerk} from @clerk/expo is undefined
I could use useAuth but this is awkward I want to do something like

import {getToken} from '@clerk/expo'

fetch('whatever', {headers: {Authorization: await getToken()}})

Activity

  1. JonLoesch commented on Jul 28, 2023

    @JonLoesch

    I'm not a Clerk expert but I ran into a similar issue (with react not expo). My workaround was to manually instantiate a Clerk instance (had to import @clerk/clerk-js directly) and then have it exported from some util module, globally available. Then it was a simple matter of getting the token from the global object (see here). Judging from my brief look through the source, a singleton object is what the <ClerkProvider> does anyway -- but if you construct it yourself and pass it in via the Clerk prop, then you have direct access to the clerk object outside of the react tree.

    I'm really not sure if this is best practice. Their examples show that if you can do things using custom hooks you should do that -- that approach doesn't work in my case but maybe it does for you. Also, it appears that Clerk sets a global window.Clerk reference to the singleton object ... so maybe that's the recommended approach to getting global imperative access? idk, I just am more comfortable with controlling initialization myself so I don't have to worry about order of operations when it comes to initializing clerk and my fetch middleware.

    Not sure if any of this helps, but figured I'd ping since I ran into a very similar issue and found your post while looking for solutions.

  2. nvsd commented on Jul 30, 2023

    @nvsd
    Author

    @JonLoesch It helps. I figured out how to achieve the API I wanted using hooks. I asked on Discord but didn't get a straight answer.

    I also dug through the source code and found the same things. I was going to do something similar but went with the hooks route.

    It would seem like they expect you to repeat a bunch of logic on every API call or revert to some awkward API where you pass in the getToken function to another function.

    const Component = () => {
      const {getToken} = useAuth();
      
    useEffect(() => {
      authApi(getToken, 'users')
    }, [])
    
      return (
        <div />
      )
    }
    
    const authApi = async (getToken, url) => {
      const token = await getToken()
      return fetch(url, {headers: {Authorization: token}})
    }

    Doing the above was a poor DX to me, and since I'm using react query, it didn't solve the issue.
    I had to do a lot of work to make a semi-decent DX for the rest of the devs on my team but ultimately ended up with

    export const api = ky.create({
      prefixUrl: env.apiUrl,
      hooks: { beforeRequest: [printRequest], afterResponse: [printResponse] },
    });
    
    export type Api = typeof api;
    export type GetToken = () => Promise<string | null>;
    
    const createAuthApi = async (tokenGetter: GetToken) => {
      const token = await tokenGetter();
      if (!token) throw new Error('User is not signed in');
      return api.extend({ headers: { Authorization: `Bearer ${token}` } });
    };
    
    type AuthQueryFunction<
      T = unknown,
      TQueryKey extends QueryKey = QueryKey,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      TPageParam = any,
    > = (api: Api, context: QueryFunctionContext<TQueryKey, TPageParam>) => T | Promise<T>;
    
    export function useAuthQuery<
      TQueryFnData = unknown,
      TError = unknown,
      TData = TQueryFnData,
      TQueryKey extends QueryKey = QueryKey,
    >({
      authQueryFn,
      ...options
    }: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
      authQueryFn: AuthQueryFunction<TQueryFnData, TQueryKey, unknown>;
    }): UseQueryResult<TData, TError> {
      const { getToken } = useAuth();
      return useQuery({
        ...options,
        queryFn: async (args) => {
          const api = await createAuthApi(getToken);
          
          // I pass the created api with the auth header already setup to the caller
          return authQueryFn(api, args);
        },
      });
    }

    and using that looks like

    export const useUsers = () => {
      const { data, ...rest } = useAuthQuery({
        queryKey: ['users'],
        authQueryFn: (api) => api.get(`users`).json<User[]>()
      });
    
      return { users: data, ...rest };

    It's disappointing and frustrating that it seemed like there was no acknowledgment of a problem here when I asked on Discord. I'll give them the benefit of the doubt since it seemed like me and the clerk dev weren't communicating that well.

    The issue still remains, though: the fact that developers can't encapsulate await getToken inside the function you use to fetch makes API design so awkward.

  3. dimkl commented on Aug 4, 2023

    @dimkl
    Contributor

    hello @nvsd and thank you for opening this issue.
    The Clerk from import {Clerk} from @clerk/expo is being populated when ClerkProvider is being loaded.
    I think that using the above code inside a component (inside ClerkProvider), it will be populated.

    I think you could also try the following solutions:

    // 1. Using hook exported from the @clerk/expo
    import { useClerk } from '@clerk/expo';
    const clerk = useClerk();
    const token = await clerk.session?.getToken();
    
    fetch('whatever', {headers: {Authorization: token })
    
    // 2. initializing a new clerk client using @clerk/clerk-js
    import Clerk from '@clerk/clerk-js/headless';
    const clerk = new Clerk(publishableKey);
    const token = await Clerk.session?.getToken();
    
    fetch('whatever', {headers: {Authorization: token })
  4. JonLoesch commented on Aug 5, 2023

    @JonLoesch

    @nvsd Glad you got something working, and it sounds like you've already solved the problem, even if you're not 100% happy with the solution. I'm going to try to flesh out my idea a bit more because I think it may be what you're looking for? As I've understood it, it sounds like you want your DX to be something along the lines of a globally available api, rather than as a parameter in a hook callback. Assuming I have understood that correctly, then what you could have is something like (This is basically a re-working of existing code on this thread):

    import Clerk from '@clerk/clerk-js/headless';
    
    export const _api = ky.create({
      prefixUrl: env.apiUrl,
      hooks: { beforeRequest: [printRequest], afterResponse: [printResponse] },
    });
    
    export type Api = typeof _api;
    
    export const clerk = new Clerk(publishableKey);
    
    export const api = async () => {
      const token = await clerk.session?.getToken();
      if (!token) throw new Error('User is not signed in');
      return api.extend({ headers: { Authorization: `Bearer ${token}` } });
    }
    

    And then wherever you have your <ClerkProvider publishableKey={...}> near the root of your application you can replace it with this instead:

    <ClerkProvider Clerk={clerk}>
      ...
    </ClerkProvider>
    

    This way both requests going through your api logic and requests coming from Clerk built in components (i.e. going through the react context) will be using the same underlying Clerk object. I think you'll still need to call api.extend(...) on every api request, because the token does not necessarily stay constant throughout the lifetime of the application. But at least this way you don't have to go through hooks for every request.

  5. JonLoesch commented on Aug 5, 2023

    @JonLoesch

    P.S. This is not clerk related but maybe still worth mentioning?

    I said you needed to call api.extend(...) on every API request. I'm not 100% sure that's true because I don't know what API / HTTP tech you're using.

    For example, I'm using in a trpc application which has this for custom headers:

    export interface HTTPLinkOptions {
      ...
      headers?:
        | HTTPHeaders
        | ((...) => HTTPHeaders | Promise<HTTPHeaders>);
    }
    

    The fact that it offers () => Promise<HTTPHeaders> as an option means that for my use case, I actually was able to initialize the api once and then have it call an async callback to get the headers every time (which would then do getToken). I'm not sure what API / HTTP tech you're working with and if that's an option for you.

  6. phuhl commented on Aug 23, 2023

    @phuhl

    I have the same issue using @clerk/clerk-react. I'd love some kind of official way to retrieve a token outside of the react context with the react library

  7. locked and limited conversation to collaborators on Sep 20, 2023
  8. converted this issue into a discussion #1751 on Sep 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions