カランのブログ

Kalan 頭像照片,在淡水拍攝,淺藍背景

四零二曜日電子報上線啦!訂閱訂起來

ソフトウェアエンジニア / 台湾人 / 福岡生活
このブログはRSS Feed をサポートしています。RSSリンクをクリックして設定してください。技術に関する記事はコードがあるのでブログで閲覧することをお勧めします。

今のモード ライト

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

記事のタイトルや概要は自動翻訳であるため(中身は翻訳されてない場合が多い)、変な言葉が出たり、意味伝わらない場合がございます。空いてる時間で翻訳します。

リミックスのフォームとデータ読み取りメカニズムの探討

最近 Remix は Twitter 上で話題になってて、多くのフロントエンドコミュニティが議論しています。 つい最近、社内ツールの開発が必要で、気まぐれに試してみるのもいいかなと思って導入してみました。

所感としては、Remix と Next.js は似ていて、同じくファイルルーティング、関数を定義してサーバーのデータを取得するような機能が存在しています。

remixはreact-routerの作者であるRyan Florence氏が開発したライブラリです。元々は有料のサービスとして宣伝したが、最近オープンソースに変更されました。

「おっと、またかフレームワークか」と感じるかもしれませんが、Remix には独特な特徴があります。それは <form> に対してのこだわりです。確かに SPA がメインになってる時代では、<form> 丸ごと使うアプリは少ないが、個人的には非常に便利だなと思いつつ、実装が綺麗でない限りはむしろ <form> の方が UX にいいと考えています。振り返ってみると、確かフォームの処理はフロントエンドを始めたばかりのときによくハマるタグでした。

あれ、でもフォームはブラウザを更新させるでしょう?それはいやだな。

Remix は <form> を使って非同期リクエストを実装できることに加えて、JavaScript がオフになった場合、ブラウザーの組み込みフォーム操作にフォールバックします。(そして、ブラウザエクスペリエンスを再編成するためにフォームを直接送信する方が、非同期よりもはるかに優れている場合があると思います 🤔)

この記事では、Remix 機能について説明するのではなく、フォーム処理とデータ読み取りの 2 つの部分について説明します。

フォーム処理にはどのようなメリットがあるか?

JavaScript や状態(React state など)を宣言しなくても格納できることはメリットだと思います。特に値のチェックが必要でない場合、もしくはブラウザの仕組みを利用してチェックする場合。

たとえば:

<form action="/my/api" method="post">
  <input name="user_name" type="text" />
  <input name="world" type="text" />
  <button type="submit">
    Submit
  </button>
</form>

デフォルトでは (JavaScript ない限りは)、ボタンを押すとブラウザはuser_nameworld フィールドをフォームデータとして POST /my/api に自動的にリクエストを送ります。

ただし、デフォルトの動作では、サーバーの実装も必要です。仮に /my/api が JSON で返す場合、ページ全体が JSON になるのでそれは困るでしょう。一般的にはサーバが 301 の status code を返すことで、成功したらこのページにいくようなパータンが程んとです。

改めて言うと、ブラウザを更新するだけで簡単になる場合があります。状態管理や履歴状態をうまく実装できてない場合、むしろ体験が悪くなるでしょう。

でもインスタントメッセージやチャットなどのリアルタイム性を重視するアプリケーションには、それは理想的ではありません。

<form action="/my/api" method="post" onSubmit={e => {
  e.preventDefault()
  // call API!
}}>
  <input name="user_name" type="text" />
  <input name="world" type="text" />
  <button type="submit">
    Submit
  </button>
</form>

フォームの操作全体がブラウザ仕組みによって処理され、フォームの値には FormData を使用してブラウザから簡単にアクセスできることです。

<form action="/my/api" method="post" onSubmit={e => {
  e.preventDefault()
  const formData = new FormData(e.currentTarget)
  const userName = formData.get('user_name')
  const world = formData.get('world')
}}>
  <input name="user_name" type="text" />
  <input name="world" type="text" />
  <button type="submit">
    Submit
  </button>
</form>

これにより、ref や state を使用して入力値を取得する必要がなくなります。フォームのネイティブメカニズムを使わずに、React では次のように書いてしまうかもしれません:

const Component = () => {
  const [value, setVal1] = useState()
  const [value, setVal2] = useState()
  const [value, setVal3] = useState()
  return <>
    <input value={value1} onChange={handleChange} />
    <input value={value2} onChange={handleChange} />
    <input value={value3} onChange={handleChange} />
  </>
}

入力フィールドを増やすと、usestateも増え、書くのが面倒になることが分かります。もちろん、この部分は抽象化レイヤーによって解決することもできますが、これによってオーバーヘッドも増えます。(オーバーヘッドは悪い意味ではなく、トレドオフの問題だと思われる)

ただし、非同期に変更すると、考慮すべき状況がいくつかあります:

  • 読み込み中:フォームを送信中です。現時点では、ユーザーが操作できないようにフィールドを無効に設定する必要がある
  • フォーム検証エラー:フィールドの値を保持し、エラーメッセージを表示する必要がある
  • API 成功:値をリセットする

Remix ではフォームのみや非同期モード同時にサポートされています。なぜこの機能が好きなのかというと、多くの場合はデーターを送りたいだけに大量のコードを書いて、非同期処理を行うのに作業が煩雑になって、実際には <form> だけで解決できる場合が多いと思うからです。

同期の場合

ステートを宣言せずにフォームコンテンツをスケジュールで定義することでこれを実行できます。

// routes/users/index.js
export default function UserProfile() {
  return <form method="post" action="/users">
    <label>
      Name: <input type="text" name="userName" />
    </label>
    <label>
      Age: <input type="text" name="age" />
    </label>
    <button>Submit</button>
  </form>
}

サーバー側で対応する処理を追加するために、remix は同じファイルに action という関数を追加することでこれを行います。

import { redirect } from 'remix';

export async function action({ request }) {
  const formData = await request.formData()
  const user = await createUser(formData)
  return redirect(`/users/${user.id}`)
}

// routes/users/index.js
export default function UserProfile() {
  return <form method="post" action="/users">
    <label>
      Name: <input type="text" name="userName" />
    </label>
    <label>
      Age: <input type="text" name="age" />
    </label>
    <button>Submit</button>
  </form>
}

在 HTTP 方法不為 GET 的時候,action 函數就會被呼叫,所以要在 action 函數裏頭判斷 HTTP 方法為何。

這個 redirect 函數是由 remix 所提供的,會在伺服器端轉導至 users/id 頁面。這邊很重要的一點是轉導這件事是發生在伺服器端,而不是使用 history.push。如果表單驗證或是伺服器回傳錯誤,可以透過 useActionData 拿到伺服器回傳的資料。

export async function action({ request }) {
  const formData = await request.formData()  
+  if (someError) {
+    json({ error: message })
+  }
  return redirect(`/users/${user.id}`)
}

// routes/users/index.js
export default UserProfile() {
+ const actionData = useActionData()
  return <form method="post" action="/users">
    <label>
      Name: <input type="text" name="userName" />
    </label>
    <label>
      Age: <input type="text" name="age" />
    </label>
+   {actionData.error && <p>{actionData.error}</p>}
    <button>Submit</button>
  </form>
}

非同步

對於非同步的需求,remix 提供了 Form 元件給開發者使用。為了顯示當前的表單繳交狀態,可以另外用 useTransition 來表示:

import { Form, useTransition } from 'remix'

// routes/users/index.js
export default function UserProfile() {
  const actionData = useActionData()
  const transition = useTransition()
  
  return <form method="post" action="/users">
    <label>
      Name:
      <input
        type="text"
        name="userName"
        disabled={transition.state === 'submitting'}
      />
    </label>
    {actionData.error && <p>{actionData.error}</p>}
    <button>Submit</button>
  </form>
}

除了這兩種機制以外,也可以用 useSubmit 自行決定發送表單的時機點。不管是同步與非同步,remix 對表單處理的思想是,當你發送了一個表單處理後會先經過一番操作,然後跳轉到某個頁面。

資料讀取機制 - loader 與 fetcher

在網頁當中常見的資料讀取時機主要有兩種:

  1. 當使用者透過 <a> 或是網址列存取頁面時就讀取頁面資料,如部落格文章
  2. 當使用者進行某個操作後才呼叫 API 存取資料,如點擊評論欄後呼叫 API 拿資料

第一種情況下 remix 提供了 loader 機制,只會在伺服器端渲染,所以元件在渲染時都已經是有資料的狀態,就不用做 loading 後渲染資料的操作跟程式碼實作。

第二種情況下 remix 則提供了 fetcher 機制,你可以用 useFetcher 做 mutation 跟讀取資料:

const MyComponent = () => {
  const fetcher = useFetcher()
  useEffect(() => {
    fetcher.load('/comments')
  }, [])

  if (fetcher.state === 'loading') {
    return <p>loading...</p>
  }

  return <div>
    <Comments comments={fetcher.data.comments} />
  </div>
}

不只讀取資料而已,fetcher 也可以用來做 mutation:

const MyComponent = ({ content }) => {
  const fetcher = useFetcher()
  
  return <fetcher.Form method="post" action="/comments">
    <input type="text" name="content"></input>
    <button type="submit">Submit</button>
  </fetcher.Form>
}

在 remix 背後會幫你處理上述提到的操作:

  • 點擊 submit 時將狀態改為 submitting 方便做載入中的 UI
  • 回傳資料後可以用 fetcher.data 取得
  • fethcer.submit 直接提交表單內容,但不會做跳轉(背後其實就是呼叫 API 跟各種狀態處理)

Edge Computing

另外一點則是 remix 不斷強調所謂 Edge server(CDN)的概念,也就是不單單只用純靜態網頁部署到 CDN,而是將伺服器部署各個節點來減少 Network traffic

的確,如果使用者的設備效能已經不再是瓶頸的話,那麼唯一有影響的就是網路速度了。這邊的 edge computing 是指把 server 部署到離使用者較近的地區,進而減少封包走的 routing。不過對於某些應用來說,服務的對象都是當地的使用者,區別可能沒有那麼大就是了。

總結

我會喜歡 remix 的處理方式,是因為它對表單的執著讓我想起以前也曾經有這樣的想法,透過原生表單機制可以省下一大堆 value 狀態宣告,透過表單的處理機制也可以應付一些簡單的應用場景,而不需要寫各種非同步 API,在開發時間不足的情況下,非同步要考慮的事情很多,往往一個沒處理好體驗就比傳統的表單還要差。

remix 兼顧了各種場景,在沒有 JavaScript 的情況下可以走原生表單機制,需要即時性可以用 Form 元件來組裝,需要更多 SPA 的互動場景可以用 fetcher,對於前端最在意的資料處理來說問題解決了一大半。

次の記事

avr-libc 中的 ATOMIC_BLOCK

前の記事

C言語における文字列処理

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee