React Router アプリケーションを Remix に移行する
世界中で展開されている何百万もの React アプリケーションが React Router によって支えられています。おそらく、あなたもいくつかリリースしていることでしょう!Remix は React Router を基盤として構築されているため、大規模なリファクタリングを避けるために段階的に作業できる簡単な移行プロセスになるよう努めてきました。
まだ React Router を使用していない場合でも、再考する価値のあるいくつかの説得力のある理由があると考えています!履歴管理、動的なパスマッチング、ネストされたルーティングなど、多くの機能があります。React Router のドキュメント を見て、私たちが提供するすべての機能を確認してください。
React Router v6 の使用を保証する
古いバージョンの React Router を使用している場合、最初のステップは v6 にアップグレードすることです。アプリを迅速かつ段階的に v6 にアップグレードするには、v5 から v6 への移行ガイド と 後方互換性パッケージ を参照してください。
Remixのインストール
最初に、Remixで開発するためにいくつかのパッケージをインストールする必要があります。以下の手順に従って、プロジェクトのルートディレクトリからすべてのコマンドを実行してください。
npm install @remix-run/react @remix-run/node @remix-run/serve
npm install -D @remix-run/devサーバーとブラウザのエントリポイントの作成
ほとんどのReact Routerアプリは主にブラウザで動作します。サーバーの唯一の仕事は、単一の静的HTMLページを送信することであり、React Routerはクライアント側でルートベースのビューを管理します。これらのアプリは一般的に、次のようなルートindex.jsのようなブラウザのエントリポイントファイルを持っています。
import { render } from "react-dom";
import App from "./App";
render(<App />, document.getElementById("app"));サーバーレンダリングされたReactアプリは少し異なります。ブラウザスクリプトはアプリをレンダリングするのではなく、サーバーが提供したDOMを「ハイドレート」します。ハイドレーションとは、DOM内の要素を対応するReactコンポーネントにマッピングし、イベントリスナーを設定してアプリをインタラクティブにするプロセスです。
2つの新しいファイルを作成しましょう。
app/entry.server.tsx(またはentry.server.jsx)app/entry.client.tsx(またはentry.client.jsx)
appディレクトリに配置されます。既存のアプリが同じ名前のディレクトリを使用している場合は、Remixに移行する際に区別するために、srcやold-appのような名前に変更してください。
import { PassThrough } from "node:stream";
import type {
AppLoadContext,
EntryContext,
} from "@remix-run/node";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
const ABORT_DELAY = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext
) {
return isbot(request.headers.get("user-agent") || "")
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(
createReadableStreamFromReadable(body),
{
headers: responseHeaders,
status: responseStatusCode,
}
)
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
console.error(error);
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(
createReadableStreamFromReadable(body),
{
headers: responseHeaders,
status: responseStatusCode,
}
)
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}クライアントのエントリポイントは次のようになります。
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});root ルートの作成
RemixはReact Routerの上に構築されていることを述べました。あなたのアプリは、JSXのRouteコンポーネントで定義されたルートを持つBrowserRouterをレンダリングする可能性があります。Remixではそれを行う必要はありませんが、それについては後で詳しく説明します。今のところ、Remixアプリが動作するために必要な最低レベルのルートを提供する必要があります。
ルートルート(Wes Bos風に言えば「ルートルート」)は、アプリケーションの構造を提供する役割を担います。そのデフォルトエクスポートは、他のすべてのルートがロードして依存する完全なHTMLツリーをレンダリングするコンポーネントです。アプリの足場またはシェルと考えてください。
クライアント側でレンダリングされるアプリでは、ReactアプリをマウントするためのDOMノードを含むindex HTMLファイルがあります。ルートルートは、このファイルの構造を反映したマークアップをレンダリングします。
appディレクトリにroot.tsx(またはroot.jsx)という新しいファイルを作成します。そのファイルの内容は異なりますが、index.htmlが次のようになっていると仮定しましょう。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="My beautiful React app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>My React App</title>
</head>
<body>
<noscript
>You need to enable JavaScript to run this
app.</noscript
>
<div id="root"></div>
</body>
</html>root.tsxで、その構造を反映するコンポーネントをエクスポートします。
import { Outlet } from "@remix-run/react";
export default function Root() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="My beautiful React app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>My React App</title>
</head>
<body>
<div id="root">
<Outlet />
</div>
</body>
</html>
);
}いくつかの点に注意してください。
noscriptタグを削除しました。サーバーサイドレンダリングを行うため、JavaScriptを無効にしたユーザーでもアプリを表示できます(そして、時間とともに漸進的エンハンスメントを改善するためのいくつかの調整を行うと、アプリの大部分が機能するようになります)。- ルート要素内では、
@remix-run/reactからOutletコンポーネントをレンダリングします。これは、通常React Routerアプリで一致したルートをレンダリングするために使用するコンポーネントと同じです。ここでは同じ機能を果たしますが、Remixのルーターに合わせて調整されています。
publicディレクトリからindex.htmlを削除してください。ファイルを保持すると、/ルートにアクセスしたときに、RemixアプリではなくそのHTMLがサーバーから送信される可能性があります。
既存のアプリコードの適応
まず、既存のReactコードのルートをappディレクトリに移動します。プロジェクトルートのsrcディレクトリにルートアプリコードが存在する場合は、app/srcに移動する必要があります。
このディレクトリの名前を変更して、これが古いコードであることを明確にすることをお勧めします。これにより、最終的にすべてのコンテンツを移行した後、削除することができます。このアプローチの利点は、アプリが通常どおりに動作するために、一度にすべてを行う必要がないことです。デモプロジェクトでは、このディレクトリをold-appと名付けています。
最後に、ルートAppコンポーネント(root要素にマウントされていたコンポーネント)で、React Routerの<BrowserRouter>を削除します。Remixは、プロバイダーを直接レンダリングする必要なく、これを処理します。
インデックスルートとキャッチオールルートの作成
Remixはルートルート以外に<Outlet />で何をレンダリングするかを知るためのルートを必要とします。幸いなことに、既にアプリ内で<Route>コンポーネントをレンダリングしており、Remixはルーティング規則を使用するように移行する際にそれらを使用できます。
まず、app内にroutesという新しいディレクトリを作成します。そのディレクトリ内に_index.tsxと$.tsxという2つのファイルを作成します。$.tsxはキャッチオールルートまたは「スプラット」ルートと呼ばれ、まだroutesディレクトリに移動していないルートを古いアプリで処理するために役立ちます。
_index.tsxと$.tsxファイル内では、古いルートAppからコードをエクスポートするだけです。
export { default } from "~/old-app/app";export { default } from "~/old-app/app";バンドラーをRemixに置き換える
Remixは、アプリの開発とビルドのために独自のバンドラーとCLIツールを提供します。おそらく、あなたのアプリはCreate React Appのようなものを使ってブートストラップされたか、あるいはWebpackを使ったカスタムビルド設定を持っているでしょう。
package.jsonファイルで、現在のビルドと開発スクリプトの代わりにremixコマンドを使用するようにスクリプトを更新します。
{
"scripts": {
"build": "remix build",
"dev": "remix dev",
"start": "remix-serve build/index.js",
"typecheck": "tsc"
}
}そして、あっという間に!あなたのアプリはサーバーサイドレンダリングされ、ビルド時間は90秒から0.5秒に短縮されました⚡
ルートの作成
時間が経つにつれて、React Routerの<Route>コンポーネントによってレンダリングされるルートを、独自のルートファイルに移行したいと思うでしょう。ルーティング規則で概説されているファイル名とディレクトリ構造がこの移行をガイドします。
ルートファイルのデフォルトエクスポートは、<Outlet />でレンダリングされるコンポーネントです。そのため、Appに次のようなルートがある場合:
function About() {
return (
<main>
<h1>About us</h1>
<PageContent />
</main>
);
}
function App() {
return (
<Routes>
<Route path="/about" element={<About />} />
</Routes>
);
}ルートファイルは次のようになります。
export default function About() {
return (
<main>
<h1>About us</h1>
<PageContent />
</main>
);
}このファイルを作成したら、Appから<Route>コンポーネントを削除できます。すべてのルートの移行が完了したら、<Routes>、そして最終的にold-app内のすべてのコードを削除できます。
落とし穴と次のステップ
この時点で、最初の移行が完了したと言えるかもしれません。おめでとうございます!しかし、Remixは一般的なReactアプリとは少し異なる方法で動作します。そうでなければ、そもそもなぜそれを構築する手間をかけたのでしょうか?😅
安全でないブラウザ参照
クライアントレンダリングコードベースをサーバーレンダリングコードベースに移行する際のよくある問題点は、サーバー上で実行されるコードにブラウザAPIへの参照が含まれている可能性があることです。状態の初期化時に見られる一般的な例を以下に示します。
function Count() {
const [count, setCount] = React.useState(
() => localStorage.getItem("count") || 0
);
React.useEffect(() => {
localStorage.setItem("count", count);
}, [count]);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}この例では、localStorageがグローバルストアとして使用され、ページのリロードを跨いでデータを保持しています。useEffect内でcountの現在の値でlocalStorageを更新していますが、useEffectはブラウザでのみ呼び出されるため、これは完全に安全です!しかし、localStorageに基づいて状態を初期化することは問題です。このコールバックはサーバーとブラウザの両方で実行されるためです。
解決策として、windowオブジェクトをチェックし、ブラウザでのみコールバックを実行することを考えるかもしれません。しかし、これは別の問題、恐ろしいハイドレーションの不一致につながる可能性があります。Reactは、サーバーでレンダリングされたマークアップがクライアントのハイドレーション中にレンダリングされるものと同一であることに依存しています。これにより、react-domはDOM要素とその対応するReactコンポーネントを一致させる方法を知り、イベントリスナーをアタッチし、状態の変化に合わせて更新を実行できます。そのため、ローカルストレージがサーバーで初期化したものとは異なる値を返す場合、新たな問題に対処する必要があります。
クライアントのみのコンポーネント
ここで考えられる解決策の1つは、サーバーで使用でき、ルートのローダーデータからプロップとしてコンポーネントに渡される、異なるキャッシングメカニズムを使用することです。しかし、サーバー上でコンポーネントをレンダリングすることがアプリケーションにとって重要ではない場合は、サーバーでのレンダリングを完全にスキップし、ハイドレーションが完了するまでブラウザでのレンダリングを待つという、より簡単な解決策があります。
// コンポーネントの外側のメモリ内状態では、ハイドレーションを安全に追跡できます。
// これは、`SomeComponent`のバージョンインスタンスがハイドレートされた後、一度だけ更新されるためです。そこから、
// ブラウザはルートの変更にわたってレンダリングの処理を引き継ぎ、ページが
// 再読み込みされ`isHydrating`がtrueにリセットされるまで、ハイドレーションの不一致について心配する必要がなくなります。
let isHydrating = true;
function SomeComponent() {
const [isHydrated, setIsHydrated] = React.useState(
!isHydrating
);
React.useEffect(() => {
isHydrating = false;
setIsHydrated(true);
}, []);
if (isHydrated) {
return <Count />;
} else {
return <SomeFallbackComponent />;
}
}この解決策を簡素化するために、remix-utilsコミュニティパッケージのClientOnlyコンポーネントの使用をお勧めします。その使用方法の例は、examplesリポジトリにあります。
React.lazy と React.Suspense
React.lazy と React.Suspense を使用してコンポーネントを遅延読み込みしている場合、使用しているReactのバージョンによっては問題が発生する可能性があります。React 18までは、React.Suspenseは当初ブラウザのみの機能として実装されていたため、サーバーでは動作しませんでした。
React 17を使用している場合、いくつかの選択肢があります。
- React 18にアップグレードする
- 上記で説明されているクライアント側のみのアプローチを使用する
- Loadable Componentsなどの代替の遅延読み込みソリューションを使用する
React.lazyとReact.Suspenseを完全に削除する
Remixは管理するすべてのルートのコード分割を自動的に処理するため、routesディレクトリに要素を移動する際には、React.lazyを手動で使用することはほとんど(もしあれば)必要ありません。
設定
さらなる設定は任意ですが、開発ワークフローの最適化に役立つ可能性のあるものが以下に示されています。
remix.config.js
すべてのRemixアプリは、プロジェクトルートにremix.config.jsファイルを受け入れます。設定はオプションですが、明確にするためにいくつか含めることをお勧めします。利用可能なすべてのオプションの詳細については、設定に関するドキュメントを参照してください。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
appDirectory: "app",
ignoredRouteFiles: ["**/*.css"],
assetsBuildDirectory: "public/build",
};jsconfig.json または tsconfig.json
TypeScriptを使用している場合、プロジェクトにtsconfig.jsonが既に存在する可能性があります。jsconfig.jsonはオプションですが、多くのエディターにとって役立つコンテキストを提供します。 以下は、言語設定に含めることをお勧めする最小限の設定です。
パスエイリアスを使用します。/_remix.config.jsのappDirectoryを変更する場合は、/_のパスエイリアスも更新する必要があります。
{
"compilerOptions": {
"jsx": "react-jsx",
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
}
}
}{
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"resolveJsonModule": true,
"moduleResolution": "Bundler",
"baseUrl": ".",
"noEmit": true,
"paths": {
"~/*": ["./app/*"]
}
}
}TypeScriptを使用している場合は、適切なグローバル型参照を使用して、プロジェクトのルートにremix.env.d.tsファイルを作成する必要もあります。
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node" />非標準インポートに関する注意
現時点では、変更を加えずにアプリを実行できる可能性があります。Create React Appや高度に設定されたバンドラーを使用している場合、スタイルシートや画像などのJavaScript以外のモジュールを含めるためにimportを使用している可能性があります。
Remixはほとんどの非標準インポートをサポートしておらず、それは正当な理由によるものだと考えています。以下は、Remixで遭遇する可能性のあるいくつかの違いとその移行時のリファクタリング方法の非網羅的なリストです。
アセットのインポート
多くのバンドラーは、画像やフォントなどの様々なアセットのインポートを可能にするプラグインを使用しています。これらは通常、アセットのファイルパスを表す文字列としてコンポーネントに取り込まれます。
import logo from "./logo.png";
export function Logo() {
return <img src={logo} alt="My logo" />;
}Remixでは、基本的に同じように動作します。<link>要素によって読み込まれるフォントなどのアセットについては、一般的にルートモジュールでインポートし、links関数が返すオブジェクトにファイル名を含めます。ルートlinksに関するドキュメントを参照してください。
SVG のインポート
Create React App やその他のビルドツールでは、SVG ファイルを React コンポーネントとしてインポートできます。これは SVG ファイルの一般的なユースケースですが、Remix ではデフォルトでサポートされていません。
// これは Remix では動作しません!
import MyLogo from "./logo.svg";
export function Logo() {
return <MyLogo />;
}SVG ファイルを React コンポーネントとして使用したい場合は、最初にコンポーネントを作成し、直接インポートする必要があります。React SVGR は、コマンドライン から、またはオンラインプレイグラウンドで(コピー&ペーストする場合)これらのコンポーネントを生成するのに役立つ優れたツールセットです。
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13a1 1 0 102 0V9.414l1.293 1.293a1 1 0 001.414-1.414z" />
</svg>export default function Icon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13a1 1 0 102 0V9.414l1.293 1.293a1 1 0 001.414-1.414z"
/>
</svg>
);
}CSS のインポート
Create React App や多くのビルドツールは、様々な方法でコンポーネントに CSS をインポートすることをサポートしています。Remix は、下記で説明するいくつかの一般的な CSS バンドルソリューションに加えて、通常の CSS ファイルのインポートもサポートしています。
Route links エクスポート
Remixでは、通常のスタイルシートをルートコンポーネントファイルから読み込むことができます。スタイルシートをインポートしても、スタイルに対して特別な処理が行われるわけではなく、スタイルシートを読み込むために使用できるURLが返されます。コンポーネント内でスタイルシートを直接レンダリングするか、links エクスポートを使用できます。
アプリのスタイルシートとその他のいくつかのアセットをルートルートのlinks関数に移動してみましょう。
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import { Links } from "@remix-run/react";
import App from "./app";
import stylesheetUrl from "./styles.css";
export const links: LinksFunction = () => {
// `links` は、プロパティが `<link />` コンポーネントのプロップにマップされるオブジェクトの配列を返します。
return [
{ rel: "icon", href: "/favicon.ico" },
{ rel: "apple-touch-icon", href: "/logo192.png" },
{ rel: "manifest", href: "/manifest.json" },
{ rel: "stylesheet", href: stylesheetUrl },
];
};
export default function Root() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<Links />
<title>React App</title>
</head>
<body>
<App />
</body>
</html>
);
}32行目では、個々の<link />コンポーネントをすべて置き換えた<Links />コンポーネントをレンダリングしていることに気付くでしょう。ルートルートでのみリンクを使用する場合、これは重要ではありませんが、すべての子ルートは、ここでレンダリングされる独自のリンクをエクスポートする場合があります。links関数は、ユーザーが移動する可能性のあるページのリソースをプリフェッチできるPageLinkDescriptorオブジェクトを返すこともできます。
現在、既存のルートコンポーネントで、直接またはreact-helmetのような抽象化を介して、<link />タグをクライアントサイドでページに挿入している場合は、それを行うのをやめ、代わりにlinksエクスポートを使用できます。多くのコードと、場合によっては1つまたは2つの依存関係を削除できます!
CSS バンドリング
Remix は、CSS Modules、Vanilla Extract、およびCSS サイドエフェクトインポート を組み込みでサポートしています。これらの機能を使用するには、アプリケーションで CSS バンドリングを設定する必要があります。
まず、生成された CSS バンドルにアクセスするには、@remix-run/css-bundle パッケージをインストールします。
npm install @remix-run/css-bundle次に、cssBundleHref をインポートし、リンク記述子に追加します。これは、アプリケーション全体に適用されるように、ほとんどの場合 root.tsx にあるでしょう。
import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
export const links: LinksFunction = () => {
return [
...(cssBundleHref
? [{ rel: "stylesheet", href: cssBundleHref }]
: []),
// ...
];
};CSS バンドリングの詳細については、ドキュメントを参照してください。
注記: Remix は現在、Sass/Less の処理を直接サポートしていませんが、それらを別個のプロセスとして実行して CSS ファイルを生成し、それを Remix アプリにインポートすることは可能です。
<head> 内でのコンポーネントのレンダリング
<link> がルートコンポーネント内でレンダリングされ、最終的にルート <Links /> コンポーネントでレンダリングされるのと同様に、アプリはドキュメントの <head> に追加のコンポーネントをレンダリングするために、いくつかのインジェクションのトリックを使用する場合があります。これは多くの場合、ドキュメントの <title> や <meta> タグを変更するために実行されます。
links と同様に、各ルートは meta 関数をエクスポートすることもできます。この関数は、そのルートの <meta> タグ(<title>、<link rel="canonical">、<script type="application/ld+json"> など、メタデータに関連する他のいくつかのタグも含む)をレンダリングするために必要な値を返します。
meta の動作は links とわずかに異なります。ルート階層内の他の meta 関数の値をマージする代わりに、各リーフルートは独自のタグのレンダリングを担当します。これは以下の理由からです。
- 最適なSEOのために、メタデータに対するより細かい制御が必要になることが多い
- Open Graph プロトコル に従う一部のタグの場合、タグの順序によって、クローラーやソーシャルメディアサイトによる解釈に影響を与え、Remix が複雑なメタデータの結合方法を想定するのは予測が難しくなる
- 一部のタグは複数の値を許可しますが、一部のタグは許可しません。Remix は、これらのすべてのケースをどのように処理したいかを想定すべきではありません
インポートの更新
Remix は react-router-dom から取得できるすべてを再エクスポートしますが、これらのモジュールを @remix-run/react から取得するようにインポートを更新することをお勧めします。多くの場合、これらのコンポーネントには、Remix向けに最適化された追加の機能がラップされています。
変更前:
import { Link, Outlet } from "react-router-dom";変更後:
import { Link, Outlet } from "@remix-run/react";最後に
包括的な移行ガイドを提供するために最善を尽くしましたが、Remixは多くの既存のReactアプリとは大きく異なるいくつかの重要な原則に基づいてゼロから構築されていることに注意することが重要です。現時点ではアプリが動作する可能性がありますが、ドキュメントを精査し、APIを探索するにつれて、コードの複雑さを大幅に削減し、エンドユーザーエクスペリエンスを向上させることができると思います。そこに到達するには少し時間がかかるかもしれませんが、「大きな仕事も少しずつやればできる」ということです。
さあ、あなたのアプリをRemixしましょう!その過程で生まれるもの気に入っていただけると思います!💿