Function passed as argument to custom React Hook is causing problems

135 Views Asked by At

The Need:

I am using RedwoodJS for making a Fullstack app. It provides us with hook useAuth() which gives a state isAuthenticated telling if user is logged in or not.

I want to make some Queries on Web side whenever user Logs in. Not whenever isAuthenticated from useAuth() is changed. (example: on page loads, isAuthenticated is set to false from undefined.. but this doesn't mean user logged out. What if I want to run certain function only on log out?

Tried Solution:

I wrote this custom hook:

export type CurrentUser = ReturnType<typeof useAuth>['currentUser']

interface HookProps {
  // similarly for onLogout
  onLogin?: (currentUser: CurrentUser) => void
}

export function useAuthenti(props: HookProps): CurrentUser | false {
  const { onLogin } = props
  const { isAuthenticated, currentUser } = useAuth()
  const wasAuthenticated = usePrevious(isAuthenticated);

  const [currentUserOrFalse, setCurrentUserOrFalse] = useState<CurrentUser | false>(false)

  useEffect(() => {

    console.log(`isAuthenticated CHANGED: ${wasAuthenticated} => ${isAuthenticated}`)

    if (isAuthenticated) {
      setCurrentUserOrFalse(currentUser)
      if (wasAuthenticated === undefined) {
        console.log(`false login 1`)
      } else if (wasAuthenticated === false) {
        console.log(`real login [MAKE API CALLS]`)
        if (onLogin) {
          console.log(`1. [inside] calling onlogin`)
          onLogin?.(currentUser)
          console.log(`4. [inside] called onLogin`)
        }
      } else if (wasAuthenticated === true) {
        console.log(`false login 2`)
      } else {
        console.log(`false login 3`)
      }
    } else {
      setCurrentUserOrFalse(false)
      if (wasAuthenticated === undefined) {
        console.log(`false logout 1`)
      } else if (wasAuthenticated === false) {
        console.log(`false logout 2`)
      } else if (wasAuthenticated === true) {
        console.log(`real logout [MAKE API CALLS]`)
      } else {
        console.log(`false logout 3`)
      }
    }

  }, [isAuthenticated])

  return currentUserOrFalse
}

and I am using this hook as follows:

export function Initialize({ children }: ComponentProps) {

  const [getMyData, { loading: loading_MyData, data: data_MyData }] = useLazyQuery(MY_DATA_QUERY)
  const [getAllPosts, { loading: loading_AllPosts, data: data_AllPosts }] = useLazyQuery(ALL_POSTS_QUERY)

  useAuthenti({
    onLogin: (currentUser: CurrentUser) => {
      console.log(`2. [outside] onLogin start`)
      getMyData()
      getAllPosts()
      console.log(`3. [outside] onLogin done`)
    },
  })

  useEffect(() => {
    if (data_MyData && data_AllPosts) {
      console.log(data_MyData)
      console.log(data_AllPosts)
    }
  }, [data_MyData, data_AllPosts])

  return (
    <>
      {children}
    </>
  )
}

The Problem:

In the above usage, as you can see i am providing onLogin function prop to the useAuthenti custom hook. Because i want that function to run ON LOGIN and make the api calls in it.

However, the code isn't working as expected every time. sometimes it is working, other times it's not. (also weird.. seems like a race condition?)

When it's not working, I don't see any console logs (hinting the onLogin was never called), BUT, in networks tab I DO see calls being made by createHttpLink.js:100. it's failing also. During this case (when it doesn't work as expected), i see user login call succeeding BUT redwood js' getCurrentUser call isn't made. (you can ignore the redwoodjs part if you're unfamiliar with redwood js, just focus on the hook part) How are the apis that are inside onLogin running without any console logs surrounding it?

Networks Tab Debug

The red box shows attempt one at login (doesn't work) The green box shows attempt two at login (works)

Screenshot-2023-02-18-at-4-15-37-PM.png

Additional Note:

Apparently, only passing the functions as arguments is causing them to run EVEN if I don't run them inside the hook. Why are the functions passed as arguments to hook running on their own?

Is the way I am making a custom React Hook wrong? What am I doing wrong here if anyone could please let me know.

Is there a race condition as sometimes it'w working and sometimes it's not?

How do I proceed?

1

There are 1 best solutions below

0
On

I don't understand why, but writing a custom hook to replace useLazyQuery that returns a Promise fixed all the problems.

This Github thread helped: https://github.com/apollographql/react-apollo/issues/3499

Particularly this answer: https://github.com/apollographql/react-apollo/issues/3499#issuecomment-539346982

To which I extended to support Typescript and States like loading error and data. (his version is useLazyQuery, extended hook is useLazyQueryModded)

useLazyQueryModded.tsx

import { useState } from 'react';
import { DocumentNode, ApolloError, useApolloClient, OperationVariables } from '@apollo/client';

type QueryVariables = Record<string, any>;

type UseLazyQueryResult<TData, TVariables extends QueryVariables> = [
  (variables?: TVariables) => void,
  { data: TData | undefined, error: ApolloError | undefined, loading: boolean }
];

function useLazyQuery<TData = any, TVariables = OperationVariables>(query: DocumentNode) {
  const client = useApolloClient()
  return React.useCallback(
    (variables: TVariables) =>
      client.query<TData, TVariables>({
        query: query,
        variables: variables,
        fetchPolicy: 'cache-first'
      }),
    [client]
  )
}

function useLazyQueryModded<TData = any, TVariables extends QueryVariables = QueryVariables>(
  query: DocumentNode
): UseLazyQueryResult<TData, TVariables> {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState<TData | undefined>(undefined);
  const [error, setError] = useState<ApolloError | undefined>(undefined);

  const runQuery = useLazyQuery<TData, TVariables>(query);

  const executeQuery = async (variables?: TVariables) => {
    setLoading(true);
    setData(undefined);
    setError(undefined);

    try {
      const result = await runQuery(variables);
      setData(result.data);
    } catch (error) {
      setError(error);
    } finally {
      setLoading(false);
    }
  };

  const result = { data, error, loading };

  return [executeQuery, result];
}


export { useLazyQueryModded };

If anyone can explain why useLazyQuery wasn't returning a Promise or why even when it didn't code worked sometime, I'd be relieved of this itch.