在 Remix 中開發提供了豐富的工具集,這些工具的功能有時會重疊,讓新手感到困惑。在 Remix 中有效開發的關鍵在於了解每個工具的細微差別和適當的使用案例。本文旨在闡明何時以及為何使用特定的 API。
了解這些 API 的區別和交集對於高效且有效地進行 Remix 開發至關重要。
在這些工具之間進行選擇時,主要標準是您是否希望 URL 發生變化
需要變更 URL:在頁面之間導覽或轉換時,或在建立或刪除記錄等特定動作之後。這可確保使用者的瀏覽器歷史記錄準確反映他們在應用程式中的歷程。
不需要變更 URL:對於不會顯著變更目前視圖的內容或主要內容的動作。這可能包括更新個別欄位或不需要新 URL 或頁面重新載入的次要資料操作。這也適用於使用 fetcher 載入彈出視窗、組合方塊等資料。
這些動作通常反映使用者情境或狀態的重大變更
建立新紀錄:建立新紀錄後,通常會將使用者重新導向至專用於該新紀錄的頁面,讓他們可以在其中檢視或進一步修改它。
刪除紀錄:如果使用者位於專用於特定紀錄的頁面,並決定刪除它,則下一步的邏輯是將他們重新導向至一般頁面,例如所有紀錄的清單。
對於這些情況,開發人員應考慮組合使用 <Form>
、useActionData
和 useNavigation
。可以協調使用這些工具中的每一個來處理表單提交、呼叫特定動作、擷取與動作相關的資料,以及分別管理導覽。
這些動作通常更細微,不需要使用者切換情境
更新單一欄位:也許使用者想要變更清單中項目的名稱或更新紀錄的特定屬性。此動作是次要的,不需要新的頁面或 URL。
從清單中刪除紀錄:在清單檢視中,如果使用者刪除項目,他們可能會希望停留在清單檢視中,並且該項目不再出現在清單中。
在清單檢視中建立紀錄:將新項目新增至清單時,使用者通常會留在該情境中,看到他們的新項目新增至清單,而不會進行完整的頁面轉換。
為彈出視窗或組合方塊載入資料:當為彈出視窗或組合方塊載入資料時,使用者的情境保持不變。資料會在背景中載入,並顯示在小型、獨立的 UI 元素中。
對於此類動作,useFetcher
是首選的 API。它用途廣泛,結合了其他四個 API 的功能,非常適合用於 URL 應保持不變的任務。
如您所見,這兩組 API 有許多相似之處
導覽/URL API | Fetcher API |
---|---|
<Form> |
<fetcher.Form> |
useActionData() |
fetcher.data |
navigation.state |
fetcher.state |
navigation.formAction |
fetcher.formAction |
navigation.formData |
fetcher.formData |
import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { redirect } from "@remix-run/node"; // or cloudflare/deno
import {
Form,
useActionData,
useNavigation,
} from "@remix-run/react";
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const errors = await validateRecipeFormData(formData);
if (errors) {
return json({ errors });
}
const recipe = await db.recipes.create(formData);
return redirect(`/recipes/${recipe.id}`);
}
export function NewRecipe() {
const { errors } = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting =
navigation.formAction === "/recipes/new";
return (
<Form method="post">
<label>
Title: <input name="title" />
{errors?.title ? <span>{errors.title}</span> : null}
</label>
<label>
Ingredients: <textarea name="ingredients" />
{errors?.ingredients ? (
<span>{errors.ingredients}</span>
) : null}
</label>
<label>
Directions: <textarea name="directions" />
{errors?.directions ? (
<span>{errors.directions}</span>
) : null}
</label>
<button type="submit">
{isSubmitting ? "Saving..." : "Create Recipe"}
</button>
</Form>
);
}
此範例利用 <Form>
、useActionData
和 useNavigation
來促進直覺式的記錄建立流程。
使用 <Form>
可確保直接且邏輯性的導覽。建立記錄後,使用者自然會被引導至新食譜的唯一 URL,從而強化其動作的結果。
useActionData
連接伺服器和用戶端,針對提交問題提供立即回饋。這種快速回應讓使用者能夠順利地修正任何錯誤。
最後,useNavigation
會動態反映表單的提交狀態。這種細微的 UI 變更(例如切換按鈕的標籤)可向使用者保證他們的動作正在處理中。
結合使用時,這些 API 可提供結構化導覽和回饋的平衡組合。
現在考慮一下,我們正在查看一個食譜清單,每個項目都有刪除按鈕。當使用者按一下刪除按鈕時,我們希望從資料庫中刪除食譜,並將其從清單中移除,而不會離開清單。
首先考慮在頁面上取得食譜清單的基本路由設定
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
export async function loader({
request,
}: LoaderFunctionArgs) {
return json({
recipes: await db.recipes.findAll({ limit: 30 }),
});
}
export default function Recipes() {
const { recipes } = useLoaderData<typeof loader>();
return (
<ul>
{recipes.map((recipe) => (
<RecipeListItem key={recipe.id} recipe={recipe} />
))}
</ul>
);
}
現在我們將查看刪除食譜的動作和轉譯清單中每個食譜的元件。
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const id = formData.get("id");
await db.recipes.delete(id);
return json({ ok: true });
}
const RecipeListItem: FunctionComponent<{
recipe: Recipe;
}> = ({ recipe }) => {
const fetcher = useFetcher();
const isDeleting = fetcher.state !== "idle";
return (
<li>
<h2>{recipe.title}</h2>
<fetcher.Form method="post">
<button disabled={isDeleting} type="submit">
{isDeleting ? "Deleting..." : "Delete"}
</button>
</fetcher.Form>
</li>
);
};
在此案例中使用 useFetcher
非常完美。我們想要就地更新,而不是離開或重新整理整個頁面。當使用者刪除食譜時,會呼叫動作,而 fetcher 會管理對應的狀態轉換。
這裡的主要優勢在於維護情境。刪除完成時,使用者會留在清單上。fetcher 的狀態管理功能可用來提供即時回饋:它會在 "Deleting..."
和 "Delete"
之間切換,清楚地指示正在進行的程序。
此外,由於每個 fetcher 都可以自主管理其本身的狀態,因此個別清單項目上的作業會變得獨立,確保對一個項目執行的動作不會影響其他項目 (雖然頁面資料的重新驗證是一個共同關注的問題,涵蓋在 網路並行管理 中)。
本質上,useFetcher
提供了一種無縫機制,用於執行不需要更改 URL 或導航的操作,透過提供即時回饋和保持上下文來增強使用者體驗。
假設您想要在當前使用者瀏覽頁面一段時間並滾動到頁面底部後,將某篇文章標記為已讀。您可以建立一個類似以下的 hook:
function useMarkAsRead({ articleId, userId }) {
const marker = useFetcher();
useSpentSomeTimeHereAndScrolledToTheBottom(() => {
marker.submit(
{ userId },
{
action: `/article/${articleId}/mark-as-read`,
method: "post",
}
);
});
}
每當您顯示使用者頭像時,您可以加入懸停效果,從 loader 取得資料並將其顯示在彈出視窗中。
export async function loader({
params,
}: LoaderFunctionArgs) {
return json(
await fakeDb.user.find({ where: { id: params.id } })
);
}
function UserAvatar({ partialUser }) {
const userDetails = useFetcher<typeof loader>();
const [showDetails, setShowDetails] = useState(false);
useEffect(() => {
if (
showDetails &&
userDetails.state === "idle" &&
!userDetails.data
) {
userDetails.load(`/users/${user.id}/details`);
}
}, [showDetails, userDetails]);
return (
<div
onMouseEnter={() => setShowDetails(true)}
onMouseLeave={() => setShowDetails(false)}
>
<img src={partialUser.profileImageUrl} />
{showDetails ? (
userDetails.state === "idle" && userDetails.data ? (
<UserPopup user={userDetails.data} />
) : (
<UserPopupLoading />
)
) : null}
</div>
);
}
Remix 提供了一系列工具,以滿足各種不同的開發需求。雖然某些功能可能看起來重疊,但每個工具都是針對特定的情境而設計的。透過了解 <Form>
、useActionData
、useFetcher
和 useNavigation
的複雜性和理想應用,開發人員可以建立更直觀、反應更快且使用者友好的 Web 應用程式。