ウェブサイト検索

javascript - カスタムReactフックに渡された引数としての関数が問題を引き起こしています


必要性:

私はフルスタックアプリを作るためにRedwoodJSを使用しています。RedwoodJSはフックuseAuth()を提供しており、これによりユーザーがログインしているかどうかを示す状態isAuthenticatedが得られます。

ユーザーがログインしたときだけウェブ側でいくつかクエリを実行したいです。useAuth() の isAuthenticated が変更されたときにではなく。例えば、ページが読み込まれたとき、isAuthenticated は undefined から false に設定されますが、それはユーザーがログアウトしたことを意味するわけではありません。特定の関数をログアウト時にだけ実行したい場合はどうすればいいでしょうか?

試した解決策:

私はこのカスタムフックを書きました:

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
}

そして私はこのフックを次のように使用しています:

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}
    </>
  )
}

問題:

上記の使用例では、ご覧の通り、useAuthenti カスタムフックに onLogin 関数のプロップを渡しています。これは、その関数をログイン時に実行し、その中で API コールを行いたいためです。

しかし、そのコードは毎回期待通りに動作しているわけではありません。時には動作しますが、他の時には動作しません。(また奇妙です…レースコンディションのように見えますか?)

動作していないとき、コンソールのログはまったく表示されません(これは onLogin が呼び出されていないことを示唆しています)が、ネットワークタブでは createHttpLink.js:100 による呼び出しが行われているのが確認できます。これも失敗しています。この場合(期待通りに動作しないとき)、ユーザーのログイン呼び出しは成功しているのに、redwood js の getCurrentUser 呼び出しは行われません。(redwood js に詳しくない場合は無視して構いません。フックの部分にだけ注目してください)onLogin 内にある API は、周囲にコンソールログがないのに、どのようにして動作しているのでしょうか?

ネットワークタブのデバッグ

赤いボックスは1回目のログイン試行(うまくいかない)を示しています。緑のボックスは2回目のログイン試行(うまくいく)を示しています。

追加の注意:

どうやら、関数を引数として渡すだけで、それをフック内で実行しなくても実行されてしまうようです。なぜ、フックに引数として渡された関数が勝手に実行されるのでしょうか?

私がカスタムのReactフックを作っている方法は間違っていますか?もし誰か教えてくれるなら、ここで私が何を間違えているのか教えてください。

時々動作して、時々動作しないのは、レースコンディションがあるのでしょうか?

どのように進めればよいですか?

回答 #1

理由はわかりませんが、Promise を返す useLazyQuery を置き換えるカスタムフックを書くことで、すべての問題が解決しました。

このGithubのスレッドが役に立ちました: https://github.com/apollographql/react-apollo/issues/3499

特にこの回答: https://github.com/apollographql/react-apollo/issues/3499#issuecomment-539346982

それに対して、Typescript と loading、error、data のようなステートをサポートするように拡張しました。(このバージョンは useLazyQuery で、拡張したフックは 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 };

もし誰かが、なぜ useLazyQuery が Promise を返さなかったのか、または返さなかったときでも時々コードが動作したのかを説明できるなら、私はこのもやもやから解放されるでしょう。