100个React最佳实践小技巧

一. 组件相关

1. 使用自闭合组件

// 不好的写法
<Component></Component>
// 推荐写法
<Component />

2. 推荐使用Fragment组件而不是 DOM 元素来分组元素

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

不好的写法:使用 div 会使 DOM 变得杂乱,并且可能需要更多 CSS 代码。

import Header from "./header";
import Content from "./content";
import Footer from "./footer";

const Test = () => {
  return (
    <div>
      <Header />
      <Content />
      <Footer />
    </div>
  );
};

推荐写法: <Fragment> 包装元素而不影响 DOM 结构。

import Header from "./header";
import Content from "./content";
import Footer from "./footer";

const Test = () => {
  return (
    // 如果元素不需要添加属性,则可以使用简写形式<></>
    <Fragment>
      <Header />
      <Content />
      <Footer />
    </Fragment>
  );
};

3. 使用 React fragment 简写

使用 React fragment 简写 <></>(除非你需要设置一个 key 属性)

不好写法:下面的代码有点冗余。

const Test = () => {
  return (
    <Fragment>
      <Header />
      <Content />
      <Footer />
    </Fragment>
  );
};

推荐写法:

const Test = () => {
  return (
    <>
      <Header />
      <Content />
      <Footer />
    </>
  );
};

除非你需要一个 key 属性。

const Tools = ({ tools }) => {
    return (
        <Container>
            {
                tools?.map((item, index) => {
                    <Fragment key={`${item.id}-${index}`}>
                        <span>{ item.id }</span>
                        <span>{ item.name }</span>
                    <Fragment>
                })
            }
        </Container>
    )
}

4. 优先分散使用 props,而不是单独访问每个 props

不好的写法: 下面的代码更难阅读(特别是在项目较大时)。

const TodoLists = (props) => (
  <div className="todo-list">
    {props.todoList?.map((todo, index) => (
      <div className="todo-list-item" key={todo.uuid}>
        <p onClick={() => props.seeDetail?.(todo)}>
          {todo?.uuid}:{todo.text}
        </p>
        <div className="todo-list-item-btn-group">
          <button type="button" onClick={() => props.handleEdit?.(todo, index)}>
            编辑
          </button>
          <button
            type="button"
            onClick={() => props.handleDelete?.(todo, index)}
          >
            删除
          </button>
        </div>
      </div>
    ))}
  </div>
);
export default TodoLists;

推荐写法: 下面的代码更加简洁。

const TodoLists = ({ todoList, seeDetail, handleEdit, handleDelete }) => (
  <div className="todo-list">
    {todoList?.map((todo, index) => (
      <div className="todo-list-item" key={todo.uuid}>
        <p onClick={() => seeDetail?.(todo)}>
          {todo?.uuid}:{todo.text}
        </p>
        <div className="todo-list-item-btn-group">
          <button type="button" onClick={() => handleEdit?.(todo, index)}>
            编辑
          </button>
          <button type="button" onClick={() => handleDelete?.(todo, index)}>
            删除
          </button>
        </div>
      </div>
    ))}
  </div>
);
export default TodoLists;

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

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

const Text = ({ size, type }) => {
  const Component = type || "span";
  const comSize = size || "mini";
  return <Component size={comSize} />;
};

推荐写法,直接在对象解构里给出默认值。

const Text = ({ size = "mini", type: Component = "span" }) => {
  return <Component size={comSize} />;
};

6. 传递字符串类型属性时删除花括号。

不好的写法:带花括号的写法

<button type={"button"} className={"btn"}>
  按钮
</button>

推荐写法: 不需要花括号

<button type="button" className="btn">
  按钮
</button>

7. 确保Value是布尔值

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

不好的写法: 当列表的长度为 0,则有可能显示 0。

const DataList = ({ data }) => {
  return <Container>{data.length && <List data={data} />}</Container>;
};

推荐写法: 当列表没有数据时,则不会渲染任何东西。

const DataList = ({ data }) => {
  return <Container>{data.length > 0 && <List data={data} />}</Container>;
};

8. 使用函数(内联或非内联)避免中间变量污染你的上下文

不好的写法: 变量 totalCount 和 totalPrice 使组件的上下文变得混乱。

const GoodList = ({ goods }) => {
  if (goods.length === 0) {
    return <>暂无数据</>;
  }
  let totalCount = 0;
  let totalPrice = 0;
  goods.forEach((good) => {
    totalCount += good.count;
    totalPrice += good.price;
  });
  return (
    <>
      总数量:{totalCount};总价:{totalPrice}
    </>
  );
};

推荐写法: 将变量 totalCount 和 totalPrice 控制在一个函数内。

const GoodList = ({ goods }) => {
  if (goods.length === 0) {
    return <>暂无数据</>;
  }
  // 使用函数
  const {
    totalCount,
    totalPrice,
  } = () => {
    let totalCount = 0,
      totalPrice = 0;
    goods.forEach((good) => {
      totalCount += good.count;
      totalPrice += good.price;
    });
    return { totalCount, totalPrice };
  };
  return (
    <>
      总数量:{totalCount};总价:{totalPrice}
    </>
  );
};

个人更喜欢的写法: 封装成 hooks 来使用。

const useTotalGoods = ({ goods }) => {
  let totalCount = 0,
    totalPrice = 0;
  goods.forEach((good) => {
    totalCount += good.count;
    totalPrice += good.price;
  });
  return { totalCount, totalPrice };
};
const GoodList = ({ goods }) => {
  if (goods.length === 0) {
    return <>暂无数据</>;
  }
  const { totalCount, totalPrice } = useTotalGoods(goods);
  return (
    <>
      总数量:{totalCount};总价:{totalPrice}
    </>
  );
};

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

不好的写法: 表单更新字段重复。

const UserLoginForm = () => {
  const [{ username, password }, setFormUserState] = useState({
    username: "",
    password: "",
  });

  return (
    <>
      <h1>登陆</h1>
      <form>
        <div class="form-item">
          <label>用户名:</label>
          <input
            placeholder="请输入用户名"
            value={username}
            onChange={(e) =>
              setFormUserState((state) => ({
                ...state,
                username: e.target.value,
              }))
            }
          />
        </div>
        <div class="form-item">
          <label>密码:</label>
          <input
            placeholder="请输入密码"
            value={username}
            type="password"
            onChange={(e) =>
              setFormUserState((state) => ({
                ...state,
                password: e.target.value,
              }))
            }
          />
        </div>
      </form>
    </>
  );
};

推荐写法: 引入 createFormValueChangeHandler 方法,为每个字段返回正确的处理方法。

笔记: 如果你启用了 ESLint 规则 jsx-no-bind,此技巧尤其有用。你只需将柯里化函数包装在 useCallback 中。

const UserLoginForm = () => {
  const [{ username, password }, setFormUserState] = useState({
    username: "",
    password: "",
  });

  const createFormValueChangeHandler = (field: string) => {
    return (e) => {
      setFormUserState((state) => ({
        ...state,
        [field]: e.target.value,
      }));
    };
  };
  return (
    <>
      <h1>登陆</h1>
      <form>
        <div class="form-item">
          <label>用户名:</label>
          <input
            placeholder="请输入用户名"
            value={username}
            onChange={createFormValueChangeHandler("username")}
          />
        </div>
        <div class="form-item">
          <label>密码:</label>
          <input
            placeholder="请输入密码"
            value={username}
            type="password"
            onChange={createFormValueChangeHandler("password")}
          />
        </div>
      </form>
    </>
  );
};

10. 将不依赖组件 props/state 的数据移到组件外部,以获得更干净(和更高效)的代码

不好的写法: OPTIONS 和 renderOption 不需要位于组件内部,因为它们不依赖任何 props 或状态。此外,将它们保留在内部意味着每次组件渲染时我们都会获得新的对象引用。如果我们将 renderOption 传递给包裹在 memo 中的子组件,则会破坏缓存功能。
tsx 代码解读复制代码

const ToolSelector = () => {
  const options = [
    {
      label: "html工具",
      value: "html-tool",
    },
    {
      label: "css工具",
      value: "css-tool",
    },
    {
      label: "js工具",
      value: "js-tool",
    },
  ];
  const renderOption = ({
    label,
    value,
  }: {
    label?: string;
    value?: string;
  }) => <Option value={value}>{label}</Option>;
  return (
    <Select placeholder="请选择工具">
      {options.map((item, index) => (
        <Fragment key={`${item.value}-${index}`}>{renderOption(item)}</Fragment>
      ))}
    </Select>
  );
};

推荐写法: 将它们移出组件以保持组件干净和引用稳定。

const options = [
  {
    label: "html工具",
    value: "html-tool",
  },
  {
    label: "css工具",
    value: "css-tool",
  },
  {
    label: "js工具",
    value: "js-tool",
  },
];
const renderOption = ({ label, value }: { label?: string; value?: string }) => (
  <Option value={value}>{label}</Option>
);
const ToolSelector = () => {
  return (
    <Select placeholder="请选择工具">
      {options.map((item, index) => (
        <Fragment key={`${item.value}-${index}`}>{renderOption(item)}</Fragment>
      ))}
    </Select>
  );
};

笔记: 在这个示例中,你可以通过使用选项元素内联来进一步简化。

const options = [
  {
    label: "html工具",
    value: "html-tool",
  },
  {
    label: "css工具",
    value: "css-tool",
  },
  {
    label: "js工具",
    value: "js-tool",
  },
];
const ToolSelector = () => {
  return (
    <Select placeholder="请选择工具">
      {options.map((item, index) => (
        <Option value={item.value} key={`${item.value}-${index}`}>
          {item.label}
        </Option>
      ))}
    </Select>
  );
};

11. 存储列表组件中选定的对象时,存储对象 ID,而不是整个对象

不好的写法: 如果选择了某个对象但随后它发生了变化(即,我们收到了相同 ID 的全新对象引用),或者该对象不再存在于列表中,则 selectedItem 将保留过时的值或变得不正确。

const List = ({ data }) => {
  // 引用的是整个选中的是对象
  const [selectedItem, setSelectedItem] = useState<Item | undefined>();
  return (
    <>
      {selectedItem && <div>{selectedItem.value}</div>}
      <List
        data={data}
        onSelect={setSelectedItem}
        selectedItem={selectedItem}
      />
    </>
  );
};

推荐写法: 我们通过 ID(应该是稳定的)存储所选列表对象。这确保即使列表对象从列表中删除或其某个属性发生变化,UI 也应该正确。

const List = ({ data }) => {
  const [selectedItemId, setSelectedItemId] = useState<string | number>();
  // 我们从列表中根据选中id查找出选定的列表对象
  const selectedItem = data.find((item) => item.id === selectedItemId);
  return (
    <>
      {selectedItemId && <div>{selectedItem.value}</div>}
      <List
        data={data}
        onSelect={setSelectedItemId}
        selectedItemId={selectedItemId}
      />
    </>
  );
};

12. 如果需要多次用到 prop 里面的值,那就引入一个新的组件

不好的写法: 由于 type === null 的检查使得代码变得混乱。

注意: 由于hooks 规则,我们不能提前返回 null。

const CreatForm = ({ type }) => {
  const formList = useMemo(() => {
    if (type === null) {
      return [];
    }
    return getFormList({ type });
  }, [type]);
  const onHandleChange = useCallback(
    (id) => {
      if (type === null) {
        return;
      }
      // do something
    },
    [type]
  );
  if (type === null) {
    return null;
  }

  return (
    <>
      {formList.map(({ value, id, ...rest }, index) => (
        <item.component
          value={value}
          onChange={onHandleChange}
          key={id}
          {...rest}
        />
      ))}
    </>
  );
};

推荐写法: 我们引入了一个新组件 FormLists,它采用定义的表单项组件并且更加简洁。

const FormList = ({ type }) => {
  const formList = useMemo(() => getFormList({ type }), [type]);
  const onHandleChange = useCallback(
    (id) => {
      // do something
    },
    [type]
  );
  return (
    <>
      {formList.map(({ value, id, ...rest }, index) => (
        <item.component
          value={value}
          onChange={onHandleChange}
          key={id}
          {...rest}
        />
      ))}
    </>
  );
};
const CreateForm = ({ type }) => {
  if (type === null) {
    return null;
  }
  return <FormList type={type} />;
};

13. 将所有状态(state)和上下文(context)分组到组件顶部

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

不好的写法: 状态和上下文分散,难以跟踪。

const LoginForm = () => {
  const [username, setUsername] = useState("");
  const onHandleChangeUsername = (e) => {
    setUserName(e.target.value);
  };
  const [password, setPassword] = useState("");
  const onHandleChangePassword = (e) => {
    setPassword(e.target.value);
  };
  const theme = useContext(themeContext);

  return (
    <div class={`login-form login-form-${theme}`}>
      <h1>login</h1>
      <form>
        <div class="login-form-item">
          <label>用户名:</label>
          <input
            value={username}
            onChange={onHandleChangeUsername}
            placeholder="请输入用户名"
          />
        </div>
        <div class="login-form-item">
          <label>密码:</label>
          <input
            value={password}
            onChange={onHandleChangePassword}
            placeholder="请输入密码"
            type="password"
          />
        </div>
      </form>
    </div>
  );
};

推荐写法: 所有状态和上下文都集中在顶部,以便于快速定位。

const LoginForm = () => {
  // context
  const theme = useContext(themeContext);
  // state
  const [password, setPassword] = useState("");
  const [username, setUsername] = useState("");
  // method
  const onHandleChangeUsername = (e) => {
    setUserName(e.target.value);
  };
  const onHandleChangePassword = (e) => {
    setPassword(e.target.value);
  };

  return (
    <div class={`login-form login-form-${theme}`}>
      <h1>login</h1>
      <form>
        <div class="login-form-item">
          <label>用户名:</label>
          <input
            value={username}
            onChange={onHandleChangeUsername}
            placeholder="请输入用户名"
          />
        </div>
        <div class="login-form-item">
          <label>密码:</label>
          <input
            value={password}
            onChange={onHandleChangePassword}
            placeholder="请输入密码"
            type="password"
          />
        </div>
      </form>
    </div>
  );
};

二. 有效的设计模式与技巧

14. 利用 children 属性来获得更清晰的代码(以及性能优势)

利用子组件 props 来获得更简洁的代码(和性能优势)。使用子组件 props 有几个好处:

好处 1:你可以通过将 props 直接传递给子组件而不是通过父组件路由来避免 prop 混入。
好处 2:你的代码更具可扩展性,因为你可以轻松修改子组件而无需更改父组件。
好处 3:你可以使用此技巧避免重新渲染组件(参见下面的示例)。

不好的写法: 每当 Timer 渲染时,OtherSlowComponent 都会渲染,每次当前时间更新时都会发生这种情况。

const Container = () => <Timer />;

const Timer = () => {
  const [time, setTime] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => setTime(new Date()), 1000);
    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return (
    <>
      <h1>当前时间:{dayjs(time).format("YYYY-MM-DD HH:mm:ss")}</h1>
      <OtherSlowComponent />
    </>
  );
};

推荐写法: Timer 呈现时,OtherSlowComponent 不会呈现。

const Container = () => (
  <Timer>
    <OtherSlowComponent />
  </Timer>
);

const Timer = ({ children }) => {
  const [time, setTime] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => setTime(new Date()), 1000);
    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return (
    <>
      <h1>当前时间:{dayjs(time).formate("YYYY-MM-DD HH:mm:ss")}</h1>
      {children}
    </>
  );
};

15. 使用复合组件构建可组合代码

像搭积木一样使用复合组件,将它们拼凑在一起以创建自定义 UI。这些组件在创建库时效果极佳,可生成富有表现力且高度可扩展的代码。以下是一个以reach.ui为示例的代码:

<Menu>
  <MenuButton>
    操作吧 <span aria-hidden>▾</span>
  </MenuButton>
  <MenuList>
    <MenuItem onSelect={() => alert("下载")}>下载</MenuItem>
    <MenuItem onSelect={() => alert("复制")}>创建一个复制</MenuItem>
    <MenuLink as="a" href="https://reach.tech/menu-button/">
      跳转链接
    </MenuLink>
  </MenuList>
</Menu>

16. 使用渲染函数或组件函数 props 使你的代码更具可扩展性

假设我们想要显示各种列表,例如消息、个人资料或帖子,并且每个列表都应该可排序。

为了实现这一点,我们引入了一个 List 组件以供重复使用。我们可以通过两种方式解决这个问题:

不好的写法:选项 1。

List 处理每个项目的渲染及其排序方式。这是有问题的,因为它违反了开放封闭原则。每当添加新的项目类型时,此代码都会被修改。

export interface ListItem {
  id: string;
}
// 不好的列表组件写法
// 我们还需要了解这些接口
type PostItem = ListItem & { title: string };
type UserItem = ListItem & { name: string; date: Date };
type ListNewItem =
  | { type: "post"; value: PostItem }
  | { type: "user"; value: UserItem };
interface BadListProps<T extends ListNewItem> {
  type: T["type"];
  items: Array<T["value"]>;
}

const SortList = <T extends ListNewItem>({ type, items }: BadListProps<T>) => {
  const sortItems = [...items].sort((a, b) => {
    // 我们还需注意这里的比较逻辑,这里或者直接使用下方导出的比较函数
    return 0;
  });

  return (
    <>
      <h2>{type === "post" ? "帖子" : "用户"}</h2>
      <ul className="sort-list">
        {sortItems.map((item, index) => (
          <li className="sort-list-item" key={`${item.id}-${index}`}>
            {(() => {
              switch (type) {
                case "post":
                  return (item as PostItem).title;
                case "user":
                  return (
                    <>
                      <span>{(item as UserItem).name}</span>
                      <span> - </span>
                      <em>
                        加入时间: {(item as UserItem).date.toDateString()}
                      </em>
                    </>
                  );
              }
            })()}
          </li>
        ))}
      </ul>
    </>
  );
};

export function compareStrings(a: string, b: string): number {
  return a < b ? -1 : a == b ? 0 : 1;
}

推荐写法:选项 2。

List 采用渲染函数或组件函数,仅在需要时调用它们。

export interface ListItem {
  id: string;
}
interface ListProps<T extends ListItem> {
  items: T[]; // 列表数据
  header: React.ComponentType; // 头部组件
  itemRender: (item: T) => React.ReactNode; // 列表项
  itemCompare: (a: T, b: T) => number; // 列表项自定义排序函数
}

const SortList = <T extends ListItem>({
  items,
  header: Header,
  itemRender,
  itemCompare,
}: ListProps<T>) => {
  const sortedItems = [...items].sort(itemCompare);

  return (
    <>
      <Header />
      <ul className="sort-list">
        {sortedItems.map((item, index) => (
          <li className="sort-list-item" key={`${item.id}-${index}`}>
            {itemRender(item)}
          </li>
        ))}
      </ul>
    </>
  );
};

export default SortList;

17. 处理不同情况时,以避免保留旧状态

处理不同情况时, 使用 value === case && 以避免保留旧状态

不好的写法: 在如下示例中,在切换时计数器 count 不会重置。发生这种情况的原因是,在渲染同一组件时,其状态在currentTab更改后保持不变。

tab.tsx: 文件

const tabList = [
  {
    label: "首页",
    value: "tab-1",
  },
  {
    label: "详情页",
    value: "tab-2",
  },
];

export interface TabItem {
  label: string;
  value: string;
}
export interface TabProps {
  tabs: TabItem[];
  currentTab: string | TabItem;
  onTab: (v: string | TabItem) => void;
  labelInValue?: boolean;
}

const Tab: React.FC<TabProps> = ({
  tabs = tabList,
  currentTab,
  labelInValue,
  onTab,
}) => {
  const currentTabValue = useMemo(
    () => (labelInValue ? (currentTab as TabItem)?.value : currentTab),
    [currentTab, labelInValue]
  );
  return (
    <div className="tab">
      {tabs?.map((item, index) => (
        <div
          className={`tab-item${
            currentTabValue === item.value ? " active" : ""
          }`}
          key={`${item.value}-${index}`}
          onClick={() => onTab?.(labelInValue ? item : item.value)}
        >
          {item.label}
        </div>
      ))}
    </div>
  );
};

export default Tab;

Resource.tsx: 文件

export interface ResourceProps {
  type: string;
}
const Resource: React.FC<ResourceProps> = ({ type }) => {
  const [count, setCount] = useState(0);
  const onHandleClick = () => {
    setCount((c) => c + 1);
  };

  return (
    <div className="tab-content">
      你当前在{type === "tab-1" ? "首页" : "详情页"},
      <button onClick={onHandleClick} className="btn" type="button">
        点击我
      </button>
      增加访问{count}次数
    </div>
  );
};

推荐写法: 根据 currentTab 渲染组件或在类型改变时使用 key 强制重新渲染组件。

function App() {
  const [currentTab, setCurrentTab] = useState("tab-1");

  return (
    <>
      <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} />
      {currentTab === "tab-1" && <Resource type="tab-1" />}
      {currentTab === "tab-2" && <Resource type="tab-2" />}
    </>
  );
}
// 使用key属性
function App() {
  const [currentTab, setCurrentTab] = useState("tab-1");

  return (
    <>
      <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} />
      <Resource type={currentTab} key={currentTab} />
    </>
  );
}

完整示例代码可前往这里查看

18. 始终使用错误边界处理组件渲染错误

默认情况下,如果你的应用程序在渲染过程中遇到错误,整个 UI 都会崩溃。

为了防止这种情况,请使用错误边界来:

  • 即使发生错误,也要保持应用程序的某些部分正常运行。
  • 显示用户友好的错误消息并可选择跟踪错误。

提示:你可以使用 react-error-boundary 库。

三. key 与 ref

19. 使用 crypto.randomUUID 或 Math.random 生成 key

map 调用(也就是列表渲染)中的 JSX 元素始终需要 key。

假设你的元素还没有 key。在这种情况下,你可以使用 crypto.randomUUID、Math.random 或 uuid 库生成唯一 ID。

注意:请注意,旧版浏览器中未定义 crypto.randomUUID。

20. 确保你的列表项 id 是稳定的(即:它们在渲染中是不会发生变化的)

尽可能的让 id/key 可以稳定。

否则,React 可能会无用地重新渲染某些组件,或者触发一些功能异常,如下例所示。

不好的写法: 每次 App 组件渲染时 selectItemId 都会发生变化,因此设置 id 的值将永远不会正确。
tsx 代码解读复制代码

const App = () => {
  const [items, setItems] = useState([]);
  const [selectItemId, setSelectItemId] = useState(undefined);

  const loadItems = () => {
    fetchItems().then((res) => setItems(res));
  };
  //  请求列表
  useEffect(() => {
    loadItems();
  }, []);

  // 添加列表id,这是一种很糟糕的做法
  const newItems = items.map((item) => ({ ...item, id: crypto.randomUUID() }));

  return (
    <List
      items={newItems}
      selectedItemId={selectItemId}
      onSelectItem={setSelectItemId}
    />
  );
};

推荐写法: 当我们获取列表项的时候添加 id。

const App = () => {
  const [items, setItems] = useState([]);
  const [selectItemId, setSelectItemId] = useState(undefined);

  const loadItems = () => {
    // 获取列表数据并通过 id 保存
    fetchItems().then((res) =>
      // 一旦获得结果,我们就会添加“id”
      setItems(res.map((item) => ({ ...item, id: crypto.randomUUID() })))
    );
  };
  //  请求列表
  useEffect(() => {
    loadItems();
  }, []);

  return (
    <List
      items={items}
      selectedItemId={selectItemId}
      onSelectItem={setSelectItemId}
    />
  );
};

推荐写法: 当我们获取列表项的时候添加 id。

const App = () => {
  const [items, setItems] = useState([]);
  const [selectItemId, setSelectItemId] = useState(undefined);

  const loadItems = () => {
    // 获取列表数据并通过 id 保存
    fetchItems().then((res) =>
      // 一旦获得结果,我们就会添加“id”
      setItems(res.map((item) => ({ ...item, id: crypto.randomUUID() })))
    );
  };
  //  请求列表
  useEffect(() => {
    loadItems();
  }, []);

  return (
    <List
      items={items}
      selectedItemId={selectItemId}
      onSelectItem={setSelectItemId}
    />
  );
};

21. 策略性地使用 key 属性来触发组件重新渲染

想要强制组件从头开始重新渲染?只需更改其 key 属性即可。

在下面的示例中,我们使用此技巧在切换到新选项卡时重置错误边界。(该示例基于前面第 17 点所展示的示例改造)

Resource.tsx::

export interface ResourceProps {
  type: string;
}
const Resource: React.FC<ResourceProps> = ({ type }) => {
  const [count, setCount] = useState(0);
  const onHandleClick = () => {
    setCount((c) => c + 1);
  };

  // 新增抛出异常的代码
  useEffect(() => {
    if (type === "tab-1") {
      throw new Error("该选项不可切换");
    }
  }, []);

  return (
    <div className="tab-content">
      你当前在{type === "tab-1" ? "首页" : "详情页"},
      <button onClick={onHandleClick} className="btn" type="button">
        点击我
      </button>
      增加访问{count}次数
    </div>
  );
};

App.tsx:

import { ErrorBoundary } from "react-error-boundary";

const App = () => {
  const [currentTab, setCurrentTab] = useState("tab-1");

  return (
    <>
      <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} />
      <ErrorBoundary
        fallback={<div className="error">组件渲染发生了一些错误</div>}
        key={currentTab}
        // 如果没有key属性,当currentTab值为“tab-2”时也会呈现错误
      >
        <Resource type={currentTab} />
      </ErrorBoundary>
    </>
  );
};

完整示例代码可前往这里查看

22. 使用 ref 回调函数执行诸如监控大小变化和管理多个节点元素等任务。

你知道可以将函数传递给 ref 属性而不是 ref 对象吗?

它的工作原理如下:

  • 当 DOM 节点添加到屏幕时,React 会以 DOM 节点作为参数调用该函数。
  • 当 DOM 节点被移除时,React 会以 null 调用该函数。

在下面的示例中,我们使用此技巧跳过 useEffect。

不好的写法: 使用 useEffect 关注输入框焦点

const FocusInput = () => {
  const ref = useRef<HTMLInputElement>();

  useEffect(() => {
    ref.current?.focus();
  }, []);

  return <input ref={ref} type="text" />;
};

推荐写法: 我们在输入可用时立即聚焦输入。

const FocusInput = () => {
  const ref = useCallback((node) => node?.focus(), []);
  return <input ref={ref} type="text" />;
};

四. 组织 react 代码

23. 将 React 组件与其资源(例如样式、图像等)放在一起

始终将每个 React 组件与相关资源(如样式和图像)放在一起。

这样,当不再需要组件时,可以更轻松地删除它们。 它还简化了代码导航,因为你需要的一切都集中在一个地方。

24. 限制组件文件大小

包含大量组件和导出内容的大文件可能会令人困惑。

此外,随着更多内容的添加,它们往往会变得更大。

因此,请以合理的文件大小为目标,并在合理的情况下将组件拆分为单独的文件。

25. 限制功能组件文件中的返回语句数量

功能组件中的多个返回语句使得很难看到组件返回的内容。

对于我们可以搜索渲染术语的类组件来说,这不是问题。

一个方便的技巧是尽可能使用不带括号的箭头函数(VSCode 有一个针对此的操作)。

不好的写法: 更难发现组件返回语句。

export interface UserInfo {
  id: string;
  name: string;
  age: number;
}
export interface UserListProps {
  users: UserInfo[];
  searchUser: string;
  onSelectUser: (u: UserInfo) => void;
}
const UserList: React.FC<UserListProps> = ({
  users,
  searchUser,
  onSelectUser,
}) => {
  // 多余return语句
  const filterUsers = users?.filter((user) => {
    return user.name.includes(searchUser);
  });

  const onSelectUserHandler = (user) => {
    // 多余return语句
    return () => {
      onSelectUser(user);
    };
  };

  return (
    <>
      <h2>用户列表</h2>
      <ul>
        {filterUsers.map((user, index) => {
          return (
            <li key={`${user.id}-${index}`} onClick={onSelectUserHandler(user)}>
              <p>
                <span>用户id</span>
                <span>{user.id}</span>
              </p>
              <p>
                <span>用户名</span>
                <span>{user.name}</span>
              </p>
              <p>
                <span>用户年龄</span>
                <span>{user.age}</span>
              </p>
            </li>
          );
        })}
      </ul>
    </>
  );
};

推荐写法: 组件仅有一个返回语句。

export interface UserInfo {
  id: string;
  name: string;
  age: number;
}
export interface UserListProps {
  users: UserInfo[];
  searchUser: string;
  onSelectUser: (u: UserInfo) => void;
}
const UserList: React.FC<UserListProps> = ({
  users,
  searchUser,
  onSelectUser,
}) => {
  const filterUsers = users?.filter((user) => user.name.includes(searchUser));

  const onSelectUserHandler = (user) => () => onSelectUser(user);

  return (
    <>
      <h2>用户列表</h2>
      <ul>
        {filterUsers.map((user, index) => (
          <li key={`${user.id}-${index}`} onClick={onSelectUserHandler(user)}>
            <p>
              <span>用户id</span>
              <span>{user.id}</span>
            </p>
            <p>
              <span>用户名</span>
              <span>{user.name}</span>
            </p>
            <p>
              <span>用户年龄</span>
              <span>{user.age}</span>
            </p>
          </li>
        ))}
      </ul>
    </>
  );
};

26. 优先使用命名导出而不是默认导出

让我们比较一下这两种方法:

//默认导出
export default function App() {
  // 组件内容
}
// 命名导出
export function App() {
  // 组件内容
}

我们现在就像如下这样导入组件:

// 默认导入
import App from "/path/to/App";
// 命名导入
import { App } from "/path/to/App";

默认导出存在如下一些问题:

  • 如果组件被重命名,编辑器将不会自动重命名导出。

例如,如果将 App 重命名为 Index,我们将得到以下内容:

// 默认导入名字并未更改
import App from "/path/to/Index";
// 命名导入名字已更改
import { Index } from "/path/to/Index";
  • 很难看出从具有默认导出的文件中导出了什么。

例如,在命名导入的情况下,一旦我们输入 import { } from "/path/to/file",当我将光标放在括号内时就会获得自动完成功能。

  • 默认导出很难重新再导出。
    例如,如果我想从 index 文件重新导出 App 组件,我必须执行以下操作:
export { default as App } from "/path/to/App";

使用命名导出的解决方案更加直接。

export { App } from "/path/to/App";

因此,建议默认使用命名导出。

注意:即使你使用的是 React lazy,你仍然可以使用命名导出。请参阅此处的介绍示例。

五. 高效的状态管理

27. 永远不要为可以从其他 state 或 props 派生的值创建新的 state

state 越多 = 麻烦越多。

每个 state 都可能触发重新渲染,并使重置 state 变得麻烦。

因此,如果可以从 state 或 props 中派生出值,则跳过添加新的 state。

不好的做法:filteredUsers 不需要处于 state 中。

const FilterUserComponent = ({ users }) => {
  const [filters, setFilters] = useState([]);
  // 创建了新的state
  const [filteredUsers, setFilteredUsers] = useState([]);

  const filterUsersMethod = (filters, users) => {
    // 过滤逻辑方法
  };

  useEffect(() => {
    setFilteredUsers(filterUsersMethod(filters, users));
  }, [users, filters]);

  return (
    <Card>
      <Filters filters={filters} onChangeFilters={setFilters} />
      {filteredUsers.length > 0 && <UserList users={filteredUsers} />}
    </Card>
  );
};

推荐做法: filteredUsers 由 users 和 filters 决定。

const FilterUserComponent = ({ users }) => {
  const [filters, setFilters] = useState([]);

  const filterUsersMethod = (filters, users) => {
    // 过滤逻辑方法
  };

  const filteredUsers = filterUsersMethod(filters, users);

  return (
    <Card>
      <Filters filters={filters} onChangeFilters={setFilters} />
      {filteredUsers.length > 0 && <UserList users={filteredUsers} />}
    </Card>
  );
};

28. 将 state 创建在仅需要更新的组件内部,以减少组件的重新渲染

每当组件内部的状态发生变化时,React 都会重新渲染该组件及其所有子组件(包裹在 memo 中的子组件除外)。

即使这些子组件不使用已更改的状态,也会发生这种情况。为了最大限度地减少重新渲染,请尽可能将状态移到组件树的下方。

不好的做法: 当 type 发生改变时,会使不依赖 type 状态的 LeftList 和 RightList 组件也触发重新渲染。

const App = () => {
  const [type, setType] = useState("");

  return (
    <Container>
      <LeftList />
      <Main type={type} setType={setType} />
      <RightList />
    </Container>
  );
};

const mainBtnList = [
  {
    label: "首页",
    value: "home",
  },
  {
    label: "详情页",
    value: "detail",
  },
];
const Main = ({ type, setType }) => {
  return (
    <>
      {mainBtnList.map((item, index) => (
        <Button
          className={`${type.value === type ? "active" : ""}`}
          key={`${item.value}-${index}`}
          onClick={() => setType(item.value)}
        >
          {item.label}
        </Button>
      ))}
    </>
  );
};

推荐做法: 将状态耦合到 Main 组件内部,仅影响 Main 组件的重新渲染。

const App = () => {
  return (
    <Container>
      <LeftList />
      <Main />
      <RightList />
    </Container>
  );
};

const mainBtnList = [
  {
    label: "首页",
    value: "home",
  },
  {
    label: "详情页",
    value: "detail",
  },
];
const Main = () => {
  const [type, setType] = useState("");
  return (
    <>
      {mainBtnList.map((item, index) => (
        <Button
          className={`${type.value === type ? "active" : ""}`}
          key={`${item.value}-${index}`}
          onClick={() => setType(item.value)}
        >
          {item.label}
        </Button>
      ))}
    </>
  );
};

29. 定义需要明确初始状态和当前状态的区别

不好的做法: 不清楚 userInfo 只是初始值,这可能会导致状态管理的混乱或错误。

const UserInfo = ({ userInfo }) => {
  const [userInfo, setUserInfo] = useState(userInfo);

  return (
    <Card>
      <Title>当前用户: {userInfo?.name}</Title>
      <UserInfoDetail detail={userInfo?.detail} />
    </Card>
  );
};

推荐做法: 命名可以清楚地表明什么是初始状态,什么是当前状态。

const UserInfo = ({ initialUserInfo }) => {
  const [userInfo, setUserInfo] = useState(initialUserInfo);

  return (
    <Card>
      <Title>当前用户: {userInfo?.name}</Title>
      <UserInfoDetail detail={userInfo?.detail} />
    </Card>
  );
};

30. 根据之前的状态更新状态,尤其是在使用 useCallback 进行缓存时

React 允许你将更新函数从 useState 传递给 set 函数。

此更新函数使用当前状态来计算下一个状态。

每当需要根据之前状态更新状态时,都可以使用此行为,尤其是在使用 useCallback 包装的函数内部。事实上,这种方法可以避免将状态作为钩子依赖项之一。

不好的做法: 无论什么时候,当 todoList 变化的时候,onHandleAddTodoonHandleRemoveTodo 都会跟着改变。

const App = () => {
  const [todoList, setTodoList] = useState([]);

  const onHandleAddTodo = useCallback(
    (todo) => {
      setTodoList([...todoList, todo]);
    },
    [todoList]
  );

  const onHandleRemoveTodo = useCallback(
    (todo) => {
      setTodoList([...todoList].filter((item) => item.id !== todo.id));
    },
    [todoList]
  );
  return (
    <div className="App">
      <TodoInput onAddTodo={onHandleAddTodo} />
      <TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} />
    </div>
  );
};

推荐做法: 即使 todoList 发生变化,onHandleAddTodoonHandleRemoveTodo 仍然保持不变。

const App = () => {
  const [todoList, setTodoList] = useState([]);

  const onHandleAddTodo = useCallback((todo) => {
    setTodoList((prevTodoList) => [...prevTodoList, todo]);
  }, []);

  const onHandleRemoveTodo = useCallback((todo) => {
    setTodoList((prevTodoList) =>
      [...prevTodoList].filter((item) => item.id !== todo.id)
    );
  }, []);
  return (
    <div className="App">
      <TodoInput onAddTodo={onHandleAddTodo} />
      <TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} />
    </div>
  );
};

31. 使用 useState 中的函数进行延迟初始化并提高性能,因为它们只被调用一次。

在 useState 中使用函数可确保初始状态仅计算一次。

这可以提高性能,尤其是当初始状态来自“昂贵”操作(例如从本地存储读取)时。

不好的做法:每次组件渲染时,我们都会从本地存储读取主题。

const THEME_LOCAL_STORAGE_KEY = "page_theme_key";

const Theme = ({ theme, onChangeTheme }) => {
  // ....
};

const App = ({ children }) => {
  const [theme, setTheme] = useState(
    localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
  );

  const onChangeTheme = (theme: string) => {
    setTheme(theme);
    localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
  };

  return (
    <div className={`app${theme ? ` ${theme}` : ""}`}>
      <Theme onChange={onChangeTheme} theme={theme} />
      <div>{children}</div>
    </div>
  );
};

推荐做法: 当组件挂载时,我们仅只会读取本地存储一次。

// ...

const App = ({ children }) => {
  const [theme, setTheme] = useState(
    () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
  );

  const onChangeTheme = (theme: string) => {
    setTheme(theme);
    localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
  };

  return (
    <div className={`app${theme ? ` ${theme}` : ""}`}>
      <Theme onChange={onChangeTheme} theme={theme} />
      <div>{children}</div>
    </div>
  );
};

32. 使用 React 上下文来处理广泛需要的静态状态,以防止 prop 钻取

每当我有一些数据时,我都会使用 React 上下文:

  • 在多个地方都需要(例如,主题、当前用户等)
  • 主要是静态或只读的(即,用户不能/不会经常更改数据)
  • 这种方法有助于避免 prop 钻取(即,通过组件层次结构的多个层传递数据或状态)。

来看一个示例的部分代码:

context.ts:

// UserInfo接口来自测试数据

export const userInfoContext = createContext<string | UserInfoData>("loading");

export const useUserInfo = <T extends UserInfoData>() => {
  const value = useContext(userInfoContext);
  if (value == null) {
    throw new Error("Make sure to wrap the userInfoContext inside provider");
  }
  return value as T;
};

App.tsx:

function App() {
  const [userInfoData, setUserInfoData] = useState<UserInfoData | string>(
    "loading"
  );

  useEffect(() => {
    getCurrentUser().then(setUserInfoData);
  }, []);

  if (userInfoData === "loading") {
    return <Loading />;
  }

  return (
    <div className="app">
      <userInfoContext.Provider value={userInfoData}>
        <Header />
        <Sidebar />
        <Main />
      </userInfoContext.Provider>
    </div>
  );
}

header.tsx:

const Header: React.FC<HeaderProps> = (props) => {
  // 使用context
  const userInfo = useUserInfo();
  return (
    <header className="header" {...props}>
      欢迎回来{userInfo?.name}
    </header>
  );
};

main.tsx:

const Main: React.FC<MainProps> = ({ title }) => {
  const { posts } = useUserInfo();
  return (
    <div className="main">
      <h2 className="title">{title}</h2>
      <ul className="list">
        {posts?.map((post, index) => (
          <li className="list-item" key={`${post.id}-${index}`}>
            {post.title}
          </li>
        ))}
      </ul>
    </div>
  );
};

完整示例代码前往这里查看

33. React Context:将 react 上下文分为经常变化的部分和不经常变化的部分,以提高应用程序性能

React 上下文的一个挑战是,只要上下文数据发生变化,所有使用该上下文的组件都会重新渲染,即使它们不使用发生变化的上下文部分。

解决方案是什么?使用单独的上下文。

在下面的示例中,我们创建了两个上下文:一个用于操作(常量),另一个用于状态(可以更改)。

export interface TodosInfoItem {
  id?: string;
  title?: string;
  completed?: boolean;
}
export interface TodosInfo {
  search?: string;
  todos: TodosInfoItem[];
}

export const todosStateContext = createContext<TodosInfo>(void 0);
export const todosActionContext = createContext<Dispatch<ReducerActionParams>>(
  void 0
);

export interface ReducerActionParams extends TodosInfoItem {
  type?: string;
  value?: string;
}
export const getTodosReducer = (
  state: TodosInfo,
  action: ReducerActionParams
) => {
  switch (action.type) {
    case TodosActionType.ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: crypto.randomUUID(),
            title: action.title,
            completed: false,
          },
        ],
      };
    case TodosActionType.REMOVE_TODO:
      return {
        ...state,
        todos: [...state.todos].filter((item) => item.id !== action.id),
      };
    case TodosActionType.TOGGLE_TODO_STATUS:
      return {
        ...state,
        todos: [...state.todos].map((item) =>
          item.id === action.id ? { ...item, completed: !item.completed } : item
        ),
      };
    case TodosActionType.SET_SEARCH_TERM:
      return {
        ...state,
        search: action.value,
      };
    default:
      return state;
  }
};

完整示例代码前往这里查看

34. React Context:当值计算不直接时,引入 Provider 组件

不好的做法:App 内部有太多逻辑来管理 theme context。

const THEME_LOCAL_STORAGE_KEY = "current-project-theme";
const DEFAULT_THEME = "light";
const ThemeContext = createContext({
  theme: DEFAULT_THEME,
  setTheme: () => null,
});

const App = () => {
  const [theme, setTheme] = useState(
    () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
  );

  useEffect(() => {
    if (theme !== "system") {
      updateRootElementTheme(theme);
      return;
    }
    // 我们需要根据系统主题获取要应用的主题类
    const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
      .matches
      ? "dark"
      : "light";

    updateRootElementTheme(systemTheme);
    // 然后观察系统主题的变化并相应地更新根元素
    const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
    const listener = (event) => {
      updateRootElementTheme(event.matches ? "dark" : "light");
    };
    darkThemeMq.addEventListener("change", listener);
    return () => darkThemeMq.removeEventListener("change", listener);
  }, [theme]);

  const themeContextValue = {
    theme,
    setTheme: (theme) => {
      localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
      setTheme(theme);
    },
  };

  const [selectedUserId, setSelectedUserId] = useState(undefined);
  const onUserSelect = (id) => {
    // 待做:一些逻辑
    setSelectedUserId(id);
  };

  const users = useSWR("/api/users", fetcher);

  return (
    <div className="App">
      <ThemeContext.Provider value={themeContextValue}>
        <UserList
          users={users}
          onUserSelect={onUserSelect}
          selectedUserId={selectedUserId}
        />
      </ThemeContext.Provider>
    </div>
  );
};

推荐:主题 context 相关的逻辑封装在 ThemeProvider 中。

const THEME_LOCAL_STORAGE_KEY = "current-project-theme";
const DEFAULT_THEME = "light";
const ThemeContext = createContext({
  theme: DEFAULT_THEME,
  setTheme: () => null,
});

const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState(
    () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
  );

  useEffect(() => {
    if (theme !== "system") {
      updateRootElementTheme(theme);
      return;
    }
    // 我们需要根据系统主题获取要应用的主题类
    const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
      .matches
      ? "dark"
      : "light";

    updateRootElementTheme(systemTheme);
    // 然后观察系统主题的变化并相应地更新根元素
    const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
    const listener = (event) => {
      updateRootElementTheme(event.matches ? "dark" : "light");
    };
    darkThemeMq.addEventListener("change", listener);
    return () => darkThemeMq.removeEventListener("change", listener);
  }, [theme]);

  const themeContextValue = {
    theme,
    setTheme: (theme) => {
      localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
      setTheme(theme);
    },
  };

  return (
    <div className="App">
      <ThemeContext.Provider value={themeContextValue}>
        {children}
      </ThemeContext.Provider>
    </div>
  );
};

const App = () => {
  const [selectedUserId, setSelectedUserId] = useState(undefined);
  const onUserSelect = (id) => {
    // 待做:一些逻辑
    setSelectedUserId(id);
  };

  const users = useSWR("/api/users", fetcher);

  return (
    <div className="App">
      <ThemeProvider>
        <UserList
          users={users}
          onUserSelect={onUserSelect}
          selectedUserId={selectedUserId}
        />
      </ThemeProvider>
    </div>
  );
};

35. 考虑使用 useReducer hook 作为轻量级状态管理解决方案

每当我的状态或复杂状态中的值太多并且不想依赖外部库时,我都会使用 useReducer

当与上下文结合使用时,它对于更广泛的状态管理需求特别有效。

示例:这里

36. 使用 useImmer 或 useImmerReducer 简化状态更新

使用 useStateuseReducer 等钩子时,状态必须是不可变的(即,所有更改都需要创建新状态,而不是修改当前状态)。

这通常很难实现。

这就是 useImmeruseImmerReducer 提供更简单的替代方案的地方。它们允许你编写自动转换为不可变更新的“可变”代码。

不好的做法: 我们必须小心确保我们正在创建一个新的状态对象。

export const App = () => {
  const [{ email, password }, setState] = useState({
    email: "",
    password: "",
  });
  const onEmailChange = (event) => {
    setState((prevState) => ({ ...prevState, email: event.target.value }));
  };
  const onPasswordChange = (event) => {
    setState((prevState) => ({ ...prevState, password: event.target.value }));
  };

  return (
    <div className="App">
      <h1>欢迎登陆</h1>
      <div class="form-item">
        <label>邮箱号: </label>
        <input type="email" value={email} onChange={onEmailChange} />
      </div>
      <div className="form-item">
        <label>密码:</label>
        <input type="password" value={password} onChange={onPasswordChange} />
      </div>
    </div>
  );
};

推荐做法: 更直接一点,我们可以直接修改 draftState

import { useImmer } from "use-immer";

export const App = () => {
  const [{ email, password }, setState] = useImmer({
    email: "",
    password: "",
  });
  const onEmailChange = (event) => {
    setState((draftState) => {
      draftState.email = event.target.value;
    });
  };
  const onPasswordChange = (event) => {
    setState((draftState) => {
      draftState.password = event.target.value;
    });
  };

  // 剩余代码
};

37. 使用 Redux(或其他状态管理解决方案)来跨多个组件访问复杂的客户端状态

每当出现以下情况时,我都会求助于 Redux

我有一个复杂的 FE 应用程序,其中包含大量共享的客户端状态(例如,仪表板应用程序)

  • 我希望用户能够回到过去并恢复更改。
  • 我不希望我的组件像使用 React 上下文那样不必要地重新渲染。
  • 我有太多开始难以控制的上下文。

为了获得简化的体验,我建议使用 redux-tooltkit

💡 注意:你还可以考虑 Redux 的其他替代方案,例如 Zustand 或 Recoil。

38. Redux:使用 Redux DevTools 调试你的状态

Redux DevTools 浏览器扩展是调试 Redux 项目的有用工具。

它允许你实时可视化你的状态和操作,在刷新时保持状态持久性等等。

要了解它的用途,请观看这个精彩的视频

六. React 代码优化

39. 使用 memo 防止不必要的重新渲染

当处理渲染成本高昂且父组件频繁更新的组件时,将它们包装在 memo 中可能会改变渲染规则。

memo 确保组件仅在其 props 发生变化时重新渲染,而不仅仅是因为其父组件重新渲染。

在以下示例中,我通过 useGetInfoData 从服务器获取一些数据。如果数据没有变化,将 UserInfoList 包装在 memo 中将阻止它在数据的其他部分更新时重新渲染。

export const App = () => {
  const { currentUserInfo, users } = useGetInfoData();
  return (
    <div className="App">
      <h1>信息面板</h1>
      <CurrentUserInfo data={currentUserInfo} />
      <UserInfoList users={users} />
    </div>
  );
};

const UserInfoList = memo(({ users }) => {
  // 剩余实现
});

一旦 React 编译器变得稳定,这个小技巧可能就不再有用了。

40. 用 memo 指定一个相等函数来指示 React 如何比较 props。

默认情况下,memo 使用Object.is将每个 prop 与其先前的值进行比较。

但是,对于更复杂或特定的场景,指定自定义相等函数可能比默认比较或重新渲染更有效。

示例如下:

const UserList = memo(
  ({ users }) => {
    return <div>{JSON.stringify(users)}</div>;
  },
  (prevProps, nextProps) => {
    // 仅当最后一个用户或列表大小发生变化时才重新渲染
    const prevLastUser = prevProps.users[prevProps.users.length - 1];
    const nextLastUser = nextProps.users[nextProps.users.length - 1];
    return (
      prevLastUser.id === nextLastUser.id &&
      prevProps.users.length === nextProps.users.length
    );
  }
);

41.声明缓存组件时,优先使用命名函数而不是箭头函数

定义缓存组件时,使用命名函数而不是箭头函数可以提高 React DevTools 中的清晰度。

箭头函数通常会导致像 _c2 这样的通用名称,这会使调试和分析更加困难。

不好的做法:对缓存组件使用箭头函数会导致 React DevTools 中的名称信息量较少。

const UserInfoList = memo(({ users }) => {
  // 剩余实现逻辑
});

推荐做法: 该组件的名称将在 DevTools 中可见。

42. 使用 useMemo 缓存昂贵的计算或保留引用

我通常会使用 useMemo:

  • 当我有昂贵的计算,不应该在每次渲染时重复这些计算时。
  • 如果计算值是非原始值,用作 useEffect 等钩子中的依赖项。
  • 计算出的非原始值将作为 prop 传递给包裹在 memo 中的组件;否则,这将破坏缓存,因为 React 使用 Object.is 来检测 props 是否发生变化。

不好的做法:UserInfoList 的 memo 不会阻止重新渲染,因为每次渲染时都会重新创建样式。

export const UserInfo = () => {
  const { profileInfo, users, baseStyles } = useGetUserInfoData();
  // 每次重新渲染我们都会得到一个样式对象
  const styles = { ...baseStyles, margin: 10 };
  return (
    <div className="App">
      <h1>用户页</h1>
      <Profile data={profileInfo} />
      <UserInfoList users={users} styles={styles} />
    </div>
  );
};

const UserInfoList = memo(function UserInfoListFn({ users, styles }) {
  /// 剩余实现
});

推荐做法: useMemo 的使用确保只有当 baseStyles 发生变化时,styles 才会发生变化,从而使 memo 能够有效防止不必要的重新渲染。

export const UserInfo = () => {
  const { profileInfo, users, baseStyles } = useGetUserInfoData();
  // 每次重新渲染我们都会得到一个样式对象
  const styles = useMemo(() => ({ ...baseStyles, margin: 10 }), [baseStyles]);
  return (
    <div className="App">
      <h1>用户页</h1>
      <Profile data={profileInfo} />
      <UserInfoList users={users} styles={styles} />
    </div>
  );
};

const UserInfoList = memo(function UserInfoListFn({ users, styles }) {
  /// 剩余实现
});

43. 使用 useCallback 缓存函数

useCallback 与 useMemo 类似,但专为缓存函数而设计。

不好的做法:每当 theme 发生变化时,handleThemeChange 都会被调用两次,并且我们会将日志推送到服务器两次。

const useTheme = () => {
  const [theme, setTheme] = useState("light");

  // 每次渲染`handleThemeChange`都会改变
  // 因此,每次渲染后都会触发该效果
  const handleThemeChange = (newTheme) => {
    sendLog(["Theme changed"], {
      context: {
        theme: newTheme,
      },
    });
    setTheme(newTheme);
  };

  useEffect(() => {
    const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    handleThemeChange(dqMediaQuery.matches ? "dark" : "light");
    const listener = (event) => {
      handleThemeChange(event.matches ? "dark" : "light");
    };
    dqMediaQuery.addEventListener("change", listener);
    return () => {
      dqMediaQuery.removeEventListener("change", listener);
    };
  }, [handleThemeChange]);

  return theme;
};

推荐做法:将 handleThemeChange 包装在 useCallback 中可确保仅在必要时重新创建它,从而减少不必要的执行。

const handleThemeChange = useCallback((newTheme) => {
  sendLog(["Theme changed"], {
    context: {
      theme: newTheme,
    },
  });
  setTheme(newTheme);
}, []);

44. 缓存回调函数或使用程序钩子返回的值以避免性能问题

当你创建自定义钩子与他人共享时,记住返回的值和函数至关重要。

这种做法可以使你的钩子更高效,并防止任何使用它的人出现不必要的性能问题。

不好的做法:loadData 没有被缓存并产生了性能问题。

const useLoadData = (fetchData) => {
  const [result, setResult] = useState({
    type: "pending",
  });

  const loadData = async () => {
    setResult({ type: "loading" });
    try {
      const data = await fetchData();
      setResult({ type: "loaded", data });
    } catch (err) {
      setResult({ type: "error", error: err });
    }
  };

  return { result, loadData };
};

推荐做法: 我们缓存所有内容,因此不会出现意外的性能问题。

const useLoadData = (fetchData) => {
  const [result, setResult] = useState({
    type: "pending",
  });

  // 包裹在 `useRef` 中并使用 `ref` 值,这样函数就不会改变
  const fetchDataRef = useRef(fetchData);

  useEffect(() => {
    fetchDataRef.current = fetchData;
  }, [fetchData]);

  // 包裹在 `useCallback` 中并使用 `ref` 值,这样函数就不会改变
  const loadData = useCallback(async () => {
    setResult({ type: "loading" });
    try {
      const data = await fetchDataRef.current();
      setResult({ type: "loaded", data });
    } catch (err) {
      setResult({ type: "error", error: err });
    }
  }, []);

  // 使用useMemo缓存值
  return useMemo(() => ({ result, loadData }), [result, loadData]);
};

45. 利用懒加载和 Suspense 让你的应用加载更快

构建应用时,请考虑对以下代码使用懒加载和 Suspense:

  • 加载成本高。
  • 仅与某些用户相关(如高级功能)。
  • 对于初始用户交互而言并非立即需要。

在下面的示例,Slider 资源(JS + CSS)仅在你单击卡片后加载。

//...
const LazyLoadedSlider = lazy(() => import("./Slider"));
//...
const App = () => {
  // ....
  return (
    <div className="container">
      {/* .... */}
      {selectedUser != null && (
        <Suspense fallback={<div>Loading...</div>}>
          <LazyLoadedSlider
            avatar={selectedUser.avatar}
            name={selectedUser.name}
            address={selectedUser.address}
            onClose={closeSlider}
          />
        </Suspense>
      )}
    </div>
  );
};

46. 限制网络以模拟慢速网络

你知道可以直接在 Chrome 中模拟慢速互联网连接吗?

这在以下情况下尤其有用:

  • 用户报告加载时间缓慢,而你无法在更快的网络上复制。
  • 你正在实施懒加载,并希望观察文件在较慢条件下的加载方式,以确保适当的加载状态。

47. 使用 react-window 或 react-virtuoso 高效渲染列表

切勿一次性渲染一长串项目,例如聊天消息、日志或无限列表。

这样做可能会导致浏览器卡死崩溃。相反,可以使用虚拟化列表,这意味着仅渲染可能对用户可见的项目子集。

react-window、react-virtuoso 或 @tanstack/react-virtual 等库就是为此目的而设计的。

不好的做法:NonVirtualList 会同时呈现所有 50,000 条日志行,即使它们不可见。

const NonVirtualList = ({ items }: { items: LogLineItem[] }) => {
  return (
    <div style={{ height: "100%" }}>
      {items?.map((log, index) => (
        <div
          key={log.id}
          style={{
            padding: "5px",
            borderBottom:
              index === items.length - 1 ? "none" : "1px solid #535455",
          }}
        >
          <LogLine log={log} index={index} />
        </div>
      ))}
    </div>
  );
};

推荐做法: VirtualList 仅渲染可能可见的项目。

const VirtualList = ({ items }: { items: LogLineItem[] }) => {
  return (
    <Virtuoso
      style={{ height: "100%" }}
      data={items}
      itemContent={(index, log) => (
        <div
          key={log.id}
          style={{
            padding: "5px",
            borderBottom:
              index === items.length - 1 ? "none" : "1px solid #535455",
          }}
        >
          <LogLine log={log} index={index} />
        </div>
      )}
    />
  );
};

你可以在这个完整的示例中在两个选项之间切换,并注意使用 NonVirtualList 时应用程序的性能有多糟糕。

七. 调试 react 代码

48. 在将组件部署到生产环境之前,使用 StrictMode 捕获组件中的错误

使用严格模式是一种在开发过程中检测应用程序中潜在问题的主动方法。它有助于识别以下问题:

  • 效果中的清理不完整,例如忘记释放资源。
  • React 组件中的杂质,确保它们在给定相同输入(props、state 和 context)的情况下返回一致的 JSX。

下面的示例显示了一个错误,因为从未调用过 clearInterval。 严格模式通过运行两次效果(创建两个间隔)来帮助捕获此错误。

export default function App() {
  const [time, setTime] = useState<Date>(new Date());
  const handleTimeChange = useCallback((newTime: Date) => {
    // 这将被记录两次,因为 `useEffect`
    // 使用 `StrictMode` 运行两次,并且我们从未清除定时器
    console.log("这是当前时间", newTime);
    setTime(newTime);
  }, []);

  useEffect(() => {
    const intervalId = setInterval(() => {
      handleTimeChange(new Date());
    }, 1_000);
    // 取消注释下面这行代码来修复错误
    // return () => clearInterval(intervalId);
  }, [handleTimeChange]);

  return (
    <div className="App">
      <h1>当前时间: {time.toLocaleTimeString()}</h1>
    </div>
  );
}

49. 安装 React Developer Tools 浏览器扩展来查看/编辑你的组件并检测性能问题

React Developer Tools 是一款必备扩展程序(Chrome、Firefox)。此扩展程序可让你:

  • 可视化并深入研究 React 组件的细节,检查从 props 到状态的所有内容。
  • 直接修改组件的状态或 props,以查看更改如何影响行为和渲染。
  • 分析你的应用程序以确定组件重新渲染的时间和原因,帮助你发现性能问题。
  • 等等。

指南1指南2中了解如何使用它。

50. React DevTools 组件:突出显示渲染的组件以识别潜在问题

如果你的应用存在性能问题时,都可以使用这个技巧。你可以突出显示渲染的组件(高亮显示)以检测潜在问题(例如,渲染次数过多)。

只要时间发生变化,App 组件就会重新渲染,这是错误的。 西瓜视频中搜索。

51. 在自定义 hooks 中利用 useDebugValue 可以在 React DevTools 中获得更好的可视性

useDebugValue 可以成为一种便捷的工具,用于在 React DevTools 中为自定义钩子添加描述性标签。这使得直接从 DevTools 界面监视它们的状态变得更加容易。

例如,考虑一下我用来获取和显示当前时间的这个自定义钩子,每秒更新一次:

const useCurrentTime = () => {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => clearInterval(intervalId);
  }, [setTime]);

  return time;
};

不好的做法:如果没有 useDebugValue,实际时间值不会立即可见;你需要扩展 CurrentTime 钩子:

推荐做法:使用 useDebugValue 可以很容易地看到当前时间:

const useCurrentTime = () => {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => clearInterval(intervalId);
  }, [setTime]);

  // 新增代码
  useDebugValue(time);

  return time;
};

注意:请谨慎使用 useDebugValue。它最好用于共享库中的复杂钩子,因为了解内部状态至关重要。

52. 使用 why-did-you-render 等相关库来跟踪组件渲染并识别潜在的性能瓶颈

有时,组件会重新渲染,但无法立即查明原因。虽然 React DevTools 很有用,但在大型应用中,它可能只会提供模糊的解释,例如“hook #1 已渲染”,这可能是无用的。

在这种情况下,你可以求助于 why-did-you-render 库。它提供了有关组件重新渲染原因的更详细见解,有助于更有效地查明性能问题。来看以下一个示例,多亏了这个库,我们可以找到 FollowersList 组件的问题。

53. 在严格模式下第二次渲染时隐藏日志

StrictMode 有助于在应用程序开发早期发现错误。

但是,由于它会导致组件渲染两次,因此可能会导致重复的日志,从而使控制台变得混乱。

你可以在 Strict Mode 的第二次渲染期间隐藏日志以解决此问题,勾选hide logs....选项即可。如下图所示:

八. 测试 react 代码

54. 使用 React 测试库有效地测试你的 React 组件

想要测试你的 React 应用吗?请务必使用 @testing-library/react。

你可以在此处找到一个最基本的示例

55. React 测试库:使用测试演练场轻松创建测试用例

难以决定在测试中使用哪些测试用例?

考虑使用测试演练场从组件的 HTML 快速生成测试用例。

以下是两种使用方法:

方法 1:在测试中使用 screen.logTestingPlaygroundURL()。此函数生成一个 URL,打开测试环境工具,其中已加载组件的 HTML

方法 2:安装 Testing Playground Chrome 扩展程序。此扩展程序允许你直接在浏览器中将鼠标悬停在应用中的元素上,以找到测试它们的最佳查询。

56. 使用 Cypress 或 Playwright 进行端到端测试

需要进行端到端测试吗?

请务必查看 CypressPlaywright

57. 使用 MSW 在测试中模拟网络请求

有时,你的测试需要发出网络请求。

与其实现自己的模拟(或者,但愿不会发出实际的网络请求),不如考虑使用 MSW(Mock Service Worker)来处理你的 API 响应。

MSW 允许你直接在测试中拦截和操纵网络交互,为模拟服务器响应提供了一种强大而直接的解决方案,而不会影响实时服务器。

这种方法有助于维护受控且可预测的测试环境,从而提高测试的可靠性。

九. React hooks(钩子函数)

58. 确保在 useEffect 钩子中执行所有必要的清理

如果你设置了任何需要稍后清理的内容,请始终在 useEffect 钩子中返回清理函数,这可能是任何内容,忽略此步骤可能会导致资源使用率低下和潜在的内存泄漏。

不好的做法:此示例设置了一个间隔。但我们从未清除它,这意味着即使组件卸载后它仍会继续运行。

const Timer = () => {
  const [date, setDate] = useState(new Date());
  useEffec(() => {
    setInterval(() => {
      setDate(new Date());
    }, 1000);
  }, []);
  return <>当前时间:{date.toLocaleTimeString()}</>;
};

推荐做法: 当组件卸载时,间隔会被正确清除。

const Timer = () => {
  const [date, setDate] = useState(new Date());
  useEffec(() => {
    const interval = setInterval(() => {
      setDate(new Date());
    }, 1000);
    // 当组件卸载时,我们清除了定时器
    return () => clearInterval(interval);
  }, []);
  return <>当前时间:{date.toLocaleTimeString()}</>;
};

59. 使用 ref 访问 DOM 元素

在 React 中,你永远不应该直接操作 DOM。

尽管 React 可以直接访问/操作 DOM,不过还是不推荐使用 document.getElementByIddocument.getElementsByClassName 等方法。

那么,当你需要访问 DOM 元素时应该怎么做?

你可以使用 useRef 钩子函数,如下面的示例中所示,我们需要访问 canvas 元素。

import { useEffect, useRef } from "react";
import Chart from "chart.js/auto";

export interface ChartComponentProps<T> {
  data?: T[];
}
const ChartComponent = <T extends { year?: number; count?: number }>({
  data,
}: ChartComponentProps<T>) => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  useEffect(() => {
    const canvasElement = canvasRef.current;
    if (canvasElement == null) {
      return;
    }
    const chart = new Chart(canvasElement, {
      type: "bar",
      data: {
        labels: data?.map((row) => row.year),
        datasets: [
          {
            label: "一年的点赞数",
            data: data?.map((row) => row.count),
          },
        ],
      },
    });
    return () => chart.destroy();
  }, []);
  return <canvas ref={canvasRef} />;
};

export default ChartComponent;

注意:我们可以向 canvas 元素添加一个 ID 并使用 document.getElementById 获取,但不建议这样做。

60. 使用 ref 在重新渲染时保存值

如果你的 React 组件中有未存储在状态中的可变值,你会注意到对这些值的更改不会在重新渲染后持续存在。

除非你全局保存它们,否则会发生这种情况。

你可能会考虑将这些值放入状态中。但是,如果它们与渲染无关,这样做可能会导致不必要的重新渲染,从而浪费性能。

这也是 useRef 大放异彩的地方。

在下面的例子中,我想在用户点击某个按钮时停止计时器。为此,我需要将 interval 存储在某处。

不好的做法:下面的示例无法按预期工作,因为每次重新渲染组件时都会重置 interval。

const Timer = () => {
  const [date, setDate] = useState(new Date());
  let interval: ReturnType<typeof setInterval>;
  useEffec(() => {
    interval = setInterval(() => {
      setDate(new Date());
    }, 1000);
    // 当组件卸载时,我们清除了定时器
    return () => clearInterval(interval);
  }, []);
  const stopInterval = () => interval && clearInterval(interval);
  return (
    <>
      <p>当前时间:{date.toLocaleTimeString()}</p>
      <button onClick={stopInterval} type="button">
        停止定时器
      </button>
    </>
  );
};

推荐做法: 通过使用 useRef,我们确保渲染之间的间隔 ID 得以保留。

const Timer = () => {
  const [date, setDate] = useState(new Date());
  let interval = useRef<ReturnType<typeof setInterval>>();
  useEffec(() => {
    interval.current = setInterval(() => {
      setDate(new Date());
    }, 1000);
    // 当组件卸载时,我们清除了定时器
    return () => clearInterval(interval.current);
  }, []);
  const stopInterval = () =>
    interval.current && clearInterval(interval.current);
  return (
    <>
      <p>当前时间:{date.toLocaleTimeString()}</p>
      <button onClick={stopInterval} type="button">
        停止定时器
      </button>
    </>
  );
};

61. 在 hooks 中使用命名函数而不是箭头函数(例如 useEffect),以便在 React Dev Tools 中轻松找到它们

如果你有许多钩子函数,在 React DevTools 中找到它们可能会很困难。

一个技巧是使用命名函数,这样你就可以快速发现它们。

不好的做法: 在众多的钩子函数中很难找到具体的效果。

const HelloWorld = () => {
  useEffect(() => {
    console.log("我已经挂载了!");
  }, []);

  return <>Hello World</>;
};

推荐做法: 你可以很快发现其效果。

const HelloWorld = () => {
  useEffect(function HelloWorldFn() {
    console.log("我已经挂载了!");
  }, []);

  return <>Hello World</>;
};

推荐做法: 你可以很快发现其效果。

const HelloWorld = () => {
  useEffect(function HelloWorldFn() {
    console.log("我已经挂载了!");
  }, []);

  return <>Hello World</>;
};

62. 使用自定义钩子函数封装逻辑

假设我有一个组件,它从用户的暗模式偏好设置中获取主题并在应用程序内使用它。

最好将返回主题的逻辑提取到自定义钩子中(以重复使用它并保持组件清洁)。

不好的做法: App 组件过于繁琐。

const App = () => {
  const [theme, setTheme] = useState("light");

  useEffect(() => {
    const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    setTheme(dqMediaQuery.matches ? "dark" : "light");
    const listener = (event) => {
      setTheme(event.matches ? "dark" : "light");
    };
    dqMediaQuery.addEventListener("change", listener);
    return () => {
      dqMediaQuery.removeEventListener("change", listener);
    };
  }, []);

  return (
    <div className={`App ${theme === "dark" ? "dark" : ""}`}>Hello Word</div>
  );
};

推荐做法: App 组件简单多了,我们可以重用逻辑。

// 自定义钩子函数可以被重复使用
const useTheme = () => {
  const [theme, setTheme] = useState("light");

  useEffect(() => {
    const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    setTheme(dqMediaQuery.matches ? "dark" : "light");
    const listener = (event) => {
      setTheme(event.matches ? "dark" : "light");
    };
    dqMediaQuery.addEventListener("change", listener);
    return () => {
      dqMediaQuery.removeEventListener("change", listener);
    };
  }, []);

  return theme;
};
const App = () => {
  const theme = useTheme();
  return (
    <div className={`App ${theme === "dark" ? "dark" : ""}`}>Hello Word</div>
  );
};

63. 优先使用函数而不是自定义钩子函数

当可以使用函数时,切勿将逻辑放在钩子函数中。

效果:

  • 钩子函数只能在其他钩子函数或组件内使用,而函数可以在任何地方使用。
  • 函数比钩子函数更简单。
  • 函数更容易测试。
  • 等等。

不好的做法: useLocale 钩子是不必要的,因为它不需要是一个钩子。它不使用其他钩子,如 useEffectuseState 等。

const useLocale = () => {
  return window.navigator.languages?.[0] ?? window.navigator.language;
};
const App = () => {
  const locale = useLocale();
  return (
    <div className="App">
      <ConfigProvider locale={locale}>
        <Main />
      </ConfigProvider>
    </div>
  );
};

推荐做法: 创建一个函数 getLocale

const getLocale = () =>
  window.navigator.languages?.[0] ?? window.navigator.language;
const App = () => {
  const locale = getLocale();
  return (
    <div className="App">
      <ConfigProvider locale={locale}>
        <Main />
      </ConfigProvider>
    </div>
  );
};

64. 使用 useLayoutEffect 钩子防止视觉 UI 故障

当效果不是由用户交互引起时,用户将在效果运行之前看到 UI(通常很短暂)。

因此,如果效果修改了 UI,用户将在看到更新后的 UI 版本之前很快看到初始 UI 版本,从而产生视觉故障。

使用 useLayoutEffect 可确保效果在所有 DOM 突变后同步运行,从而防止初始渲染故障。

在下面的示例中,我们希望宽度在列之间均匀分布(我知道这可以在 CSS 中完成,但我需要一个例子)。

使用 useEffect,你可以在开始时短暂地看到表格正在发生变化。列以其默认大小呈现,然后调整为正确大小。

const BlogPostsTable = ({ posts }: { posts: BlogPosts }) => {
  const tableRef = useRef<HTMLTableElement | null>(null);
  const [columnWidth, setColumnWidth] = useState<number>();

  // 使用 `useLayoutEffect` 来查看表格如何以正确的尺寸呈现
  useEffect(() => {
    // 屏幕故障太快,可能不可见
    // 所以我只是挡住屏幕让故障可见
    blockScreenSync();
    const tableElement = tableRef.current;
    if (tableElement != null) {
      // 在列之间平均分配宽度
      // 这可以用 CSS 来实现,所以我们在这里这样做是为了说明目的
      setColumnWidth(tableElement.offsetWidth / 4);
    }
  }, []);

  return (
    <table ref={tableRef}>
      <thead>
        <tr>
          {tableColumn.map((item, index) => (
            <th key={`${item}-${index}`}>{item.title}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {posts.map((post) => (
          <tr key={post.href}>
            {tableColumn.map((col, index) => (
              <td key={`${col}-${index}`} style={{ width: columnWidth }}>
                {col.render ? (
                  col.render(post[col.dataIndex as keyof BlogPostsItem] as any)
                ) : (
                  <>{post[col.dataIndex as keyof BlogPostsItem]}</>
                )}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};

如果你正在寻找其他出色的用途,请查看这篇文章

65. 使用 useId 钩子为可访问性属性生成唯一 ID

厌倦了想出 ID 或让它们发生冲突?

你可以使用 useId 钩子函数在 React 组件内生成唯一 ID,并确保你的应用可访问。

const TestForm = () => {
  const id = useId();
  return (
    <div className="App">
      <div className="form-item">
        <label>用户名:</label>
        <input type="text" aria-describedby={id} placeholder="请输入用户名" />
      </div>
      <span id={id}>确保用户名是不重复的</span>
    </div>
  );
};

66. 使用 useSyncExternalStore 订阅外部存储

这是一个很少需要但功能非常强大的钩子。

如果出现以下情况,请使用此钩子:

  • 你有一些在 React 树中无法访问的状态(即,在状态或上下文中不存在)。
  • 状态可以更改,并且你需要通知你的组件更改。

在下面的示例中,我想要一个 Logger 单例来记录整个应用程序中的错误、警告、信息等。

这些是需求:

  • 我需要能够在我的 React 应用程序中的任何地方调用它(即使在非 React 组件内),所以我不会将它放在状态/上下文中。
  • 我想在 Logs 组件内向用户显示所有日志。

我可以在 Logs 组件内使用 useSyncExternalStore 来访问日志并监听更改。

const createLogger = <
  T extends { level: string; message: string },
  U extends () => void
>() => {
  let logDataList: T[] = [],
    logListeners: U[] = [];
  const pushLog = (log: T) => {
    logDataList = [...logDataList, log];
    logListeners.forEach((listener) => listener());
  };

  return {
    logs: () => Object.freeze(logDataList),
    subscribe: (listener: U) => {
      logListeners.push(listener);
      return () => {
        logListeners = logListeners.filter((l) => l !== listener);
      };
    },
    info: (message: string) => {
        pushLog({ level: "info", message } as T);
        console.info(message);
    },
    error: (message: string) => {
      pushLog({ level: "error", message } as T);
      console.error(message);
    },
    warn: (message: string) => {
      pushLog({ level: "warn", message } as T);
      console.warn(message);
    },
  };
};

前往这里查看完整的示例。

67. 使用 useDeferredValue 钩子显示先前的查询结果,直到有新的结果可用

假设你正在构建一个在地图上表示国家/地区的应用程序。

用户可以过滤以查看人口规模达到特定水平的国家/地区。

每次 maxPopulationSize 更新时,地图都会重新渲染(请参阅下面的示例)。

示例地址

因此,请注意滑块移动速度过快时滑块会变得多么不稳定。这是因为每次滑块移动时都会重新渲染地图。

为了解决这个问题,我们可以使用 useDeferredValue 钩子,以便滑块顺利更新。

const deferredMaxPopulationSize = useDeferredValue(maxPopulationSize);
<Map
    maxPopulationSize={deferredMaxPopulationSize}
    // …
/>

如果你正在寻找其他用法,请查看这篇文章

十. 必须知道的 React 库/工具

68. 使用 react-router 将路由功能集成到你的应用中

如果你需要你的应用支持多个页面,请查看 react-router

你可以在此处找到一个最简单的示例

69. 使用 swr 或 React Query 在你的应用中实现一流的数据获取

数据获取可能非常棘手。

但是,swr 或 React Query 等库可以让它变得容易得多。

对于简单的用例,建议使用 swr,对于更复杂的用例,建议使用 React Query。

70. 使用 formik、React Hook Form 或 TanStack Form 等库简化表单状态管理

如果你在使用表单时遇到困难,推荐可以看看这些库。

  • formik
  • React Hook Form
  • TanStack Form

71. 使用 Format.js、Lingui 或 react-i18next 使你的应用国际化。

如果你的应用需要支持多种语言,则应将其国际化。

你可以使用以下库来实现此目的:

  • Format.js
  • Lingui
  • react-i18next

72. 使用 framer-motion 轻松创建令人印象深刻的动画

动画可以让你的应用脱颖而出,你可以使用 framer-motion 轻松创建动画。

73. 还在使用自定义钩子重新发明轮子?

你是否还在使用自定义钩子来重新创造轮子?

推荐先看看ahooks或usehooks,看看是否有人已经为你完成了这项工作。

74. 利用 UI 库简化应用程序开发

构建可访问、响应迅速且美观的大规模 UI 非常困难。

Shadcdn、Headless UI、acro design、ant design、 等库可让这一过程变得更容易。

  • Shadcdn 提供了一组可访问、可重复使用且可组合的 React 组件,你可以将其复制并粘贴到你的应用中,不过它可能需要 Tailwind CSS。

  • Headless UI 提供了无样式、完全可访问的 UI 组件,你可以使用它们来构建自己的 UI 组件。

  • acro design 提供了提供了一套全面的设计规范和组件库,确保设计一致性。组件库丰富,涵盖表单、表格、导航、图标等常用元素。支持灵活的主题定制和国际化。遵循响应式设计理念,考虑访问性标准。

  • Ant Design提供了丰富的组件体系,覆盖了常见的中后台应用场景,如通用组件、布局组件、导航组件和数据录入组件等。

75. 使用 axe-core-npm 库检查你的网站的可访问性

网站应该对所有人开放。

然而,很容易忽略可访问性问题。

axe-core-npm 是一种快速、安全且可靠的方法,可在开发网站时检查网站的可访问性。

76. 使用 react-codemod 轻松重构 React 代码

Codemods 是以编程方式在代码库上运行的转换,它们使重构代码库变得容易。

例如,React codemods 可以帮助你从代码库中删除所有 React 导入,更新代码以使用最新的 React 功能等等。

因此,在手动重构代码之前,请务必检查这些内容。

77. 使用 vite-pwa 将你的应用转变为渐进式 Web 应用程序 (PWA)

渐进式 Web 应用程序 (PWA) 的加载方式与常规网页类似,但提供离线工作、推送通知和设备硬件访问等功能。

你可以使用 vite-pwa 在 React 中轻松创建 PWA。

十一. React 与 Visual Studio Code

78. 使用 Simple React Snippets 代码片段扩展来提高你的工作效率

引导新的 React 组件可能很繁琐。

Simple React Snippets 扩展中的代码片段让这一切变得更容易。

79. 将 editor.stickyScroll.enabled 设置为 true,可以快速定位当前组件

如果文件很大,可能很难找到当前组件,通过将 editor.stickyScroll.enabled 设置为 true,当前组件将始终位于屏幕顶部。类似于吸附效果。

80. 使用 VSCode Glean 或 VSCode React Refactor 等扩展简化重构

如果你需要频繁重构代码(例如,将 JSX 提取到新组件中),请务必查看 VSCode Glean 或 VSCode React Refactor 等扩展。

十二. React 与 TypeScript

81. 使用 ReactNode 保持代码更简洁

使用 ReactNode 代替 JSX.Element | null | undefined | ... 来保持代码更简洁

不要像这样输入 leftElement 和 rightElement 属性:

type Element =  JSX.Element | null | undefined; // | ...
const Panel = ({ leftElement,rightElement }: { leftElement?: Element;rightElement?: Element }) => {
    // ...
}

你可以使用 ReactNode 来保持代码更简洁。

const Panel = ({ leftElement,rightElement }: { leftElement?: ReactNode;rightElement?: ReactNode }) => {
    // ...
}

82. 使用 PropsWithChildren 简化需要子 props 的组件的输入

你不必手动输入 children 属性。

事实上,你可以使用 PropsWithChildren 来简化输入。

// PropsWithChildren类型来自于react
import { PropsWithChildren } from 'react';

interface PageProps {
   // ...
}
// 这样做也没有什么问题
const HeaderPage = ({ children,...pageProps }: { children: ReactNode } & PageProps) => {
  // ...
};

// 更好的做法
const HeaderPage = ({ children, ...pageProps } : PropsWithChildren<PageProps>) => {
  // ...
};

83. 使用 ComponentProps、ComponentPropsWithoutRef 等高效访问元素的props

在某些情况下,你需要弄清楚组件的 props。

例如,假设你想要一个按钮,当单击时会记录到控制台。

你可以使用 ComponentProps 访问按钮元素的 props,然后覆盖click prop。

import { ComponentProps } from 'react';

const ButtonWithLogging = ({ onClick }: ComponentProps<"button">) => {
  const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => {
    console.log("Button clicked");
    onClick?.(e);
  };
  return <button {...props} onClick={handleClick} />;
};

此技巧也适用于自定义组件。

import { ComponentProps } from 'react';

// 自定义组件
const MyComponent = (props: { name: string }) => {
  // ...
};

const MyComponentWithLogging = (props: ComponentProps<typeof MyComponent>) => {
  // ...
};

84. 利用 MouseEventHandler、FocusEventHandler 等类型来实现简洁的类型

你无需手动输入事件处理程序,而是可以使用 MouseEventHandler 之类的类型来使代码更简洁、更易读。

import { MouseEventHandler,FocusEventHandler,ChangeEventHandler } from 'react';
// 这样写也没什么问题
const MyComponent = ({ onClick, onFocus, onChange }: {
  onClick: (e: MouseEvent<HTMLButtonElement>) => void;
  onFocus: (e: FocusEvent<HTMLButtonElement>) => void;
  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}) => {
  // ...
};

// 更好的做法
const MyComponent = ({ onClick, onFocus, onChange }: {
  onClick: MouseEventHandler<HTMLButtonElement>;
  onFocus: FocusEventHandler<HTMLButtonElement>;
  onChange: ChangeEventHandler<HTMLInputElement>;
}) => {
  // ...
};

85. 当无法或不应从初始值推断类型时,请在 useState、useRef 等中明确指定类型

当无法从初始值推断出类型时,不要忘记指定类型。

例如,在下面的例子中,状态中存储了一个 selectedItemId,它应该是字符串或未定义。

由于未指定类型,TypeScript 会将类型推断为未定义,这不是我们想要的。

// 不好的做法: `selectedItemId`将会被推导为undefined
const [selectedItemId, setSelectedItemId] = useState(undefined);

// 推荐做法
const [selectedItemId, setSelectedItemId] = useState<string | undefined>(undefined);

注意:与此相反的是,当 TypeScript 可以为你推断类型时,你不需要指定类型。

86. 利用Record类型获得更清晰、更易于扩展的代码

假设我有一个代表日志级别的类型。

type LogLevel = "info" | "warn" | "error";

对于每个日志级别,我们都有一个相应的函数来记录消息。

const logFunctions = {
  info: (message: string) => console.info(message),
  warn: (message: string) => console.warn(message),
  error: (message: string) => console.error(message),
};

你可以使用 Record 类型,而不必手动输入 logFunctions的类型。

const logFunctions: Record<LogLevel, (message: string) => void> = {
  info: (message) => console.info(message),
  warn: (message) => console.warn(message),
  error: (message) => console.error(message),
};

使用 Record 类型可使代码更简洁、更易读,此外,如果添加或删除了新的日志级别,它还有助于捕获任何错误,例如,如果我决定添加调试日志级别,TypeScript 就会抛出错误。

87. 使用 as const 技巧来准确输入钩子返回值

假设我们有一个钩子 useIsHovered 来检测 div 元素是否处于悬停状态。

该钩子返回一个与 div 元素一起使用的 ref 和一个指示 div 是否处于悬停状态的布尔值。

const useIsHovered = () => {
  const ref = useRef<HTMLDivElement>(null);
  const [isHovered, setIsHovered] = useState(false);
  return [ref, isHovered]
};

目前,TypeScript 无法正确推断函数返回类型。

你可以通过明确输入返回类型来解决此问题,如下所示:

const useIsHovered = (): [RefObject<HTMLDivElement>, boolean] => {
  return [ref, isHovered]
};

或者你可以使用 as const 技巧来准确输入返回值:

const useIsHovered = () => {
  return [ref, isHovered] as const;
};

88. Redux:通过参考文档确保输入正确,以正确输入 Redux 状态和帮助程序。

如果你的项目是使用 Redux 来管理繁重的客户端状态,它也能很好地与 TypeScript 配合使用,你可以在此处找到有关如何将 Redux 与 TypeScript 结合使用的出色指南。

https://react-redux.js.org/using-react-redux/usage-with-typescript

89. 使用 ComponentType 简化你的类型

假设你正在设计一款像 Figma 这样的应用,该应用由小组件组成,每个小组件都接受一个size prop。

为了重用逻辑,我们可以定义一个共享的 WidgetWrapper 组件,该组件采用 Widget 类型的小组件,定义如下:

interface Size {
  width: number;
  height: number
};

interface Widget {
  title: string;
  Component: ComponentType<{ size: Size }>;
}

WidgetWrapper 组件将呈现小组件并将相关尺寸传递给它。

const WidgetWrapper = ({ widget }: { widget: Widget }) => {
  const { Component, title } = widget;
  const { onClose, size, onResize } = useGetProps(); // 待做:更好的名字,但你应该能明白我的意思
  return (
    <Wrapper onClose={onClose} onResize={onResize}>
      <Title>{title}</Title>
      {/* 我们可以使用以下尺寸渲染组件 */}
      <Component size={size} />
    </Wrapper>
  );

90. 使用 TypeScript 泛型提高代码的可重用性

TypeScript 泛型使你的代码更具可重用性和灵活性。

例如,假设我在博客上有不同的项目(例如,帖子、关注者等),并且我想要一个通用列表组件来显示它们。

export interface Post {
  id: string;
  title: string;
  contents: string;
  publicationDate: Date;
}

export interface User {
  username: string;
}

export interface Follower extends User {
  followingDate: Date;
}

每个列表都应该可排序,有好的方法和不好的方法可以做到这一点。

不好的方法:创建了一个接受项目联合的列表组件。

这很糟糕,因为:

  • 每次添加新项目时,都必须更新函数/类型。
  • 该函数不是完全类型安全的(请参阅注释)。
  • 此代码依赖于其他文件(例如:FollowerItem、PostItem)。
  • 等等。
import { FollowerItem } from "./FollowerItem";
import { PostItem } from "./PostItem";
import { Follower, Post } from "./types";

type ListItem = { type: "follower"; follower: Follower } | { type: "post"; post: Post };

const ListBad = ({
  items,
  title,
  vertical = true,
  ascending = true,
}: {
  title: string;
  items: ListItem[];
  vertical?: boolean;
  ascending?: boolean;
}) => {
  const sortedItems = [...items].sort((a, b) => {
    const sign = ascending ? 1 : -1;
    return sign * compareItems(a, b);
  });

  return (
    <>
      <h3 className="title">{title}</h3>
      <div className={`list ${vertical ? "vertical" : ""}`}>
        {sortedItems.map((item) => (
          <div key={getItemKey(item)}>{renderItem(item)}</div>
        ))}
      </div>
    </>
  );
}

const compareItems = (a: ListItem, b: ListItem) => {
  if (a.type === "follower" && b.type === "follower") {
    return (
      a.follower.followingDate.getTime() - b.follower.followingDate.getTime()
    );
  } else if (a.type == "post" && b.type === "post") {
    return a.post.publicationDate.getTime() - b.post.publicationDate.getTime();
  } else {
    // This shouldn't happen
    return 0;
  }
}

const getItemKey = (item: ListItem) => {
  switch (item.type) {
    case "follower":
      return item.follower.username;
    case "post":
      return item.post.id;
  }
}

const renderItem = (item: ListItem) => {
  switch (item.type) {
    case "follower":
      return <FollowerItem follower={item.follower} />;
    case "post":
      return <PostItem post={item.post} />;
  }
}

相反,我们可以使用 TypeScript 泛型来创建更可重用且类型安全的列表组件。

前往这里查看一个完整的示例

91. 使用 NoInfer 类型确保输入值的推断准确

想象一下,你正在开发一款视频游戏,游戏有多个地点(例如,山谷、公路等),你想创建一个将玩家传送到新位置的函数。

const teleportPlayer = <L extends string>(
  position: Position,
  locations: L[],
  defaultLocation: L,
) : L => {
  // ...
}

该函数将按如下方式调用:

const position = { x: 1, y: 2, z: 3 };
teleportPlayer(position, ['LeynTir', 'Forin', 'Karin'], 'Forin');
teleportPlayer(position, ['LeynTir', 'Karin'], 'anythingCanGoHere'); // 这会起作用,但这是错误的,因为“anythingCanGoHere”不应该是一个有效的位置

第二个示例无效,因为 anythingCanGoHere 不是有效位置,但是,TypeScript 不会抛出错误,因为它从列表和默认位置推断出 L 的类型。

要解决此问题,请使用 NoInfer 实用程序类型。

const teleportPlayer = <L extends string>(
  position: Position,
  locations: L[],
  defaultLocation: NoInfer<L>,
) : NoInfer<L> => {
  // ...
}

现在 TypeScript 将抛出一个错误:

teleportPlayer(position, ['LeynTir', 'Karin'], 'anythingCanGoHere'); // 错误:类型为“anythingCanGoHere”的参数无法分配给类型为“LeynTir”|“Karin”的参数

使用 NoInfer 工具类型可确保默认位置必须是列表中提供的有效位置之一,从而防止无效输入。

说明: NoInfer类型自ts5.4开始提供。ts5.4版本以下可以使用如下的代码模拟实现:

type NoInfer<T> = [T][T extends any ? 0 : never];

92. 使用 ElementRef 类型定义ref的类型

有2种方法来定义 ref 的类型。

比较困难的方法是记住元素的类型名称并直接使用它。

const ref = useRef<HTMLDivElement>(null);

最简单的方法是使用 ElementRef 类型。这种方法更直接,因为你应该已经知道元素的名称。

import { ElementRef } from 'react';

const ref = useRef<ElementRef<"div">>(null);

十三. 其它技巧

93. 使用 eslint-plugin-react 和 Prettier 提高代码的质量和安全性。

如果你不使用 eslint-plugin-react,你就不能写出好的 React代码。它可以帮助你捕获潜在的错误并实施最佳实践。因此,请确保为你的项目安装和配置它。

你也可以使用 Prettier 自动格式化你的代码并确保你的代码库一致。

94. 使用 Sentry 或 Grafana Cloud Frontend Observability 等工具记录和监控你的应用程序。

你无法改进你没有测试的应用。如果你正在寻找用于生产应用程序的监控工具,请查看 Sentry 或 Grafana Cloud Frontend Observability。

95. 使用在线 IDE 快速开始编码

设置本地开发环境可能很麻烦,尤其是对于初学者,因此,请从 Code Sandbox 、 Stackblitz 、豆包、jsbin、码上掘金等在线 IDE 开始,这些工具可让你快速开始编码,而无需担心设置环境。

96. 想要学习高级 React 技能?看看这些书

如果你正在寻找高级 React 书籍 📚,我推荐:

97. 准备 React 面试?查看 reactjs-interview-questions

React 面试可能会比较棘手,幸运的是,你可以通过查看这个 repo 来做好准备。

98. 向 Nadia、Dan、Josh、Kent 等专家学习 React 最佳实践。

如果你想了解最佳实践并学习技巧,请务必关注以下专家:

  • @adevnadia:x.com/adevnadia 了解高级 React 技巧
  • @joshwcomeau:x.com/joshwcomeau
  • @kentcdodds:x.com/kentcdodds
  • @mattpocockuk:x.com/mattpococku… 了解 TypeScript 技巧
  • @tejaskumar_:x.com/TejasKumar_
  • @housecor:x.com/housecor
  • @_ndeyefatoudiop:x.com/_ndeyefatou…

99. 订阅本周 React 或 ui.dev 等新闻通讯,了解 React 生态系统的最新动态

React 是一个快速发展的生态系统。

有许多工具、库和最佳实践需要跟上。

要保持最新状态,请务必订阅新闻通讯,例如:

  • @sebastienlorberThis Week In React
  • ui.dev
  • 等等。

100. 在 r/reactjs 等平台上与 React 社区互动

React 社区非常棒。

你可以从其他开发人员那里学到很多东西并分享你的知识。

因此,请在 r/reactjs 等平台上与社区互动。

关于我
loading
在线编辑器