Closed11

React19を使ってTodoアプリを開発する(バックエンドはGoのAPI)

kakkkykakkky

この開発について

以前開発したGoのREST APIをReactで叩く感じで、Todo アプリを完成させてみる。
https://github.com/kakkky/todo-api

使用技術を決める

  • React19
  • Vite(環境構築)
  • node.js (jsランタイム)
  • Typescript
  • TailwindCSS
  • shadcn/ui (コンポーネント)
  • TanStack Query(データフェッチ)
  • Tanstack Router (ルーティング)
  • Zod (バリデーション)
  • Biome (Lint+Format)
  • Vitest (テスト)

新めの、使ってみたい技術ベースで決めた。

kakkkykakkky

環境構築

プロジェクト作成

以下を参考に、

Biome

https://biomejs.dev/ja/

Tailwind

viteの設定があった。
https://tailwindcss.com/docs/installation/using-vite

shadcn/ui

tailwind4で動くカナリーverのものをインストールしてみた。

パスエイリアスの再設定(要求される)等々が必要だった。
https://ui.shadcn.com/docs/tailwind-v4

kakkkykakkky

プロジェクト構成について

以下の二つを軸に。

  • The Feature Based Pattern
  • Presentational/Container Pattern

とりあえず以下の感じでやってみる。
Tanstack routerを使うので、File Basedルーティングになる。(/routes配下?)

src
├── assets
├── features
│   ├── auth
│   ├── task
│   └── user
│       ├── shared
│       │   ├── components
│       │   ├── containers
│       │   └── hooks
│       ├── user-detail
│       └── user-list
├── lib
├── routes
├── shared
│   ├── components
│   └── hooks
└── styles

参考
https://github.com/alan2207/bulletproof-react/tree/master
https://itnext.io/top-6-best-folder-structures-for-react-ultimate-comparison-effc29ae5045
https://dev.to/profydev/screaming-architecture-evolution-of-a-react-folder-structure-4g25
https://zenn.dev/porokyu32/articles/2e6511fa8b606d
https://medium.com/@megh16/setting-up-a-react-project-folder-structure-in-2024-best-practices-93c27a49bbfe
https://qiita.com/uminoooon18/items/0ba109feddedf6a3539b
https://zenn.dev/t_keshi/articles/bulletproof-react-2022#routingについて

kakkkykakkky

Reactのデザインパターン

プロジェクト構成を考えるに際して、Reactデザインパターンが気になる。

https://zenn.dev/cybozu_frontend/articles/design-patterns-in-react
https://zenn.dev/morinokami/books/learning-patterns-1

フックパターン

React Hooks

クラスコンポーネントを使わずにステートやライフサイクルメソッドを利用できる。
- ステートフック
関数コンポーネント内のステートを管理する。
- 副作用フック
コンポーネントのライフサイクルに接続する(hook into)ことができる

カスタムHook

実態としては、関数コンポーネントから呼び出し可能な関数
ルールとして、

  • useXxxxであること
  • 関数内では他のフックを利用していること
  • コンポーネントで再利用したい値/コードを返り値として返すこと

フックを使用することで、コンポーネントのロジックをいくつかの小さなまとまりへ分割できる
(クラスコンポーネントと比べるとわかる)

HOC(高階コンポーネント)パターン

  • 高階関数 :「関数を引数にとる」「関数を返す」のいずれかまたはどちらも満たすような関数のこと

高階コンポーネントパターンは、高階関数を利用して、アプリケーション全体で再利用可能なロジックをコンポーネントに渡す
つまり、高階コンポーネント(HOCs)はコンポーネントを引数にとり、さらに手の加えられた新たなコンポーネントを返す

  • HOCs はアプリケーションを通して横断的な関心ごとにまとめて対応するとき特に有効

    • コンポーネント共通のホバー機能をつける
    • などなど
  • 同じロジックを複数のコンポーネントで使う

    • 認可
    • スタイル
    • グローバルステート

同じスタイルを当てられるHOC。

function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />
  }
}

const Button = () = <button>Click me!</button>
const Text = () => <p>Hello World!</p>

const StyledButton = withStyles(Button)
const StyedText = withStyles(Text)
  • 複数のHOCを合成(compose)することも可能。

フックで置き換えることも可能

フックによって、コンポーネントツリーの深さを減らすことができる。

  • Pros
    再利用したいロジックが一箇所にまとまる
    関心の分離
    DRY

  • Cons
    HOCに渡すpropsの名前が、衝突する可能性がある。


  function withStyles(Component) {
  return props => {
    const style = {
      padding: '0.2rem',
      margin: '1rem',
      ...props.style
    }

    return <Component style={style} {...props} />
  }
}

const Button = () = <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)

Presentational/Containerパターン

コンポーネントの役割を、PresentationとContainerで分割

Container Component
データを管理する。ラップしているPresentational Componentにデータを渡す。

Presentational Component
Container Componentから受け取ったデータを期待通りに表示する。データを変更しない。

  • Reactで関心の分離を図る
    • ビューをロジックから分離する

Compound Component パターン

1つのタスクを実行するために連携する複数のコンポーネントを作成するのに有用。

  • useContextを使って、配下のコンポーネントでステートを共有できるようにする。

複合コンポーネントのプロパティとして設定する。
▶︎ FlyOut コンポーネントのみのインポートですむ
※子要素としてレンダリングするでも可能

const FlyOutContext = createContext();

function FlyOut(props) {
  const [open, toggle] = useState(false);

  return (
    <FlyOutContext.Provider value={{ open, toggle }}>
      {props.children}
    </FlyOutContext.Provider>
  );
}

function Toggle() {
  const { open, toggle } = useContext(FlyOutContext);

  return (
    <div onClick={() => toggle(!open)}>
      <Icon />
    </div>
  );
}

function List({ children }) {
  const { open } = useContext(FlyOutContext);
  return open && <ul>{children}</ul>;
}

function Item({ children }) {
  return <li>{children}</li>;
}

FlyOut.Toggle = Toggle;
FlyOut.List = List;
FlyOut.Item = Item;
import React from "react";
import { FlyOut } from "./FlyOut";

export default function FlyoutMenu() {
  return (
    <FlyOut>
      <FlyOut.Toggle />
      <FlyOut.List>
        <FlyOut.Item>Edit</FlyOut.Item>
        <FlyOut.Item>Delete</FlyOut.Item>
      </FlyOut.List>
    </FlyOut>
  );
}

Render Propsパターン

レンダープロップス=JSX(TSX)を返却する関数を値とするprops

▶︎ props を用いて何をレンダリングするかを外側から決定できるような構造に

  • コンポーネント自身は、レンダープロップ以外のものをレンダリングしない

Inputコンポーネントはレンダープロップを受け取る。

function Input(props) {
  const [value, setValue] = useState("");

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="Temp in °C"
      />
      {props.render(value)}
    </>
  );
}

export default function App() {
  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input
        render={value => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      />
    </div>
  );
}

これらも、レンダリングしたいものを注入する形になるので、ロジックとUIを引き剥がせる。
関心の分離も可能。

propとしてではなく、childrenとして渡すのもある。

export default function App() {
  return (
    <div className="App">
      <h1>☃️ Temperature Converter 🌞</h1>
      <Input>
        {value => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      </Input>
    </div>
  );
}
function Input(props) {
  const [value, setValue] = useState("");

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="Temp in °C"
      />
      {props.children(value)}
    </>
  );
}
  • 多くの場合、フックで置き換えられる。
  • render propにはライフサイクルメソッドを追加できないので、受け取ったデータを変更する必要のないコンポーネントに対してのみ使用可能
kakkkykakkky

以下の記事によれば、
https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce

HOCよりもレンダープロップの方が良さげ。

  • 孫のコンポーネントに、PropsやStateがどのコンポーネントから渡されたのかがわかりやすい
  • 名前の衝突が起こりづらい
  • 型定義が簡単
  • HOCはJSXの中で動的に使えず、コンポーネントの外で結合させなければならない
kakkkykakkky

Tanstack Routerを理解したい。

https://zenn.dev/aishift/articles/ad1744836509dd

https://tanstack.com/router/v1/docs/framework/react/guide/file-based-routing

https://qiita.com/suiwave/items/041b98f230bdfa52fa39

ファイル名

1. __root.tsx
全てのルートに適用される。つまり、このコンポーネントは常にレンダリングされる。

2. $token(ex:tasks/$id)
$をprefixに使用すると、URL Pathnameとなる。
マッチしたpathnameはloader関数やコンポーネントの中で参照できる。(loaderはまた調べる。)

3. _prefix
子要素のレイアウトルートとなる。が、いまいちよく分からん。

4. route.tsx
ディレクトリパス、ディレクトリ配下のパスでレンダリングされる。
src/routes/users/route.tsx内でOutletを載せておけば、src/routes/users/index.tsxにも適用される。
特定のディレクトリにおけるレイアウトとして扱えそう。
「static route」としてのroute.tsxは、pathの先頭がマッチしたら適用される。
(/aだけでなく/a/bもマッチ)

5. index.tsx
pathが正確に一致しないと適用されない。
つまり、レイアウトとして振る舞えない。

ページコンポーネントとして扱えると思う。

RouteOptions

// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  component, // skip
  loader,
  errorComponent,
  pendingComponent,
  validateSearch,
})

1. loader
ルートが呼ばれるタイミングで発火する。失敗時にはエラーをthrow。

type loader = ({/** 省略 */}) => Promise<TLoaderData> | TLoaderData | void

loaderがPromiseを返している間はpending状態。
rejectされるとエラー状態に。

2. errorComponent``pendingComponent
各ルートごとに適用できる。loaderでかえすPromiseの状態によって描画するコンポーネントを指定可能。

3. validateSerch
serch Paramsを検証可能に。
zodのようなライブラリと組み合わせれる。

Code Splitting(Lazy Loading)

目的は、

  • 初回ページ読み込み時に必要なコード量を減らす
  • 対象コードが必要になったときに読み込む
  • チャンク分割することで細かい単位でキャッシュ可能に

方法は、
.lazy.tsxsuffixをつけて、createLazyFileRouteを利用する

Critical Route(初回にロードされるコード)

初回ロードされるモノ

Path Parsing/Serialization
Search Param Validation
Loaders, Before Load
Route Context
Static Data
Links
Scripts
Styles
All other route configuration not listed below

Lazy Route (必要になったときに遅延してロードされるコード)

各ルートに対応するページのコンポーネントは、(index.tsx等々の)Lazyらしい。

遅延ロードされるモノ

Route Component
Error Component
Pending Component
Not-found Component

Search Params

  • Tanstack RouterのSearch Paramsは、型安全・バリデーション・JSON構造の扱いが可能
  • tanstack routerだと、Search Params状態管理のように扱える。
?pageIndex=3&includeCategories=["electronics","gifts"]&sortBy=price&desc=true
const link = (
  <Link
    to="/shop"
    search={{
      pageIndex: 3, 
      includeCategories: ["electronics", "gifts"], 
      sortBy: "price", 
      desc: true,
    }}
  />
);
  • 型として扱うため、バリデーション(validateSearch)可能
type ProductSearchSortOptions = "newest" | "oldest" | "price";

type ProductSearch = {
  page: number;
  filter: string;
  sort: ProductSearchSortOptions;
};

export const Route = createFileRoute("/shop/products")({
  validateSearch: (search: Record<string, unknown>): ProductSearch => {
    return {
      page: Number(search?.page ?? 1), // `page` は `number`
      filter: (search.filter as string) || "", // `filter` は `string`
      sort: (search.sort as ProductSearchSortOptions) || "newest", // `sort` は `ProductSearchSortOptions`
    };
  },
});

キャッシュを備えたData Loading

  • データをpreload氏うてキャッシュしたデータを表示したり、以前取得したデータをキャッシュ&再利用が可能

Dependency-based Stale-While-Revalidate Caching

キャッシュはルートのdependencies&ルートのパス名によって制御。
RunRouteOptionsの一つである、

loaderDeps: ({ search: { index, size } }) => ({ index, size })
  • loader 内では search params (?key=value の部分) を直接参照できない(仕様上)
    Tanstack では、「URL の pathname(パス)」+「loaderDeps(依存関係)」 をキャッシュのキーとしている。
    もし loader 内で search params を直接使ってしまうと、TanStack Router は 「/users/user」と「/users/user?userId=1」を同じものとしてキャッシュしてしまう 可能性がある。

  • loaderDepsを使い、search Paramsを明示的にloaderに渡すと正しくキャッシュ管理ができる

export const Route = createFileRoute('/users/user')({
  // `validateSearch` で search params を型安全に扱う
  validateSearch: (search) =>
    search as {
      userId: string
    },

  // `loaderDeps` を使って search params を明示的に渡す
  loaderDeps: ({ search: { userId } }) => ({
    userId,
  }),

  // `deps` を通して search params を loader に渡す
  loader: async ({ deps: { userId } }) => getUser(userId),
});

Tanstack QueryのSuspense(番外)

Suspenseに対応したQueryと、File-Basedなアプローチととても相性がいい。

RouterとQueryを組み合わせる
import { useSuspenseQuery } from '@tanstack/react-query';
import { fetchPosts } from '../api/fetchPosts';

export function Posts() {
  const { data: posts } = useSuspenseQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });

  return (
    <div>
      <h2 className="text-xl font-bold">Posts</h2>
      <ul>
        {posts.map((post) => (
          <li key={post.id} className="border-b py-2">
            <h3 className="font-semibold">{post.title}</h3>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}
import { createFileRoute } from '@tanstack/react-router';
import { Posts } from '../components/Posts';

export const Route = createFileRoute('/posts')({
  component: Posts,
  errorComponent: () => <div className="text-red-500">Something went wrong!</div>,
  pendingComponent: () => <div className="text-gray-500">Loading posts...</div>,
});

※ Queryだけの場合

function Todos() {
  const { data } = useSuspenseQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  return (
    <div>
      {data.map((todo) => (
        <div key={todo.id}>{todo.title}</div>
      ))}
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <ErrorBoundary fallback={<div>Oh no!</div>}>
        <Todos />
      </ErrorBoundary>
    </Suspense>
  );
}

RouterのloaderとuseSuspenseQueryでキャッシュデータの状態を管理

loaderでデータを事前に用意(preload)して、Tanstack Queryのキャッシュに保存する。
▶︎ useSuspenseQueryが呼ばれるタイミングでデータが確保された状態に。

// src/routes/posts.tsx

const postsQueryOptions = queryOptions({
  queryKey: 'posts',
  queryFn: () => fetchPosts,
})

export const Route = createFileRoute('/posts')({
  // Use the `loader` option to ensure that the data is loaded
  loader: () => queryClient.ensureQueryData(postsQueryOptions),
  component: () => {
    // Read the data from the cache and subscribe to updates
    const posts = useSuspenseQuery(postsQueryOptions)

    return (
      <div>
        {posts.map((post) => (
          <Post key={post.id} post={post} />
        ))}
      </div>
    )
  },
})

また、発生したerrorはルートのerrorComponentが、pending状態はルートのpendingComponentが処理する。

kakkkykakkky

ちょっと適当にfetchする関数を定義して、Suspenseも確かめてみる。
後からTanstack Queryに転換したいけど、一旦適当に書いてみる。

コード
fetchする
import type { HealthCheck } from "../types/type";

export async function fetchHealthCheck(): Promise<HealthCheck> {
	await new Promise((resolve) => {
		setTimeout(resolve, 3000);
	});
	const response = await fetch("http://localhost:8080/health");
	if (!response.ok) {
		throw new Error("Error");
	}
	const result: HealthCheck = await response.json();
	return result;
}
コンテナー
import { use } from "react";

import { fetchHealthCheck } from "../api/fetch-api";
import HealthCheckMessage from "../components/HealthCheck";
import type { HealthCheck } from "../types/type";

// useはレンダリングフェーズにおいて呼ばれるPromiseには対応していないので
// レンダリング前にPromiseを用意している
const healthCheckPromise = fetchHealthCheck();
function HealhCheckContainer() {
	const result = use<HealthCheck>(healthCheckPromise);
	const data = result.data;
	const message = data.health_check;
	return (
		<>
			<HealthCheckMessage message={message} />
		</>
	);
}

export default HealhCheckContainer;
import HealhCheckContainer from "@/features/health/containers/HealhCheckContainer";
import { createFileRoute } from "@tanstack/react-router";
import { Suspense } from "react";

export const Route = createFileRoute("/health/")({
	component: RouteComponent,
});

function RouteComponent() {
	return (
		<>
			<Suspense fallback={<p>loading...</p>}>
				<HealhCheckContainer />
			</Suspense>
		</>
	);
}

React19により導入されたuseを使ってみた。
useは、Promiseから中身を取り出すのが役目であり、未解決の場合はサスペンドさせる。
▶︎こいつのおかげで、Suspenseは使いやすくなった模様。

ただ、クライアントコンポーネント内では、async/awaitがサポートされていない。
▶︎ レンダリングフェーズで非同期関数を呼び出せない

  • 意図せず何度も非同期関数が呼ばれる可能性

https://qiita.com/Sicut_study/items/92b0874e627b322b97ba

プロミスはキャッシュされるべき....じゃないとレンダリングのたびに非同期処理が走る
しかし、useを使うのにその機能は用意されていない。
よって、サスペンス対応の外部ライブラリを使用するべき。
また、データ取得する例はドキュメントにもないそう。
https://zenn.dev/aishift/articles/051d5d3fd1f8a6#まとめ

kakkkykakkky

Tanstack Router & Queryを理解したい...

  • データフェッチではなく、効率的な非同期状態の管理が目的。

useQuery(データ取得)

queryKeyとqueryFnオプションにキャッシュ識別のためのkeyとデータフェッチのための関数を渡すだけ

const { data, isPending } = useQuery({
  queryKey: ["issues"],
  queryFn: () => axios.get("/issues").then(res => res.data)
})

オプション

queryKey

  • 必須のオプション
  • キャッシュを識別する

queryFn

  • Promiseを返す関数
queryFn: (context: QueryFunctionContext) => Promise<TData>
  • QueryFunctionContext型のcontextを参照できる
    • queryKey
      • queryKeyに渡した値がcontextを通じて参照かのう
    • signal
      • クエリのキャンセルが可能
    • meta

enabled

クエリ同士に依存関係を持たせることができる。
enabledオプションがfalseの場合はクエリが実行されない

以下の場合だと、ユーザー取得リクエストが完了するまで、プロジェクト取得のリクエストが走らない。

// ユーザ情報を取得
const { data: user } = useQuery({
  queryKey: ['user', email],
  queryFn: getUserByEmail,
})

const userId = user?.id

const {
  status,
  fetchStatus,
  data: projects,
} = useQuery({
  queryKey: ['projects', userId],
  queryFn: getProjectsByUser,
  // ユーザIDの取得が完了されるまでクエリは発火されない
  enabled: !!userId,
})

ただ、waterfallという問題には気をつける。

staleTime

キャッシュをstaleにするまでの時間。

  • stale状態:キャッシュされたデータが「古い」と見なされる状態

デフォルトは0。
Infinityにすると、自動的にstaleにはならず、常にfleshとして扱われる。

staleTime: number | Infinity

gcTime

使われなくなったキャッシュのデータをGC(ガベージコレクション)するまでの時間

select

queryFnで取得したデータを加工する。
最終的には、selectで変換された値が返る

const transformTodoNames = (data: Todos) =>
  data.map((todo) => todo.name.toUpperCase())

export const useTodosQuery = () =>
  useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: transformTodoNames,
  })

返り値

const {
  data, // データ
  error,
  status,  // pending | success | error
  fetchStatus,
  isPending,
  isSuccess,
  isFetching,
  isPaused,
  isError,
  isLoading
} = useQuery()
  • dataとstatus(データに関する状態)
    • isPending,isSuccess,isErrorはstatusの状態を確かめるもの
function Todos() {
  const { status, data, error } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodoList,
  })

  // if (isPending) {
  if (status === 'pending') {
    return <span>Loading...</span>
  }

  // if (isError) {
  if (status === 'error') {
    return <span>Error: {error.message}</span>
  }

  // also status === 'success', but "else" logic works, too
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

useSuspenseQuery

https://tanstack.com/router/latest/docs/framework/react/examples/basic-react-query-file-based

https://zenn.dev/aishift/articles/ad1744836509dd#tanstack-queryのsuspense

useMutation(データ更新)

Delete/Post/Patch処理を担当。
useQueryが宣言的なのに

kakkkykakkky

認証

バックエンドAPI

http://localhost:8080/loginすると、JWTが返ってくる。

{
  "data": {
    "jwt_token": "string"
  },
  "status": 0
}

フロント(React)でどう認証状態を扱うか

  • ログインながれ
  1. ログインフォームを送信する
  2. バックエンドからレスポンスが返る
  3. cookieにトークンを保存する
  • ログアウト流れ
  1. ログアウトボタンを押す(API叩く)
  2. cookieからも削除

コンテキストで管理?

何を「認証状態(ログイン状態)」とするか。

  • cookieにjwtがある&&users/meでカレントユーザーが取得できる

  • AuthProviderコンポーネント

  • cookieからJWTを取り出し、期限を確認する

    • そしてそもそもcookieになかったらだめ
  • トークンを元にユーザ情報を取得するフックを呼び出し、コンテキストに注入する

    • ユーザー情報はキャッシュしておく

まとめると、「cookieに値がある」&「users/meで200(status)が返ってくる」と認証状態とみなす。
トークンを解析して、有効期限を見る、とかでもいいけど、

  • APIはログイン状態を管理していて、ログアウト(Redisにあるorない)している場合を知るためには、APIに聞くしかない。

CORSではまった!

Authorizationヘッダーをつけたらプリフライトリクエストが発生する。
ここら辺は記事のネタとしてまた調べ直す。

条件 プリフライトリクエスト発生?
Authorization ヘッダーあり ✅ 発生
Content-Type: application/json ✅ 発生
credentials: "include" あり ⚠️ CORS設定次第
GET / POST / HEAD かつ Content-Type: text/plain など ❌ 発生しない

キャッシュされてるけどリロードしたらまたフェッチするなぁ

AuthProvider内で、クエリ叩いてる。けど、リロードすると再フェッチ。
これは当たり前???
そこら辺をまた調べる。

function AppProvider({ children }: AppProviderProps) {
	return (
		<>
			<QueryClientProvider client={queryClient}>
				<ErrorBoundary fallback={<p>error</p>}>
					<Suspense fallback={<LoadingSpinner />}>
						<AuthProvider>
							<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
								{children}
							</ThemeProvider>
						</AuthProvider>
					</Suspense>
				</ErrorBoundary>
				<ReactQueryDevtools initialIsOpen={false} />
			</QueryClientProvider>
		</>
	);
}
  • どうやら、キャッシュされる場所はメモリであり、(考えてみればそりゃそう?)ページがリフレッシュされると当たり前に再フェッチを行う。

以下が使える?

https://tanstack.com/query/latest/docs/framework/react/plugins/createSyncStoragePersister

一部のクエリにだけ、これを適用させるには....。が課題。

kakkkykakkky

persisterを使ってローカルストレージにキャッシュを永続化

これで、ローカルストレージやセッションストア等にデータをキャッシュできる。

https://tanstack.com/query/v5/docs/framework/react/plugins/persistQueryClient#persistqueryclientprovider

export const queryClient = new QueryClient({
	defaultOptions: {
		queries: {
			gcTime: 1000 * 60 * 60 * 24, // 24 hours
		},
	},
});

persistQueryClient({
	queryClient,
	persister: createSyncStoragePersister({ storage: window.localStorage }),
	dehydrateOptions: {
		shouldDehydrateQuery: (query) => {
			const keys = query.queryKey.map((key) => {
				return String(key);
			});
			const allowKeys = userKeys.current();
			return (
				keys.length === allowKeys.length &&
				keys.every((key) => allowKeys.includes(key))
			);
		},
	},
});

function AppProvider({ children }: AppProviderProps) {
	return (
		<>
			<QueryClientProvider client={queryClient}>
                ....
kakkkykakkky

なんとなくライブラリ等の作り方がわかった時点で、一旦モチベが落ち着いた。

また気が向いたらやる。

このスクラップは2025/03/04にクローズされました