現代前端開發 - 那些我使用過的 Pattern 和 工具

ErrorBaker

找麵包

搜尋烘焙師、標題、內文或口味

今天想吃什麼?

試試技術名稱、口味,或烘焙師的名字。

以 React Hook Form + TS 完成一個 Nested Todo List 來舉例

不多說先看結果:Nested Todo List - GitHub Pages

如果說你比較習慣看 Code,可以看這裡:Source Code

# 誰可能比較適合閱讀這篇文章?

如果你不是上述對象,也沒有上述問題,你可以考慮改去讀讀其他夥伴們的 優秀作品

# 契機

最近工作需要將專案中的表單 Migrate 到 React Hook Form,原來是想要看懂它的 Source Code 是怎麼實作,來寫一篇分享文,但因為看不懂(明明都是最熟悉的 React + TS 但我就是看不懂),加上拖延症爆發,於是去看了隔壁的 Xiang 寫的這篇《進階版 To do list》,警覺自己還沒學會走就想飛,連 React Hook Form 都還沒學會使用,竟然就妄想看懂 Source Code(?,於是便想用 React Hook Form 加上原來就已經在使用的 React + TS,做做看這個 Side Project,順便熟悉一下 React Hook Form 的使用,於是就有了這篇文章。

# 從 User Story 開始

成品 Nested Todo List

# 我是如何開始學習 React Hook Form 的?

雖然已經先用 React + TS 試著實作過 Nested Todo List,但用的都是 React 內建的 useState,並不熟悉 React Hook Form 的 API,於是我先掃描了 官方文件的 API,然後看了一下 官方的 Example,知道 Nested List 是可以做到的(知道一個東西已經做的到,比不確定做不做的到,在實作上的把握和信心程度會差很多),再知道可以用 useForm 和 useFieldArray 這 2 個 Custom Hook 來做之後,就開始邊看 Example 和 API 文件邊實作邊整理自己的程式碼。

大致的步驟是

  1. 完成功能
  2. 反覆優化(抽象、樣式)
  3. 改完收工

有興趣可以看看我那些笨拙可愛的過程[1]

# 從這個 Over Engineering 的專案,看常見的 React Pattern 和工具使用

以我對前端工程認知,其實就是在開發的過程,不斷的將重複的部分不斷抽象、再組合.不同組合和抽象方式,解決的是不同場景下的問題,值得注意的是,每一次的抽象都會帶來認知上的負擔,良好的架構、Pattern 和命名會讓我們對功能產生正確的預期,這也是他們之所以重要的原因。

從一個專案中,會持續重組大致就是這幾大類

對應到整個專案的結構就會是

# Type 的組合

# 自定義 Type

type FormValues = {
  nestedList: {
    value: string;
    isDone: boolean;
    list?: {
      value: string;
      isDone: boolean;
    }[];
  };
};
type FormValues = {
  nestedList: NestedList;
};

type NestedList = (Todo & {
  list?: List;
})[];

type List = Todo[];

type Todo = {
  value: string;
  isDone: boolean;
};
const initialList: NestedList = [
  {
    value: "todo group 2",
    isDone: true,
  },
  {
    value: "todo group 1",
    isDone: false,
    list: [
      {
        value: "group todo 2",
        isDone: false,
      },
      {
        value: "group todo 1",
        isDone: true,
      },
    ],
  },
];

# Library 定義的 Type

import React, { ReactElement, FC } from "react"; // from library type definition

const TodoForm: (props: {children: ReactElement}) => ReactElement = () => <form />
const TodoList: React.FC = ({children}) => <ol>{children}</ol>
const Todo: FC = ({children}) => <li>{children}</li>

<TodoForm>
  <TodoList>
    <Todo/>
    <Todo/>
    <Todo/>
    <Todo/>
  </TodoList>
</TodoForm>

# 綜合使用

// library type definition
import { FC, ReactElement, ChangeEventHandler } from "react";
import { UseFormRegisterReturn } from "react-hook-form/dist/types/form";

// self type definition
import { NestedList } from "../types/todo";

const TodoForm: FC = function () {
  const {} = useForm<{ nestedList: NestedList }>();

  return <form />;
};

const Todo: (props: {
  onRegister: (name: "isDone" | "value") => UseFormRegisterReturn;
  children?: ReactElement;
}) => ReactElement = function ({ onRegister, children }) {
  const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
    onRegister?.("isDone").onChange(e);
    onCheck?.();
  };

  return (
    <li>
      <StyledCheckbox onChange={handleChange} />
      <StyledTextInput />
      {children}
    </li>
  );
};

# Component 的組合與複用

# Composition Component

const TodoList: FC = function ({ children }) {
  return <ol>{children}</ol>;
};

const Todo: FC & { TodoList: typeof TodoList } = function () {
  return <li>{children}</li>;
};

Todo.TodoList = TodoList;
<TodoList>
  <Todo>
    <Todo.TodoList>
      <Todo>
    <Todo.TodoList>
  </Todo>
</TodoList>
<ol>
  <li>
    <ol>
      <li></li>
    </ol>
  </li>
</ol>

# Props

# Style 的組合與複用

# Utility First CSS (for layout)

Tailwind, Bootstrap, etc

@tailwind base;
@tailwind utilities;
<div className="flex items-center gap-3 mb-3">
  <input type="checkbox" />
  <input type="text" />
  <button />
</div>

# CSS in JS (for customize)

Styled Component, Emotion, Linaria, etc.

import { Button } from "@geist-ui/react"; // any ui library
import styled from "styled-components"; // any css in js solution

const StyledAddButton = styled(Button)`
  &&& {
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    padding: 0;
    width: 33px;
    height: 33px;
  }
`;
import styled, { css } from "styled-components";

const ContainerMixin = css`
  margin: 0 auto;
  width: 500px;
`;

const StyledSection = styled.section`
  ${ContainerMixin}
`;

# Logic 的複用(React Hooks)

在這個專案中沒有直接用到任何 React 的 Hook(useState, useEffect, useRef...),因為這些 Hook 都藏在 React Hook Form 提供給我們 Custom Hook 裡面,我們可以直接使用,這些 Hook 提供給我們重複出現於表單處理的邏輯做開發。

import { useForm, useFieldArray } from "react-hook-form"; // any react hook library or custom hook

const { control, register, getValues, setValue, handleSubmit } = useForm(); // 經常用於表單資料獲取和提交
const { fields, prepend, remove } = useFieldArray({ name, control }); // 經常用於動態新增表單項目

# MISC

# Module

# 未來展望

那些我還做不到、還沒做、也許未來也不會做的事

# 感謝

天下文章一大抄,感謝巨人們的肩膀。

# 參考資料

# 推薦閱讀

《我是如何開始做不會的事》

# 備註

[1] 紀錄那些我笨拙的時刻

你有任何處理複雜表單,或學習新工具、新知識的經驗嗎?希望你能留言與我分享~


認識這位烘焙師

紀錄那些我笨拙可愛的時刻