僕がWebアプリを開発するときはもっぱらRemixフレームワークを利用する。
理由は単純で、loader関数とaction関数の考え方が大好きだからだ。
React Nativeのアプリの開発をするときも、Remixのloader関数とaction関数を使いたい。 しかし、そんな機構はもちろんReact Nativeには存在しない。
そこで、action関数もどきをReact Nativeで再現するためにuseActionDataもどきを作ってみる。
useLoaderは別の機会に作ることにする。
引数
引数としてaction関数を受け取れるようにする。
T型は、formで送信された内容を表す。
R型は、いわゆるaction関数の返り値のActionData型を表す。
createMockActionRequest関数は、前に書いた記事で作った関数とほぼ同じ。
作成されるrequestオブジェクトではformData関数を持ってて、Remixで扱うのと同じようにformDataの内容を引き出すことができる。
function useActionData< T extends { [key: string]: string }, R extends { [key: string]: any } >( action: ({ request, }: { request: ReturnType<typeof createMockActionRequest>; }) => Promise<R> )
ロジック
useActionDataもどきの内部では、React QueryのuseMutationを使うことにする。
useMutationの返り値のdataをそのまま返すのではなくて、本家のuseActionDataをエミュレートするためにデータの取得がない場合はundefinedを返したい。 なので、actionDataステートを用意して、action関数の成功時に取得した値をセットするようにする。
mutateをそのまま返すのでは意図が伝わりづらいので、submitという名前に変えてクライアントに返す。
const { data, isSuccess, mutate } = useMutation((props: T) => action({ request: createMockActionRequest<T>(props) }) ); // 内部でキャッシュしておくaction関数の結果 const [actionData, setActionData] = useState<typeof data | undefined>( undefined ); useEffect(() => { // データの取得に成功した場合はキャッシュする if (isSuccess && data) { setActionData(data); } }, [data, isSuccess]); const submit = useCallback(mutate, []);
完成
テストも含めて書き終えるとこんな感じになった
// src/hooks/useActionData.test.ts import { afterAll, test, vitest, expect } from "vitest"; import { renderHook, act } from "@testing-library/react-hooks"; import { QueryClient, useMutation, QueryClientProvider } from "react-query"; import { createMockActionRequest } from "../createMockActionRequest"; import React, { useCallback, useEffect, useState } from "react"; afterAll(() => { vitest.clearAllMocks(); }); function useActionData< T extends { [key: string]: string }, R extends { [key: string]: any } >( action: ({ request, }: { request: ReturnType<typeof createMockActionRequest>; }) => Promise<R> ) { const { data, isSuccess, mutate } = useMutation((props: T) => action({ request: createMockActionRequest<T>(props) }) ); // 内部でキャッシュしておくaction関数の結果 const [actionData, setActionData] = useState<typeof data | undefined>( undefined ); useEffect(() => { // データの取得に成功した場合はキャッシュする if (isSuccess && data) { setActionData(data); } }, [data, isSuccess]); const submit = useCallback(mutate, []); return { actionData, submit, }; } test("smoke test", async () => { // action関数を準備する const action = async ({ request, }: { request: ReturnType<typeof createMockActionRequest>; }) => { return { fields: { name: request.formData().get("name"), email: request.formData().get("email"), }, }; }; // wrapperを準備する const queryClient = new QueryClient() const wrapper = ({ children }: { children: React.ReactNode }) => { return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ) }; const { result } = renderHook(() => useActionData(action), { wrapper }); // action関数を呼び出す await act(async () => { result.current.submit({ name: "田中太郎", email: "example@example.com", }) }); // action関数の結果が期待値であることを検証する expect(result.current.actionData).toEqual({ fields: { name: "田中太郎", email: "example@example.com", } }); });
実際に使ってみて、使いづらかったら中身変えるかもしれない。ご愛嬌。