remix-logo

2024 為什麼我開始學 REMIX 而不是 NEXT.JS?

這篇會介紹 Remix 的主要特色和入門知識,包含 Loader、Action、Outlet。

Reference: https://remix.run | https://nextjs.org

Why Remix?

Remix 的特點就是一個檔案就可以處理全端資料,Remix 會根據 function 名稱幫你決定他該在前端還是後端。以往建立一個 Application,我們必須建立 HTML/CSS/JavaScript 的前端頁面,不論是用 React/Vue/Angular 或其他函式庫或框架,然後另外寫後端 API 讓前端 Fetch,JavaScript 提供 Node.js,Python 有 Flask/Django,可能你也聽過 Java、Ruby、PHP,整個搞得好像很複雜需要會很多東西,但說到底就是一個人類軀殼(前端),每個動作都要腦袋(後端)去思考應該丟出什麼資料。哦不過 Remix 後端其實底子是 Express,我覺得他更像是幫你整合一些常用功能的東東。

以下可以看到一般進到網頁後會跑一些 Fetch 去取得資料,而 Remix 直接把整包丟給前端,全部一起出現,所以省略了前後端互動的等待時間。一般進入頁面會需要「前端觸發 > 後端回覆」,Remix 主要就是省去前端觸發的部分。

Server-Side Rendering

Remix 第一個優勢,所有的資料都是在後端處理好後直接一次丟給使用者,所以他們進到網頁後可以看到完整的畫面,在 SEO 部分也會加分,因為網頁更完整。當然也可以在前端 Fetch。

具體來說,Remix 的一個檔案會包含:

  1. Outlet(整個頁面 / Component 該長怎麼樣)
  2. Loader(取得資料之後丟給 Outlet)
  3. Action(處理使用者點擊 Form 觸發的 Post)

Outlet

每個 route 裡的檔案可以看成一個 React Component,必須要 export default function YourThing() {},在建立的時候他會被丟到 root.tsx 的 <Outlet />,或是他的上層,這個會在前端頁面顯示,如果有在寫 React 應該會看得很習慣。

// ./my-remix-app/app/route/home.tsx

// The default export is the component that will be
// rendered when a route matches the URL. This runs
// both on the server and the client
export default function Projects() {
  return (
    <div>
      {/* outlets render the nested child routes
          that match the URL deeper than this route,
          allowing each layout to co-locate the UI and
          controller code in the same file */}
      <Outlet />
    </div>
  );
}

Loader

Loader 會處理跟後端互動的東西,然後 return 資料,在 function YourThing() {} 裡面可以呼叫 useLoaderData() 取得他 return 的東西。這邊可以直接寫跟資料庫相關的程式碼,而不需要另外寫個 API Route 或文件,不過通常還是會做一個完整的 DB 互動程式碼文件統一管理,只是它就不會是一個 Route,而只是一個 Function 的統整文件。

// ./my-remix-app/app/route/home.tsx

// Loaders only run on the server and provide data
// to your component on GET requests
// 這個 loader function 會在後端執行
export async function loader() {
  return json(await db.projects.findAll());
}

export default function Projects() {
  const projects = useLoaderData<typeof loader>();

  return (
    <div>
      {projects.map((project) => (
        <Link key={project.slug} to={project.slug}>
          {project.title}
        </Link>
      ))}

      // ...Outlet 的東西
    </div>
  );
}

Action

Action 其實就是處理 Post 來的資料,跟 Loader 一樣是在後端執行,return 的東東可以在 function YourThing() {} 裡面可以呼叫 useActionData()。他跟基本的 HTML Form 其實是一樣的,所以即使無法使用 JavaScript,頁面還是可以正常執行。

// ./my-remix-app/app/route/home.tsx

export default function Projects() {
  const actionData = useActionData<typeof action>();

  return (
    <div>
      // ...Loader 的東東

      <Form method="post">
        <input name="title" />
        <button type="submit">Create New Project</button>
      </Form>
      {actionData?.errors ? (
        <ErrorMessages errors={actionData.errors} />
      ) : null}

      // Outlet 的東東
    </div>
  );
}

// Actions only run on the server and handle POST
// PUT, PATCH, and DELETE. They can also provide data
// to the component
export async function action({
  request,
}: ActionFunctionArgs) {
  const form = await request.formData();
  const errors = validate(form);
  if (errors) {
    return json({ errors });
  }
  await createProject({ title: form.get("title") });
  return json({ ok: true });
}

Conclusion

雖然這個真的讓之前的前後端東東變得很方便簡單,不過目前缺點就是相關的資料跟生態圈很小,而且是真的蠻簡單的,可能會少了一些可玩性,所以如果你害怕看 Documents 或擔心求職的話,建議還是直接學 Next 哦!

整個檔案就長這樣(https://remix.run/docs/en/main/discussion/introduction#http-handler-and-adapters):

// ./my-remix-app/app/route/home.tsx

// Loaders only run on the server and provide data
// to your component on GET requests
export async function loader() {
  return json(await db.projects.findAll());
}

// The default export is the component that will be
// rendered when a route matches the URL. This runs
// both on the server and the client
export default function Projects() {
  const projects = useLoaderData<typeof loader>();
  const actionData = useActionData<typeof action>();

  return (
    <div>
      {projects.map((project) => (
        <Link key={project.slug} to={project.slug}>
          {project.title}
        </Link>
      ))}

      <Form method="post">
        <input name="title" />
        <button type="submit">Create New Project</button>
      </Form>
      {actionData?.errors ? (
        <ErrorMessages errors={actionData.errors} />
      ) : null}

      {/* outlets render the nested child routes
          that match the URL deeper than this route,
          allowing each layout to co-locate the UI and
          controller code in the same file */}
      <Outlet />
    </div>
  );
}

// Actions only run on the server and handle POST
// PUT, PATCH, and DELETE. They can also provide data
// to the component
export async function action({
  request,
}: ActionFunctionArgs) {
  const form = await request.formData();
  const errors = validate(form);
  if (errors) {
    return json({ errors });
  }
  await createProject({ title: form.get("title") });
  return json({ ok: true });
}