译:101个React技巧#1组件组织

367 阅读5分钟

1. 使用自闭合标签保持代码简洁

// ❌ 不好:过于冗长
<MyComponent></MyComponent>

// ✅ 好
<MyComponent/>

2. 优先使用 fragments 而非 DOM 节点(如 div、span 等)来组合元素

在 React 中,每个组件必须返回单个元素。不要使用 <div><span> 包裹多个元素,而是使用 <Fragment> 来保持 DOM 整洁。

❌ 不好: 使用 div 会使你的 DOM 变得混乱,可能需要更多的 CSS 代码。

function Dashboard() {
  return (
    <div>
      <Header />
      <Main />
    </div>
  );
}

✅ 好: <Fragment> 可以包裹元素而不影响 DOM 结构。

function Dashboard() {
  return (
    <Fragment>
      <Header />
      <Main />
    </Fragment>
  );
}

3. 使用 React 片段简写 <></>(除非你需要设置 key)

❌ 不好: 以下代码不必要地冗长。

<Fragment>
  <FirstChild />
  <SecondChild />
</Fragment>

✅ 好: 除非你需要 key,否则 <></> 更简洁。

<>
  <FirstChild />
  <SecondChild />
</>;

// 由于需要 key,这里必须使用 `Fragment`。
function List({ users }) {
  return (
    <div>
      {users.map((user) => (
        <Fragment key={user.id}>
          <span>{user.name}</span>
          <span>{user.occupation}</span>
        </Fragment>
      ))}
    </div>
  );
}

4. 优先展开 props 而不是逐个访问它们

❌ 不好: 以下代码更难阅读(尤其是在代码规模变大时)。

// 我们在代码中到处使用 `props…`。
function TodoList(props) {
  return (
    <div>
      {props.todos.map((todo) => (
        <div key={todo}>
          <button
            onClick={() => props.onSelectTodo(todo)}
            style={{
              backgroundColor: todo === props.selectedTodo ? "gold" : undefined,
            }}
          >
            <span>{todo}</span>
          </button>
        </div>
      ))}
    </div>
  );
}

✅ 好: 以下代码更简洁。

function TodoList({ todos, selectedTodo, onSelectTodo }) {
  return (
    <div>
      {todos.map((todo) => (
        <div key={todo}>
          <button
            onClick={() => onSelectTodo(todo)}
            style={{
              backgroundColor: todo === selectedTodo ? "gold" : undefined,
            }}
          >
            <span>{todo}</span>
          </button>
        </div>
      ))}
    </div>
  );
}

5. 设置 props 的默认值时,在解构它们时进行设置

❌ 不好: 你可能需要在多个地方定义默认值,并引入新的变量。

function Button({ onClick, text, small, colorScheme }) {
  let scheme = colorScheme || "light";
  let isSmall = small || false;
  return (
    <button
      onClick={onClick}
      style={{
        color: scheme === "dark" ? "white" : "black",
        fontSize: isSmall ? "12px" : "16px",
      }}
    >
      {text ?? "Click here"}
    </button>
  );
}

✅ 好: 你可以在顶部的一个地方设置所有默认值。这使得其他人更容易找到它们。

function Button({
  onClick,
  text = "Click here",
  small = false,
  colorScheme = "light",
}) {
  return (
    <button
      onClick={onClick}
      style={{
        color: colorScheme === "dark" ? "white" : "black",
        fontSize: small ? "12px" : "16px",
      }}
    >
      {text}
    </button>
  );
}

6. 传递 string 类型的 props 时去掉花括号。

// ❌ 不好:不需要花括号
<Button text={"Click me"} colorScheme={"dark"} />

// ✅ 好
<Button text="Click me" colorScheme="dark" />

7. 在使用 value && <Component {...props}/> 之前,确保 value 是布尔值,以防止在屏幕上显示意外的值。

❌ 不好: 当列表为空时,0 会显示在屏幕上。

export function ListWrapper({ items, selectedItem, setSelectedItem }) {
  return (
    <div className="list">
      {items.length && ( // 列表为空时为 `0`
        <List
          items={items}
          onSelectItem={setSelectedItem}
          selectedItem={selectedItem}
        />
      )}
    </div>
  );
}

好: 当没有项目时,屏幕上不会显示任何内容。

export function ListWrapper({ items, selectedItem, setSelectedItem }) {
  return (
    <div className="list">
      {items.length > 0 && (
        <List
          items={items}
          onSelectItem={setSelectedItem}
          selectedItem={selectedItem}
        />
      )}
    </div>
  );
}

8. 使用函数(内联或非内联)避免用中间变量污染作用域

不好: 变量 gradeSumgradeCount 污染了组件的作用域。

function Grade({ grades }) {
  if (grades.length === 0) {
    return <>暂无成绩。</>;
  }

  let gradeSum = 0;
  let gradeCount = 0;

  grades.forEach((grade) => {
    gradeCount++;
    gradeSum += grade;
  });

  const averageGrade = gradeSum / gradeCount;

  return <>平均成绩: {averageGrade}</>;
}

好: 变量 gradeSumgradeCount 作用域在 computeAverageGrade 函数内。

function Grade({ grades }) {
  if (grades.length === 0) {
    return <>暂无成绩。</>;
  }

  const computeAverageGrade = () => {
    let gradeSum = 0;
    let gradeCount = 0;
    grades.forEach((grade) => {
      gradeCount++;
      gradeSum += grade;
    });
    return gradeSum / gradeCount;
  };

  return <>平均成绩: {computeAverageGrade()}</>;
}

💡 注意:你也可以在组件外部定义 computeAverageGrade 函数,然后在组件内部调用它。

9. 使用柯里化函数重用逻辑(并正确记忆回调函数)

❌ 不好: 更新字段的逻辑非常重复。

function Form() {
  const [{ name, email }, setFormState] = useState({
    name: "",
    email: "",
  });

  return (
    <>
      <h1>班级注册表单</h1>
      <form>
        <label>
          姓名: 
          <input
            type="text"
            value={name}
            onChange={(evt) =>
              setFormState((formState) => ({
                ...formState,
                name: evt.target.value,
              }))
            }
          />
        </label>
        <label>
          邮箱: 
          <input
            type="email"
            value={email}
            onChange={(evt) =>
              setFormState((formState) => ({
                ...formState,
                email: evt.target.value,
              }))
            }
          />
        </label>
      </form>
    </>
  );
}

✅ 好: 引入 createFormValueChangeHandler 函数,为每个字段返回正确的处理程序。

注意:如果你启用了 ESLint 规则 jsx-no-bind,这个技巧特别有用。你只需将柯里化函数包裹在 useCallback 中,就大功告成了!

function Form() {
  const [{ name, email }, setFormState] = useState({
    name: "",
    email: "",
  });

  const createFormValueChangeHandler = (field) => {
    return (event) => {
      setFormState((formState) => ({
        ...formState,
        [field]: event.target.value,
      }));
    };
  };

  return (
    <>
      <h1>班级注册表单</h1>
      <form>
        <label>
          姓名: 
          <input
            type="text"
            value={name}
            onChange={createFormValueChangeHandler("name")}
          />
        </label>
        <label>
          邮箱: 
          <input
            type="email"
            value={email}
            onChange={createFormValueChangeHandler("email")}
          />
        </label>
      </form>
    </>
  );
}

10. 将不依赖组件 props/state 的数据移到组件外部,使代码更简洁(更高效)

❌ 不好: OPTIONSrenderOption 不需要放在组件内部,因为它们不依赖任何 props 或 state。

此外,将它们放在组件内部意味着每次组件渲染时都会得到新的对象引用。如果我们将 renderOption 传递给一个用 memo 包裹的子组件,会破坏记忆化。

function CoursesSelector() {
  const OPTIONS = ["数学", "文学", "历史"];
  const renderOption = (option: string) => {
    return <option>{option}</option>;
  };

  return (
    <select>
      {OPTIONS.map((opt) => (
        <Fragment key={opt}>{renderOption(opt)}</Fragment>
      ))}
    </select>
  );
}

✅ 好: 将它们移到组件外部,使组件保持简洁,引用保持稳定。

const OPTIONS = ["数学", "文学", "历史"];
const renderOption = (option: string) => {
  return <option>{option}</option>;
};

function CoursesSelector() {
  return (
    <select>
      {OPTIONS.map((opt) => (
        <Fragment key={opt}>{renderOption(opt)}</Fragment>
      ))}
    </select>
  );
}

💡 注意:在这个例子中,你可以通过内联 option 元素来进一步简化代码。

const OPTIONS = ["数学", "文学", "历史"];

function CoursesSelector() {
  return (
    <select>
      {OPTIONS.map((opt) => (
        <option key={opt}>{opt}</option>
      ))}
    </select>
  );
}

11. 存储列表中选中的项时,存储项的 ID 而不是整个项

❌ 不好: 如果选中了一个项,但随后该项发生了变化(即我们为同一个 ID 得到了一个全新的对象引用),或者该项不再存在于列表中,selectedItem 要么会保留一个过时的值,要么会变得不正确。

function ListWrapper({ items }) {
  // 我们引用了整个项
  const [selectedItem, setSelectedItem] = useState<Item | undefined>();

  return (
    <>
      {selectedItem != null && <div>{selectedItem.name}</div>}
      <List
        items={items}
        selectedItem={selectedItem}
        onSelectItem={setSelectedItem}
      />
    </>
  );
}

✅ 好: 我们通过项的 ID(应该是稳定的)来存储选中的项。这确保了即使该项从列表中移除或其某个属性发生了变化,UI 也应该是正确的。

function ListWrapper({ items }) {
  const [selectedItemId, setSelectedItemId] = useState<number | undefined>();
  // 我们从列表中推导选中的项
  const selectedItem = items.find((item) => item.id === selectedItemId);

  return (
    <>
      {selectedItem != null && <div>{selectedItem.name}</div>}
      <List
        items={items}
        selectedItem={selectedItem}
        onSelectItem={(item) => setSelectedItemId(item?.id)}
      />
    </>
  );
}

14. 将所有状态和上下文分组到组件顶部

当所有状态和上下文都位于组件顶部时,就很容易发现哪些因素会触发组件重新渲染。

❌ 不好: 状态和上下文分散在各处,难以跟踪。

function App() {
  const [email, setEmail] = useState("");
  const onEmailChange = (event) => {
    setEmail(event.target.value);
  };
  const [password, setPassword] = useState("");
  const onPasswordChange = (event) => {
    setPassword(event.target.value);
  };
  const theme = useContext(ThemeContext);

  return (
    <div className={`App ${theme}`}>
      <h1>Welcome</h1>
      <p>
        Email: <input type="email" value={email} onChange={onEmailChange} />
      </p>
      <p>
        Password:{" "}
        <input type="password" value={password} onChange={onPasswordChange} />
      </p>
    </div>
  );
}

✅ 好: 所有状态和上下文都分组在顶部,便于查看。

function App() {
  const theme = useContext(ThemeContext);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const onEmailChange = (event) => {
    setEmail(event.target.value);
  };
  const onPasswordChange = (event) => {
    setPassword(event.target.value);
  };

  return (
    <div className={`App ${theme}`}>
      <h1>欢迎</h1>
      <p>
        邮箱: <input type="email" value={email} onChange={onEmailChange} />
      </p>
      <p>
        密码: 
        <input type="password" value={password} onChange={onPasswordChange} />
      </p>
    </div>
  );
}

阅读原文