11
头图

以下是个人收集总结的一些使用 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 简写 <></>(除非你需要设置一个 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 && <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 中的子组件,则会破坏缓存功能。

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

List.tsx:

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 采用渲染函数或组件函数,仅在需要时调用它们。

List.tsx:

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 && <Component /> 以避免保留旧状态

不好的写法: 在如下示例中,在切换时计数器 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.randomUUIDMath.random 生成 key

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

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

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

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

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

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

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

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}
    />
  );
};

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,你仍然可以使用命名导出。请参阅此处的介绍示例。

夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。