TanStack QueryでCRUD処理を作成

React

TanStack Queryは、Reactアプリケーションでデータを効率的に管理できる強力なライブラリです。本記事では、TanStack Queryを使用して簡単なCRUD(作成、読み取り、更新、削除)処理を実装する方法について説明します。

TanStack Queryをなぜ使うのか?

TanStack Queryは、サーバーからのデータ取得を簡素化するためのライブラリです。データの取得、キャッシュ、同期を効率的に管理でき、状態管理が容易になります。

状態管理の重要性

Reactでの状態管理は、アプリケーションの全体的な体験とパフォーマンスに直結します。状態管理には大きく分けて以下の2種類があります。

UIそのものの状態: モーダルの開閉、フォームの入力状態、タブの選択など、ユーザーインターフェースと直接関連する状態です。これらの状態はアプリケーションの見た目や操作感に大きな影響を及ぼします。

APIからのデータ: サーバーと通信して取得したデータや、その更新を管理する状態です。例えば、ユーザー情報や商品リストなどの動的なデータを管理する必要があります。

TanStack Queryの役割

TanStack Queryは主にAPIからのデータ管理に特化しており、サーバーから取得したデータをキャッシュして管理することで、データの一貫性やパフォーマンスを向上させます。これにより、APIリクエストを最小限に抑えつつ、高速なデータアクセスが可能になります。他の状態管理ライブラリ(例えば、Reduxなど)と組み合わせることで、UIの状態の管理も行うことができます。

実際にReactでアプリケーションを作成している際、初めのうちはReduxやContext APIなどの状態管理ライブラリを使用しがちですが、TanStack Queryを取り入れることで、特にデータフェッチに特化した管理が簡素化されます。これにより、API関連のビジネスロジックをシンプルに保ちながら、より効率的な開発が実現できます。

TanStack Queryは、サーバーとの通信を最適化し、データの取得・管理を簡素化する手段を提供します。Reactアプリケーションにおいて、データとUIの状態管理を効果的に分離することで、全体の開発体験が向上します。

saku
saku

データフェッチの際にuseEffectを使用していて、アプリの成長と共に複雑化、メンテナンスがしんどくなる、、、😭

TanStack Queryを使うと、このようなケースは発生しずらいのでオススメです

CRUD処理

TanStack Queryが関わる部分のコードのみ掲載しています。

Viewに関する箇所は省略しています。

1. プロジェクトのセットアップ

まずは、必要なライブラリをインストールします。

npm install @tanstack/react-query

QueryClientProviderの設定を行います。

import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App'; // メインコンポーネント(アプリケーションのルート)

// QueryClientを作成
const queryClient = new QueryClient();

ReactDOM.render(
    <React.StrictMode>
        {/* QueryClientProviderでラップして全コンポーネントにクライアントを提供 */}
        <QueryClientProvider client={queryClient}>
            <App />
        </QueryClientProvider>
    </React.StrictMode>,
    document.getElementById('root')
);

QueryClientProviderで、アプリケーションのルートをラップしてください。

これで、TanStack Queryを利用することができます。

2. データ取得(Read)

データを取得するためのReactクエリを作成します。ここではFetch APIを使用します。

const fetchTodos = async () => {
    const response = await fetch('http://localhost:3000/api/todos');
    if (!response.ok) {
        throw new Error('Network response was not ok');
    }
    return response.json() as unknown as Todo[];
}

const TodoList = () => {
    const { data, isPending, isError } = useQuery({
        queryKey: ['customers'],
        queryFn: fetchTodos,
    });

    if (isPending) {
        return <LoadingComponent />
    }

    if (isError) {
        return <ErrorComponent />
    }

    if (!data) {
        return "No Todo found";
    }

    return (
        <TodoTable todos={data} />
    );
};

isPending、isErrorを使って状態を判定します。

  • isPending: クエリにはまだデータがありません
  • isError: クエリでエラーが発生しました

ほとんどのクエリでは、 isPending状態をチェックし、次にisError状態をチェックし、最後にデータが利用可能であると想定して成功状態をレンダリングするだけで十分です。

https://tanstack.com/query/latest/docs/framework/react/guides/queries

TanStack QueryにはisPending、isError以外にも取得可能な状態があります。

しかし、公式ではisPending、isErrorでほぼ事足りるよ、ということです。

これに従うのが良いでしょう。

3. データ作成(Create)

useMutationを使用し、データの作成/更新/削除を行います。

const createPost = async (newPost: TodoFormData) => {
    const response = await fetch('http://localhost:3000/api/todos', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(newPost),
    });
    if (!response.ok) {
        throw new Error('Network response was not ok');
    }
    return response.json();
};

const Component = () => {
    const { mutate, isPending, isError, isSuccess, error } = useMutation({
        mutationFn: createPost,
    });
    const handleFormSubmit = (newPost: TodoFormData) => {
        mutate(newPost);
    }
    return (
        <div className="container mx-auto p-4">
            {isPending ? <div>Adding todo...</div> : null}
            {isError ? (
                <div>An error occurred: {error.message}</div>
            ) : null}
            {isSuccess ? <div>Todo added!</div> : null}
            <CreateTodoForm onSubmit={handleFormSubmit} />
        </div>
    )
}

useMutationは、ミューテーションの状態をトラッキングするために、いくつかのプロパティを提供します。これにより、UIで適切なフィードバックを表示できます。

  • isPending: リクエストが送信されている間、trueになります。この状態のときに「Adding todo…」のメッセージを表示し、ユーザーに処理中であることを知らせます。
  • isError: リクエストが失敗した場合にtrueとなり、エラーメッセージを表示します。これにより、ユーザーは何が問題であったかを知識できます。
  • isSuccess: リクエストが成功した場合にtrueになり、成功メッセージを表示します。これにより、ユーザーにはTodoが正常に追加されたことが通知されます
saku
saku

作成処理も宣言的に書けるので、エラーハンドリングやローディングが入ってもコードが見やすいですね!

4. データ更新(Update)

Todoのステータスを完了にする実装です。

一覧画面上でチェックボックスを押して完了にしています。

更新後に自動で再取得するよう、invalidateQueriesを使用しています。


const updatePost = async ({ id, title, completed }: Todo) => {
    const response = await fetch(`http://localhost:3000/api/todos/${id}`, {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            title,
            completed,
        }),
    });
    if (!response.ok) {
        throw new Error('Network response was not ok');
    }
    return response.json();
};

const TodoTable = ({ todos }: Props) => {
    const queryClient = useQueryClient();
    const mutation = useMutation({
        mutationFn: updatePost,
        onSuccess: () => {
            queryClient.invalidateQueries({
                queryKey: ['todos'],
            });
        },
    });
    const handleCheckboxChange = (todo: Todo) => {
        mutation.mutate({
            ...todo,
            completed: !todo.completed,
        });
    }
    return (
        <table className="min-w-full bg-white border border-gray-300">
            <thead>
                <tr>
                    <th className="px-4 py-2 border-b">ID</th>
                    <th className="px-4 py-2 border-b">Title</th>
                    <th className="px-4 py-2 border-b">Completed</th>
                    <th className="px-4 py-2 border-b">Created At</th>
                    <th className="px-4 py-2 border-b">Updated At</th>
                </tr>
            </thead>
            <tbody>
                {todos.map((todo) => (
                    <tr key={todo.id} className="text-center">
                        <td className="px-4 py-2 border-b">{todo.id}</td>
                        <td className="px-4 py-2 border-b">{todo.title}</td>
                        <td className="px-4 py-2 border-b">
                            <input onChange={() => handleCheckboxChange(todo)} type="checkbox" checked={todo.completed} />
                        </td>
                        <td className="px-4 py-2 border-b">{todo.createdAt}</td>
                        <td className="px-4 py-2 border-b">{todo.updatedAt}</td>
                    </tr>
                ))}
            </tbody>
        </table>
    );
};

export default TodoTable;

5. データ削除(Delete)

データを削除するためのミューテーションを作成します。

const deletePost = async ({ id }: Pick<Todo, "id">) => {
    const response = await fetch(`http://localhost:3000/api/todos/${id}`, {
        method: 'DELETE',
    });
    if (!response.ok) {
        throw new Error('Network response was not ok');
    }
    return response.json();
};

const mutationDelete = useMutation({
    mutationFn: deletePost,
    onSuccess: () => {
        queryClient.invalidateQueries({
            queryKey: ['todos'],
        });
    },
});
const handleDelete = (id: string) => {
    mutationDelete.mutate({id});
}

...

<button onClick={() => handleDelete(todo.id)}>
    Delete
</button>

詳細設定

useQueryのキーの設定

useQueryでは、データを一意に識別するためにクエリキーを使用します。このキーは、基本的には文字列または配列からなる値であり、データのキャッシュを管理するために重要です。

const { data, isLoading, error } = useQuery(['posts'], fetchPosts);

この例では、['posts']という配列がクエリキーです。このキーを使用することで、キャッシュされたデータを特定のクエリに関連付けて管理します。

キーの利用方法

一意性: 同じリソースに対するクエリは同じキーを持つべきです。異なるリソースの場合は異なるキーを使用します。

変数の使用: 動的な値を持っている場合、配列形式のキーを使用できます。例えば、特定のポストIDに基づいたデータを取得する場合は、次のように設定できます。

const { data } = useQuery(['post', postId], () => fetchPostById(postId));

この場合、['post', postId]がクエリキーになります。このキーを使うことで、特定のIDに対するポストデータをキャッシュし、効率的なリクエストが可能になります。

 invalidateQueriesの使用

invalidateQueriesメソッドは、指定したクエリキーに関連するキャッシュデータを無効化します。これにより、次回そのキーを使ってデータを取得する際に、再度サーバーにリクエストが行われ、新しいデータがキャッシュされます。

ここで、useMutationでデータを作成した後にinvalidateQueriesを使って、データを刷新する例を見てみましょう。

const mutation = useMutation({
    mutationFn: updatePost,
    onSuccess: () => {
        queryClient.invalidateQueries({
            queryKey: ['todos'],
        });
    },
});

invalidateQueriesを使わない場合

手動でリロードするまで更新が反映されません。(良くないUX)

invalidateQueriesを使った場合

チェックを入れた瞬時に再取得が走り、自動で更新されます。(Good)

invalidateQueriesのポイント

  • いつ使うの?: 一覧画面内でデータ更新したい場合。作成ページ=>一覧ページ、のように遷移を挟む場合には不要。
  • 引数としてキーを渡すinvalidateQueriesは、キャッシュを無効化したいクエリのキーを引数として受け取ります。このキーを指定することで、どのデータを更新するかを明示的に指定します。
  • 自動再フェッチ: キャッシュを無効化することで、次回そのキーに関連付けられたデータを取得する際、TanStack Queryは自動的にサーバーから新しいデータを取得します。この動作により、データの整合性が保たれます。

まとめ

TanStack Queryを使用することで、CRUD処理を簡単に実装し、データの取得や操作を管理することができます。Fetchを用いることでシンプルなHTTPリクエストが実現でき、開発の生産性を向上させる要素を持っています。

これで、TanStack Queryを使ったCRUD処理の実装方法について理解が深まったことと思います。データ管理を簡素化し、開発の生産性を向上させるために、ぜひTanStack Queryを活用してみてください。