101 个 React 技巧和窍门

在这篇文章中,我分享了我多年来学到的101个最佳提示和技巧。准备好了吗?让我们开始吧💪!

注意:本指南假定你对 React 有基本的了解,并了解术语 propsstatecontext 等。

类别 #1:组件组织

1. 使用自闭合标签保持代码紧凑

// ❌ Bad: too verbose
<MyComponent></MyComponent>

// ✅ Good
<MyComponent/>

2. 优先使用 fragment 而不是 DOM 节点(例如 div、span 等)来对元素进行分组

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

❌ Bad: 使用 div 会使您的 DOM 混乱,并且可能需要更多的 CSS 代码。

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

✅ Good: <Fragment> 在不影响 DOM 结构的情况下包装元素。

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

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

❌ Bad: 下面的代码过于冗长。

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

✅ Good:  <></> 更简洁。

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

// Using a `Fragment` here is required because of the key.
function List({ users }) {
  return (
    <div>
      {users.map((user) => (
        <Fragment key={user.id}>
          <span>{user.name}</span>
          <span>{user.occupation}</span>
        </Fragment>
      ))}
    </div>
  );
}

4. 更喜欢展开 props 而不是单独访问每个 prop

❌ Bad: 下面的代码更难阅读(尤其是在大规模时)。

// We do `props…` all over the code.
function TodoList(props) {
  return (
    <div>
      {props.todos.map((todo) => (
        <div key={todo}>
          <button
            onClick={() => props.onSelectTodo(todo)}
            style={{
              backgroundColor: todo === props.selectedTodo ? "gold" : undefined,
            }}
          >
            <span>{todo}</span>
          </button>
        </div>
      ))}
    </div>
  );
}

✅ Good: 下面的代码更简洁。

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

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

❌ Bad: 您可能需要在多个位置定义默认值并引入新变量。

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

✅ Good:可以在顶部的一个位置设置所有默认值。这使其他人很容易找到它们。

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

6. 传递字符串类型 props 时去掉大括号。

// ❌ Bad: curly braces are not needed
<Button text={"Click me"} colorScheme={"dark"} />

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

7. 在使用 value&&<Component {...props}/> 时以防止在屏幕上显示意外值。

❌ Bad: 当列表为空时,屏幕上将显示 0

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

✅ Good:当没有项目时,屏幕上不会打印任何内容。

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

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

❌ Bad:  变量 gradeSum 和 gradeCount 使组件的作用域变得混乱。

function Grade({ grades }) {
  if (grades.length === 0) {
    return <>No grades available.</>;
  }

  let gradeSum = 0;
  let gradeCount = 0;

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

  const averageGrade = gradeSum / gradeCount;

  return <>Average Grade: {averageGrade}</>;
}

✅ Good:  变量 gradeSum 和 gradeCount的范围限定在 computeAverageGrade 函数中。

function Grade({ grades }) {
  if (grades.length === 0) {
    return <>No grades available.</>;
  }
  // 这个地方优先考虑使用 useMemo
  const computeAverageGrade = () => {
    let gradeSum = 0;
    let gradeCount = 0;
    grades.forEach((grade) => {
      gradeCount++;
      gradeSum += grade;
    });
    return gradeSum / gradeCount;
  };

  return <>Average Grade: {computeAverageGrade()}</>;
}

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

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

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

  return (
    <>
      <h1>Class Registration Form</h1>
      <form>
        <label>
          Name:{" "}
          <input
            type="text"
            value={name}
            onChange={(evt) =>
              setFormState((formState) => ({
                ...formState,
                name: evt.target.value,
              }))
            }
          />
        </label>
        <label>
          Email:{" "}
          <input
            type="email"
            value={email}
            onChange={(evt) =>
              setFormState((formState) => ({
                ...formState,
                email: evt.target.value,
              }))
            }
          />
        </label>
      </form>
    </>
  );
}

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

注意: 如果你打开了 ESLint 规则 jsx-no-bind,这个技巧就特别好了。你可以把柯里化的函数包装在 useCallback 中。

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

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

  return (
    <>
      <h1>Class Registration Form</h1>
      <form>
        <label>
          Name:{" "}
          <input
            type="text"
            value={name}
            onChange={createFormValueChangeHandler("name")}
          />
        </label>
        <label>
          Email:{" "}
          <input
            type="email"
            value={email}
            onChange={createFormValueChangeHandler("email")}
          />
        </label>
      </form>
    </>
  );
}

10. 将不依赖于组件 props/state 的数据移到它之外,以获得更简洁高效的代码。

❌ Bad:  OPTIONS 和 renderOption 不需要在组件内部,因为它们不依赖于任何 props 或 state。

此外,将它们保留在内部意味着每次组件渲染时我们都会获得新的对象引用。如果我们要将 renderOption 传递给包装在 memo 中的子组件,它会破坏 memoization。

function CoursesSelector() {
  const OPTIONS = ["Maths", "Literature", "History"];
  const renderOption = (option: string) => {
    return <option>{option}</option>;
  };

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

✅ Good:  将它们移出组件,以保持组件干净和引用稳定。

const OPTIONS = ["Maths", "Literature", "History"];
const renderOption = (option: string) => {
  return <option>{option}</option>;
};

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

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

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

function ListWrapper({ items }) {
  // We are referencing the entire item
  const [selectedItem, setSelectedItem] = useState<Item | undefined>();

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

✅ Good:  我们按其 ID 存储所选项目(应该是稳定的)。这可确保即使从列表中删除了项目或更改了其属性之一,UI 也应该是正确的。

function ListWrapper({ items }) {
  const [selectedItemId, setSelectedItemId] = useState<number | undefined>();
  // We derive the selected item from the list
  const selectedItem = items.find((item) => item.id === selectedItemId);

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

12. 如果你经常在做某事之前检查 prop 的值,请引入一个新组件

❌ Bad:  由于所有用户 == null 检查,代码变得混乱。

在这里,由于 hook 的规则,我们不能提前返回。

function Posts({ user }) {
  // Due to the rules of hooks, `posts` and `handlePostSelect` must be declared before the `if` statement.
  const posts = useMemo(() => {
    if (user == null) {
      return [];
    }
    return getUserPosts(user.id);
  }, [user]);

  const handlePostSelect = useCallback(
    (postId) => {
      if (user == null) {
        return;
      }
      // TODO: Do something
    },
    [user]
  );

  if (user == null) {
    return null;
  }

  return (
    <div>
      {posts.map((post) => (
        <button key={post.id} onClick={() => handlePostSelect(post.id)}>
          {post.title}
        </button>
      ))}
    </div>
  );
}

✅ Good:  我们引入了一个新组件 UserPosts,它接受定义的用户并且更加简洁。

function Posts({ user }) {
  if (user == null) {
    return null;
  }

  return <UserPosts user={user} />;
}

function UserPosts({ user }) {
  const posts = useMemo(() => getUserPosts(user.id), [user.id]);

  const handlePostSelect = useCallback(
    (postId) => {
      // TODO: Do something
    },
    [user]
  );

  return (
    <div>
      {posts.map((post) => (
        <button key={post.id} onClick={() => handlePostSelect(post.id)}>
          {post.title}
        </button>
      ))}
    </div>
  );
}

13. 使用 CSS :empty 伪类隐藏没有子元素的元素

在下面的示例中👇,包装器采用 children 并在其周围添加红色边框。

function PostWrapper({ children }) {
  return <div className="posts-wrapper">{children}</div>;
}
.posts-wrapper {
  border: solid 1px red;
}

❌ 问题:即使子项为空(即等于 nullundefined 等),边框在屏幕上仍然可见。

✅ 解决:  empty CSS 伪类来确保包装器为空时不显示。

.posts-wrapper:empty {
  display: none;
}

14. 将所有 state 和 context 分组到组件的顶部

当所有 state 和 context 都位于顶部时,很容易发现什么可以触发组件重新渲染。

❌ Bad:  状态和上下文是分散的,因此难以跟踪。

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

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

✅ Good:  所有状态和上下文都集中在顶部,以便于查找。

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

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

类别#2:有效的设计模式和技术

15. 利用 children 属性实现更简洁的代码(和性能优势)

使用 children props 有几个好处:

好处 #1:你可以通过将 props 直接传递给子组件而不是通过父组件路由它们来避免 prop 钻取。

好处 #2:您的代码更具可扩展性,因为您可以在不更改父组件的情况下轻松修改 children。

好处 #3:您可以使用此技巧来避免重新渲染 “慢速” 组件(请参阅下面的👇示例)。

❌ Bad: 每当 Dashboard 渲染时,MyVerySlowComponent 都会渲染,每次当前时间更新时都会发生这种情况。您可以在下图中看到它,其中我使用了 React Developer Tool 的分析器

function App() {
  // Some other logic…
  return (
    <Dashboard />
  );
}

function Dashboard() {
  const [currentTime, setCurrentTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setCurrentTime(new Date());
    }, 1000);
    return () => clearInterval(intervalId);
  }, []);

  return (
    <>
      <h1>{currentTime.toTimeString()}</h1>
      <MyVerySlowComponent />  {/* Renders whenever `Dashboard` renders */}
    </>
  );
}

✅ Good:  MyVerySlowComponent 在 Dashboard 渲染时不渲染。 

function App() {
  return (
    <Dashboard >
      <MyVerySlowComponent />
    </Dashboard>
  );
}

function Dashboard({ children }) {
  const [currentTime, setCurrentTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setCurrentTime(new Date());
    }, 1000);
    return () => clearInterval(intervalId);
  }, []);

  return (
    <>
      <h1>{currentTime.toTimeString()}</h1>
      {children}
    </>
  );
}

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

将复合组件想象成乐高积木。将它们拼凑在一起以创建自定义 UI。这些组件在创建库时工作得非常好,从而产生富有表现力且高度可扩展的代码。

reach.ui 中的示例Menu、MenuButton、MenuList、MenuLink 是复合组件)

<Menu>
  <MenuButton>
    Actions <span aria-hidden>▾</span>
  </MenuButton>
  <MenuList>
    <MenuItem onSelect={() => alert("Download")}>Download</MenuItem>
    <MenuItem onSelect={() => alert("Copy")}>Create a Copy</MenuItem>
    <MenuLink as="a" href="https://reacttraining.com/workshops/">
    Attend a Workshop
    </MenuLink>
  </MenuList>
</Menu>

17. 使用 render 函数组件函数 props 使您的代码更具可扩展性

假设我们想要显示各种列表,例如消息、个人资料或帖子,并且每个列表都应该是可排序的。为了实现这一点,我们引入了一个 List 组件以供重用。我们有两种方法可以解决这个问题:

❌ Bad:  选项 1

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

✅ Good: 选项 2

List 接受 render 函数或组件函数,仅在需要时调用它们。

您可以在下面的 👇 Sandbox 中找到一个示例:

🏖 Sandbox 

18. 在处理不同的情况时,使用 value === case & <Component /> 以避免保持旧状态

❌ 问题:在下面的 Sandbox 中,在 Posts 和 Snippets 之间切换时,计数器不会重置。发生这种情况是因为在渲染同一个组件时,它的状态在类型更改后保持不变。

🏖 Sandbox 

✅ 解决:根据 selectedType 渲染组件,或者在类型更改时使用键强制重置。

function App() {
  const [selectedType, setSelectedType] = useState<ResourceType>("posts");
  return (
    <>
      <Navbar selectedType={selectedType} onSelectType={setSelectedType} />
      {selectedType === "posts" && <Resource type="posts" />}
      {selectedType === "snippets" && <Resource type="snippets" />}
    </>
  );
}

// We use the `selectedType` as a key
function App() {
  const [selectedType, setSelectedType] = useState<ResourceType>("posts");
  return (
    <>
      <Navbar selectedType={selectedType} onSelectType={setSelectedType} />
      <Resource type={selectedType} key={selectedType} />
    </>
  );
}

19. 始终使用误差边界

默认情况下,如果应用程序在渲染过程中遇到错误,则整个 UI 会崩溃💥。若要防止这种情况,请使用错误边界

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

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

类别 #3:Key和Refs

20. 使用 crypto.randomUUID 或 Math.random 生成密钥

map() 调用中的 JSX 元素总是需要键。假设你的元素还没有键。在这种情况下,您可以使用 crypto.randomUUIDMath.random 或 uuid 库生成唯一 ID。

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

21. 确保您的列表项 ID 稳定(即它们在渲染之间不会更改)

键/ID 应尽可能稳定。否则,React 可能会无用地重新渲染一些组件,或者选择将不再有效,就像下面的例子一样。

❌ Bad:  每当 App 呈现时,selectedQuoteId 都会更改,因此永远不会有有效的选择。

function App() {
  const [quotes, setQuotes] = useState([]);
  const [selectedQuoteId, setSelectedQuoteId] = useState(undefined);

  // Fetch quotes
  useEffect(() => {
    const loadQuotes = () =>
      fetchQuotes().then((result) => {
        setQuotes(result);
      });
    loadQuotes();
  }, []);

  // Add ids: this is bad!!!
  const quotesWithIds = quotes.map((quote) => ({
    value: quote,
    id: crypto.randomUUID(),
  }));

  return (
    <List
      items={quotesWithIds}
      selectedItemId={selectedQuoteId}
      onSelectItem={setSelectedQuoteId}
    />
  );
}

✅ Good: 当我们获取报价时,将添加 ID

function App() {
  const [quotes, setQuotes] = useState([]);
  const [selectedQuoteId, setSelectedQuoteId] = useState(undefined);

  // Fetch quotes and save with ID
  useEffect(() => {
    const loadQuotes = () =>
      fetchQuotes().then((result) => {
        // We add the `ids` as soon as we get the results
        setQuotes(
          result.map((quote) => ({
            value: quote,
            id: crypto.randomUUID(),
          }))
        );
      });
    loadQuotes();
  }, []);

  return (
    <List
      items={quotes}
      selectedItemId={selectedQuoteId}
      onSelectItem={setSelectedQuoteId}
    />
  );
}

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

想要强制组件从头开始重新渲染?只需更改其key。在下面的示例中,我们使用此技巧在切换到新选项卡时重置错误边界。

🏖 Sandbox

23. 将 ref 回调函数用于监控大小变化和管理多个节点元素等任务

您知道可以将函数传递给 ref 属性而不是 ref 对象吗?以下是它的工作原理:

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

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

❌ 以前:使用 useEffect 聚焦输入

function App() {
  const ref = useRef();

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

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

✅ 现在:一旦输入可用,我们就会立即聚焦。

function App() {
  const ref = useCallback((inputNode) => {
    inputNode?.focus();
  }, []);

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

类别 #4:组织 React 代码

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

始终保持每个 React 组件包含相关资源,例如样式和图片。

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

25. 限制组件文件大小

包含大量组件和导出的大文件可能会令人困惑。此外,随着添加更多内容,它们往往会变得更大。因此,请以合理的文件大小为目标,并合理地将组件拆分为单独的文件。

26. 限制函数组件文件中的 return 语句数量

函数式组件中的多个 return 语句使得很难看到组件返回的内容。对于我们可以搜索 render 词的类组件来说,这不是问题。一个方便的技巧是尽可能使用不带大括号的箭头函数(VSCode有一个可以实现这个的功能 😀)。

❌ Bad:  更难发现组件 return 语句

function Dashboard({ posts, searchTerm, onPostSelect }) {
  const filteredPosts = posts.filter((post) => {
    return post.title.includes(searchTerm);
  });
  const createPostSelectHandler = (post) => {
    return () => {
      onPostSelect(post.id);
    };
  };
  return (
    <>
      <h1>Posts</h1>
      <ul>
        {filteredPosts.map((post) => {
          return (
            <li key={post.id} onClick={createPostSelectHandler(post)}>
              {post.title}
            </li>
          );
        })}
      </ul>
    </>
  );
}

✅ Good:  组件有一个 return 语句

function Dashboard({ posts, searchTerm, onPostSelect, selectedPostId }) {
  const filteredPosts = posts.filter((post) => post.title.includes(searchTerm));
  const createPostSelectHandler = (post) => () => {
    onPostSelect(post.id);
  };
  return (
    <>
      <h1>Posts</h1>
      <ul>
        {filteredPosts.map((post) => (
          <li
            key={post.id}
            onClick={createPostSelectHandler(post)}
            style={{ color: post.id === selectedPostId ? "red" : "black" }}
          >
            {post.title}
          </li>
        ))}
      </ul>
    </>
  );
}

27. 首选命名导出而不是默认导出

我到处都能看到默认导出,这让我很难过🥲。让我们比较一下这两种方法:

/// `Dashboard` is exported as the default component
export default function Dashboard(props) {
 /// TODO
}

/// `Dashboard` export is named
export function Dashboard(props) {
 /// TODO
}

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

/// Default export
import Dashboard from "/path/to/Dashboard"


/// Named export
import { Dashboard } from "/path/to/Dashboard"

以下是默认导出的问题:

  • 如果组件已重命名,IDE 不会自动重命名导出。

例如,如果 Dashboard 重命名为 Console,我们将有以下内容:

/// In the default export case, the name is not changed
import Dashboard from "/path/to/Console"


/// In the named export case, the name is changed
import { Console } from "/path/to/Console"
  • 很难查看从具有默认导出的文件中导出的内容。

例如,在命名导入的情况下,一旦我从 “/path/to/file” 键入 import { },当我将光标放在括号内时,我会得到自动补全。

  • 默认导出更难重新导出。

例如,如果我想从索引文件重新导出 Dashboard 组件,则必须执行以下操作:

export { default as Dashboard } from "/path/to/Dashboard"

对于命名导出,解决方案更直接。

export { Dashboard } from "/path/to/Dashboard"

💡 注意:即使你使用的是 React lazy,你仍然可以使用命名导出。请在此处查看示例。

因此,请默认使用命名导入 🙏 。

类别 #5:高效的状态管理

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

更多的状态 = 更多的麻烦。每个 state 都可以触发重新渲染,并使重置 state 变得很麻烦。因此,如果一个值可以从 state 或 props 派生,请跳过添加新的 state。

❌ Bad:  filteredPosts 不需要处于状态中。

function App({ posts }) {
  const [filters, setFilters] = useState();
  const [filteredPosts, setFilteredPosts] = useState([]);

  useEffect(
    () => {
      setFilteredPosts(filterPosts(posts, filters));
    },
    [posts, filters]
  );

  return (
    <Dashboard>
      <Filters filters={filters} onFiltersChange={setFilters} />
      {filteredPosts.length > 0 && <Posts posts={filteredPosts} />}
    </Dashboard>
  );
}

✅ Good: filteredPosts 派生自 posts 和 filters。

function App({ posts }) {
  const [filters, setFilters] = useState({});
  const filteredPosts = filterPosts(posts, filters)

  return (
    <Dashboard>
      <Filters filters={filters} onFiltersChange={setFilters} />
      {filteredPosts.length > 0 && <Posts posts={filteredPosts} />}
    </Dashboard>
  );
}

29. 将状态保持在最小化重新渲染所需的最低级别

每当组件内部的状态发生变化时,React 就会重新渲染该组件及其所有 children(children 包装在 memo 中时会出现异常)。

即使这些子项不使用 changed 状态,也会发生这种情况。为了最大限度地减少重新渲染,请尽可能地将 state 在组件树中向下移动。

❌ Bad:  当 sortOrder 更改时,LeftSidebar 和 RightSidebar 都会重新渲染。

function App() {
  const [sortOrder, setSortOrder] = useState("popular");
  return (
    <div className="App">
      <LeftSidebar />
      <Main sortOrder={sortOrder} setSortOrder={setSortOrder} />
      <RightSidebar />
    </div>
  );
}

function Main({ sortOrder, setSortOrder }) {
  return (
    <div>
      <Button
        onClick={() => setSortOrder("popular")}
        active={sortOrder === "popular"}
      >
        Popular
      </Button>
      <Button
        onClick={() => setSortOrder("latest")}
        active={sortOrder === "latest"}
      >
        Latest
      </Button>
    </div>
  );
}

✅ Good:  sortOrder 更改只会影响 Main

function App() {
  return (
    <div className="App">
      <LeftSidebar />
      <Main />
      <RightSidebar />
    </div>
  );
}

function Main() {
  const [sortOrder, setSortOrder] = useState("popular");
  return (
    <div>
      <Button
        onClick={() => setSortOrder("popular")}
        active={sortOrder === "popular"}
      >
        Popular
      </Button>
      <Button
        onClick={() => setSortOrder("latest")}
        active={sortOrder === "latest"}
      >
        Latest
      </Button>
    </div>
  );
}

30. 明确初始状态和当前状态之间的区别

❌ Bad: 目前尚不清楚 sortOrder 只是初始值,这可能会导致状态管理中出现混淆或错误。

function Main({ sortOrder }) {
  const [internalSortOrder, setInternalSortOrder] = useState(sortOrder);
  return (
    <div>
      <Button
        onClick={() => setInternalSortOrder("popular")}
        active={internalSortOrder === "popular"}
      >
        Popular
      </Button>
      <Button
        onClick={() => setInternalSortOrder("latest")}
        active={internalSortOrder === "latest"}
      >
        Latest
      </Button>
    </div>
  );
}

✅ Good:  命名清楚地说明了什么是初始状态,什么是当前状态。

function Main({ initialSortOrder }) {
  const [sortOrder, setSortOrder] = useState(initialSortOrder);
  return (
    <div>
      <Button
        onClick={() => setSortOrder("popular")}
        active={sortOrder === "popular"}
      >
        Popular
      </Button>
      <Button
        onClick={() => setSortOrder("latest")}
        active={sortOrder === "latest"}
      >
        Latest
      </Button>
    </div>
  );
}

31. 根据之前的状态更新 state,尤其是在使用 useCallback 进行记忆时

React 允许你将 updater 函数从 useState 传递给 set 函数。此更新器函数使用当前状态来计算下一个状态。

每当我需要根据之前的状态更新状态时,我都会使用这种行为,尤其是在使用 useCallback 包装的函数中。事实上,这种方法不需要将 state 作为 hook 依赖项之一。

❌ Bad:  每当 todos 更改时,handleAddTodo 和 handleRemoveTodo 都会更改。

function App() {
  const [todos, setToDos] = useState([]);
  const handleAddTodo = useCallback(
    (todo) => {
      setToDos([...todos, todo]);
    },
    [todos]
  );

  const handleRemoveTodo = useCallback(
    (id) => {
      setToDos(todos.filter((todo) => todo.id !== id));
    },
    [todos]
  );

  return (
    <div className="App">
      <TodoInput onAddTodo={handleAddTodo} />
      <TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
    </div>
  );
}

✅ Good:  即使 todos 发生变化,handleAddTodo 和 handleRemoveTodo 也保持不变。

function App() {
  const [todos, setToDos] = useState([]);

  const handleAddTodo = useCallback((todo) => {
    setToDos((prevTodos) => [...prevTodos, todo]);
  }, []);

  const handleRemoveTodo = useCallback((id) => {
    setToDos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
  }, []);

  return (
    <div className="App">
      <TodoInput onAddTodo={handleAddTodo} />
      <TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
    </div>
  );
}

32. 使用 useState 中的函数来延迟初始化和性能提升,因为它们只调用一次 

在 useState 中使用函数可确保初始状态只计算一次。这可以提高性能,尤其是当初始状态来自“昂贵”的操作(如从本地存储读取)时。

❌ Bad:  每次组件渲染时,我们都会从本地存储中读取主题。

const THEME_LOCAL_STORAGE_KEY = "101-react-tips-theme";

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

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

  return (
    <div
      className="page-wrapper"
      style={{ background: theme === "dark" ? "black" : "white" }}
    >
      <div className="header">
        <button onClick={() => handleThemeChange("dark")}>Dark</button>
        <button onClick={() => handleThemeChange("light")}>Light</button>
      </div>
      <div>{children}</div>
    </div>
  );
}

✅ Good:  我们只在组件挂载时从本地存储中读取。

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

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

  return (
    <div
      className="page-wrapper"
      style={{ background: theme === "dark" ? "black" : "white" }}
    >
      <div className="header">
        <button onClick={() => handleThemeChange("dark")}>Dark</button>
        <button onClick={() => handleThemeChange("light")}>Light</button>
      </div>
      <div>{children}</div>
    </div>
  );
}

33. 将 react context 用于广泛需要的静态状态,以防止 prop 钻探

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

  • 在多个地方需要(例如,主题、当前用户等)
  • 大部分是静态或只读的(即,用户不能/不会经常更改数据)

这种方法有助于避免 prop 钻取(即通过组件层次结构的多个层向下传递数据或状态)。

请参阅下面的👇沙盒中的示例 。

🏖 Sandbox

34. React Context:将您的上下文拆分为经常更改的部分和不经常更改的部分,以提高应用程序性能 

React context 的一个挑战是,每当上下文数据发生变化时,所有使用上下文的组件都会重新渲染,即使它们不使用上下文更改🤦 ♀️的部分。

解决方案?使用单独的上下文。在下面的示例中,我们创建了两个上下文:一个用于 actions (常量),另一个用于 state (可以更改)。

🏖 Sandbox

35. React Context:当值计算不简单时引入 Provider 组件 

❌ Bad:  App 内部有太多的逻辑来管理主题。

const THEME_LOCAL_STORAGE_KEY = "101-react-tips-theme";
const DEFAULT_THEME = "light";

const ThemeContext = createContext({
  theme: DEFAULT_THEME,
  setTheme: () => null,
})

function App() {
  const [theme, setTheme] = useState(
    () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
  );
  useEffect(() => {
    if(theme !== "system") {
      updateRootElementTheme(theme)
      return;
    }

    // We need to get the class to apply based on the system theme
    const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
        .matches
        ? "dark"
        : "light"

    updateRootElementTheme(systemTheme)

    // Then watch for changes in the system theme and update the root element accordingly
    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 [selectedPostId, setSelectedPostId] = useState(undefined);
  const onPostSelect = (postId) => {
    // TODO: some logging
    setSelectedPostId(postId);
  };

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

  return (
    <div className="App">
      <ThemeContext.Provider value={themeContextValue}>
        <Dashboard
          posts={posts}
          onPostSelect={onPostSelect}
          selectedPostId={selectedPostId}
        />
      </ThemeContext.Provider>
    </div>
  );
}

Good:  主题逻辑封装在 ThemeProvider 中 

function App() {
  const [selectedPostId, setSelectedPostId] = useState(undefined);
  const onPostSelect = (postId) => {
    // TODO: some logging
    setSelectedPostId(postId);
  };

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

  return (
    <div className="App">
      <ThemeProvider>
        <Dashboard
          posts={posts}
          onPostSelect={onPostSelect}
          selectedPostId={selectedPostId}
        />
      </ThemeProvider>
    </div>
  );
}


function ThemeProvider({ children }) {
  const [theme, setTheme] = useState(
    () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
  );
  useEffect(() => {
    if (theme !== "system") {
      updateRootElementTheme(theme);
      return;
    }

    // We need to get the class to apply based on the system theme
    const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
      .matches
      ? "dark"
      : "light";

    updateRootElementTheme(systemTheme);

    // Then watch for changes in the system theme and update the root element accordingly
    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>
  );
}

36. 考虑使用 useReducer 钩子作为轻量级状态管理解决方案🏖 Sandbox

 每当我的 state 中有太多的值或复杂的 state 并且不想依赖外部库时,我就会使用 useReducer。当与上下文结合使用以满足更广泛的状态管理需求时,它特别有效。例:见 #34

37. 使用 useImmer 或 useImmerReducer 简化 state 更新

使用像 useState 和 useReducer 这样的 hook,state 必须是不可变的(即所有更改都需要创建一个新的 state,而不是修改当前的 state)。这通常很难实现。这就是 useImmer 和 useImmerReducer 提供更简单替代方案的地方。它们允许您编写自动转换为不可变更新的 “可变” 代码。

❌ Tedious:  我们必须仔细确保我们正在创建一个新的 state 对象。

export function 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>Welcome</h1>
      <p>
        Email: <input type="email" value={email} onChange={onEmailChange} />
      </p>
      <p>
        Password:{" "}
        <input type="password" value={password} onChange={onPasswordChange} />
      </p>
    </div>
  );
}

更直接:我们可以直接修改 draftState

import { useImmer } from "use-immer";

export function 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;
    });
  };

  /// Rest of logic
}

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

每当我求助于 Redux

  • 我有一个复杂的 FE 应用程序,其中包含许多共享的客户端状态(例如,仪表板应用程序)
  • 我希望用户能够回到过去并恢复更改
  • 我不希望我的组件像使用 React 上下文那样不必要地重新渲染
  • 我有太多的上下文开始失控

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

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

39. Redux:使用 Redux DevTools 调试你的 state

Redux DevTools 浏览器扩展是调试 Redux 项目的有用工具。它允许您实时可视化您的状态和操作,在刷新之间保持状态持久性等等。要了解它的用途,请观看这个很棒的 YouTube 视频

类别 #6:React 代码优化 🚀

40. 防止使用 memo 进行不必要的重新渲染

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

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

在下面的示例中,我通过 useGetDashboardData 从服务器获取一些数据。如果帖子没有更改,将 ExpensiveList 包装在 memo 中将阻止它在数据的其他部分更新时重新呈现。

export function App() {
  const { profileInfo, posts } = useGetDashboardData();
  return (
    <div className="App">
      <h1>Dashboard</h1>
      <Profile data={profileInfo} />
      <ExpensiveList posts={posts} />
    </div>
  );
}

const ExpensiveList = memo(
  ({ posts }) => {
    /// Rest of implementation
  }
);

💡 :一旦 React 编译器稳定😅下来,这个提示可能就无关紧要了。

41. 使用 memo 指定一个相等函数来指导 React 如何比较 props

默认情况下,memo使用 Object.is 将每个 prop 与其前一个值进行比较。但是,对于更复杂或特定的方案,指定自定义相等函数可能比默认比较或重新渲染更有效。

const ExpensiveList = memo(
  ({ posts }) => {
    return <div>{JSON.stringify(posts)}</div>;
  },
  (prevProps, nextProps) => {
    // Only re-render if the last post or the list size changes
    const prevLastPost = prevProps.posts[prevProps.posts.length - 1];
    const nextLastPost = nextProps.posts[nextProps.posts.length - 1];
    return (
      prevLastPost.id === nextLastPost.id &&
      prevProps.posts.length === nextProps.posts.length
    );
  }
)

42. 在声明记忆化组件时,首选命名函数而不是箭头函数

在定义记忆化组件时,使用命名函数而不是箭头函数可以提高 React DevTools 的清晰度。箭头函数通常会产生 _c2 等泛型名称,这使得调试和分析更加困难。

❌ Bad:  对记忆化组件使用箭头函数会导致 React DevTools 中名称的信息较少。

const ExpensiveList = memo(
  ({ posts }) => {
    /// Rest of implementation
  }
);

✅ Good:  组件的名称将在 DevTools 中可见。 

const ExpensiveList = memo(
  function ExpensiveListFn({ posts }) {
    /// Rest of implementation
  }
);

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

我一般使用useMemo的场景

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

❌ Bad:  ExpensiveList 的 memo 不会阻止重新渲染,因为每次渲染都会重新创建样式。

export function App() {
  const { profileInfo, posts, baseStyles } = useGetDashboardData();
  // We get a new `styles` object on every render
  const styles = { ...baseStyles, padding: "10px" };
  return (
    <div className="App">
      <h1>Dashboard</h1>
      <Profile data={profileInfo} />
      <ExpensiveList posts={posts} styles={styles} />
    </div>
  );
}

const ExpensiveList = memo(
  function ExpensiveListFn({ posts, styles }) {
    /// Rest of implementation
  }
);

✅ Good: useMemo 的使用保证了 baseStyles 发生变化时,样式只发生变化,让 memo 能够有效地防止不必要的重新渲染。

export function App() {
  const { profileInfo, posts, baseStyles } = useGetDashboardData();
  // We get a new `styles` object only if `baseStyles` changes
  const styles = useMemo(
    () => ({ ...baseStyles, padding: "10px" }),
    [baseStyles]
  );
  return (
    <div className="App">
      <h1>Dashboard</h1>
      <Profile data={profileInfo} />
      <ExpensiveList posts={posts} styles={styles} />
    </div>
  );
}

44. 使用 useCallback 记忆函数

useCallback 类似于 useMemo,但它是明确为记忆函数而设计的。

❌ Bad: 每当主题发生变化时,handleThemeChange 会被调用两次,我们会向服务器推送日志两次。

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

  // `handleThemeChange` changes on every render
  // As a result, the effect will be triggered after each render
  const handleThemeChange = (newTheme) => {
    pushLog(["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;
}

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

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

45. 记住从工具钩子返回的回调或值以避免性能问题

当你创建自定义 hook 与他人共享时,记住返回的值和函数至关重要。这种做法可以提高你的 hook 效率,并防止任何使用它的人出现不必要的性能问题。

❌ Bad: loadData 未被记住,并产生性能问题。

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

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

  return { result, loadData };
}

✅ Good:  我们会记住所有内容,因此不会出现意外的性能问题。

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

  // Wrap in `useRef` and use the `ref` value so the function never changes
  const fetchDataRef = useRef(fetchData);
  useEffect(() => {
    fetchDataRef.current = fetchData;
  }, [fetchData]);

  // Wrap in `useCallback` and use the `ref` value so the function never changes
  const loadData = useCallback(async () => {
    setResult({ type: "loading" });
    try {
      const data = await fetchDataRef.current();
      setResult({ type: "loaded", data });
    } catch (err) {
      setResult({ type: "error", error: err });
    }
  }, []);

  return useMemo(() => ({ result, loadData }), [result, loadData])
}

46. 利用延迟加载和 Suspense 让您的应用程序加载更快

在构建应用程序时,请考虑对以下代码使用延迟加载和 Suspense

  •  加载成本高。
  • 仅与某些用户相关(如高级功能)。
  • 初始用户交互不需要立即实现。

在下面的沙箱👇中 ,Slider 资源 (JS + CSS) 仅在您单击卡片后加载。

🏖 Sandbox

Slider 资源仅在需要时加载

47. 限制您的网络以模拟慢速网络

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

这在以下情况下特别有用:

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

 限制网络请求以观察延迟加载  

48. 使用 react-window 或 react-virtuoso 有效地渲染列表

切勿一次呈现一长串项目,例如聊天消息、日志或无限列表。这样做可能会导致浏览器卡死。相反,采用虚拟化列表。这意味着仅呈现可能对用户可见的项子集。

像 react-windowreact-virtuoso 或 @tanstack/react-virtual 这样的库就是为此目的而设计的。

❌ Bad:  NonVirtualList 同时呈现所有 50,000 行日志行,即使它们不可见。

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

✅ Good:VirtualList 仅呈现可能可见的项。

function VirtualList({ items }) {
  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 #ccc",
          }}
        >
          <LogLine log={log} index={index} />
        </div>
      )}
    />
  );
}

您可以在下面的沙盒中的两个选项之间切换,并注意使用 👇 NonVirtualList 时应用程序的性能有多糟糕。

🏖 Sandbox

类别 #7:调试 React 代码 🐞 

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

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

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

下面的示例显示了一个错误,因为从未调用 clearIntervalStrictMode 通过运行两次 effect 来帮助捕获此问题,这将创建两个间隔。

🏖 Sandbox​​​​​​

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

React Developer Tools 是必须的扩展(ChromeFirefox)。此扩展允许您:

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

💡 在这份出色的指南中了解如何使用它。

51. React DevTools 组件:突出显示呈现以识别潜在问题的组件 

每当我怀疑我的应用程序存在性能问题时,我都会使用这个技巧。您可以突出显示渲染的组件以检测潜在问题(例如,渲染过多)。

下面的 gif 显示 FollowersListFn 组件在时间发生变化时重新渲染,这是错误的。

在组件渲染时高亮显示更新。

 52. 在你的自定义钩子中使用 useDebugValue 以提高在 React DevTools 中的可见性

useDebugValue 可以是一个方便的工具,用于在 React DevTools 中为你的自定义钩子添加描述性标签。这使得直接从 DevTools 界面监控其状态变得更加容易。例如,考虑一下我用来获取和显示当前时间的自定义钩子,每秒更新一次:

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

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

  return time;
}

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

✅ Good使用 useDebugValue,当前时间很容易看到:

useDebugValue(time)

注意:请谨慎使用 useDebugValue。最好为共享库中的复杂 hook 保留,因为在这些 hooks 中,了解内部状态至关重要。 

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

有时,组件会重新渲染,但目前尚不清楚原因🤦 ♀️。

虽然 React DevTools 很有帮助,但在大型应用程序中,它可能只提供模糊的解释,例如 “hook #1 rendered”,这可能毫无用处。由于 hook 1 更改而导致的应用渲染

 在这种情况下,您可以求助于 why-did-you-render 库。它提供了有关组件重新渲染原因的更详细见解,有助于更有效地查明性能问题。

我在下面的👇沙盒中做了一个例子。多亏了这个库,我们可以找到 FollowersList 组件的问题。

🏖 Sandbox

why-did-you-render 控制台日志

 54. 在 Strict Mode 的第二次渲染期间隐藏日志

StrictMode 有助于在应用程序开发的早期捕获错误。但是,由于它会导致组件渲染两次,这可能会导致重复的日志,这可能会使您的控制台变得混乱。您可以在 Strict Mode (严格模式) 的第二次渲染期间隐藏日志以解决此问题。在下面的 gif 👇 中查看如何操作:

 类别 #8:测试 React 代码 🧪

55. 使用 React Testing Library有效地测试你的 React 组件

想要测试您的 React 应用程序吗?确保使用 @testing-library/react。您可以在此处找到一个最小示例。

56. React 测试库:使用测试游乐场轻松创建查询

正在努力决定在测试中使用哪些查询?考虑使用 testing playground 从组件的 HTML 中快速生成它们。以下是利用它的两种方法:

选项 #1:在测试中使用 screen.logTestingPlaygroundURL()此函数会生成一个 URL,用于打开 Testing Playground 工具,其中已加载组件的 HTML。

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

57. 与 Cypress 或 Playwright 进行端到端测试 

需要进行端到端测试?请务必查看 Cypress 或 Playwright

注意:在撰写本文时,Playwright 对组件的支持是试验性的。

58. 在 MSW 测试中模拟网络请求

有时,您的测试需要发出网络请求。与其实现自己的 mocks,不如考虑使用 MSW(Mock Service Worker)来处理您的 API 响应。

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

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

类别 #9:React 钩子 🎣

 59. 确保在 useEffect 钩子中执行任何必要的清理

如果你正在设置任何需要稍后清理的东西,请始终在你的 useEffect 钩子中返回一个清理函数。这可以是从结束聊天会话到关闭数据库连接的任何内容。忽略此步骤可能会导致资源使用不佳和潜在的内存泄漏。

❌ Bad此示例设置一个间隔。但是我们从来没有清除它,这意味着即使在组件卸载后它也会继续运行。

function Timer() {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    setInterval(() => {
      setTime(new Date());
    }, 1_000);
  }, []);

  return <>Current time {time.toLocaleTimeString()}</>;
}

 ✅ Good卸载组件时,将正确清除间隔。

function Timer() {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1_000);
    // We clear the interval
    return () => clearInterval(intervalId);
  }, []);

  return <>Current time {time.toLocaleTimeString()}</>;
}

60. 使用 refs 访问 DOM 元素

像 document.getElementById 和 document.getElementsByClassName 这样的方法是被禁止的,因为 React 应该访问/操作 DOM。那么,当你需要访问 DOM 元素时,你应该怎么做呢?

你可以使用 useRef 钩子,就像下面的例子一样,我们需要访问 canvas 元素。

🏖 Sandbox
 

注意:我们可以向画布添加一个 ID 并使用 document.getElementById,但不推荐。
 

61. 使用 refs 在重新渲染中保留值

如果你的 React 组件中有可变值没有存储在 state 中,你会注意到对这些值的更改不会在重新渲染后持续存在。除非您全局保存它们,否则会发生这种情况。您可以考虑将这些值放在 state 中。但是,如果它们与渲染无关,则可能会导致不必要的重新渲染,从而浪费性能。这就是 useRef 的亮点。

在下面的示例中,我想在用户单击某个按钮时停止计时器。为此,我需要将 intervalId 存储在某个位置。

❌ Bad下面的示例不会按预期工作,因为 intervalId 会随着每个组件的重新渲染而被重置。

function Timer() {
  const [time, setTime] = useState(new Date());
  let intervalId;

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

  const stopTimer = () => {
    intervalId && clearInterval(intervalId);
  };

  return (
    <>
      <>Current time: {time.toLocaleTimeString()} </>
      <button onClick={stopTimer}>Stop timer</button>
    </>
  );
}

✅ Good通过使用 useRef,我们确保在渲染之间保留间隔 ID。

function Timer() {
  const [time, setTime] = useState(new Date());
  const intervalIdRef = useRef();
  const intervalId = intervalIdRef.current;

  useEffect(() => {
    const interval = setInterval(() => {
      setTime(new Date());
    }, 1_000);
    intervalIdRef.current = interval;
    return () => clearInterval(interval);
  }, []);

  const stopTimer = () => {
    intervalId && clearInterval(intervalId);
  };

  return (
    <>
      <>Current time: {time.toLocaleTimeString()} </>
      <button onClick={stopTimer}>Stop timer</button>
    </>
  );
}

62. 在 useEffect 等钩子中更喜欢命名函数而不是箭头函数,以便在 React 开发工具中轻松找到它们

如果你有很多钩子,在 React DevTools 中找到它们可能很有挑战性。一个技巧是使用命名函数,以便您可以快速发现它们。

❌ Bad在众多钩子中很难找到具体效果。

function HelloWorld() {
  useEffect(() => {
    console.log("🚀 ~ Hello, I just got mounted")
  }, []);

  return <>Hello World</>;
}

 ✅ Good您可以快速发现效果。

function HelloWorld() {
  useEffect(function logOnMount() {
    console.log("🚀 ~ Hello, I just got mounted");
  }, []);

  return <>Hello World</>;
}

具有关联名称的效果器 63. 使用自定义钩子封装逻辑

假设我有一个组件,它从用户的深色模式首选项中获取主题并在应用程序内使用它。最好将返回主题的逻辑提取到自定义钩子中(以重用它并保持组件干净)。

❌ BadApp 过于拥挤

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

 ✅ GoodApp 要简单得多,我们可以重用 logic

function App() {
  const theme = useTheme();

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

// Custom hook that can be reused
function 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;
}

64. 更喜欢函数而不是自定义钩子

当可以使用 function 时🛑,永远不要将 logic 放在 hook 中。实际上:

  • Hook 只能在其他 hook 或 component 中使用,而 functions 可以在任何地方使用。
  • 函数比钩子更简单。
  • 函数更易于测试。

❌ BaduseLocale 钩子是不必要的,因为它不需要是一个钩子。它不使用其他钩子,如 useEffectuseState 等。

function App() {
  const locale = useLocale();
  return (
    <div className="App">
      <IntlProvider locale={locale}>
        <BlogPost post={EXAMPLE_POST} />
      </IntlProvider>
    </div>
  );
}

function useLocale() {
  return window.navigator.languages?.[0] ?? window.navigator.language;
}

✅ Good改为创建函数 getLocale

function App() {
  const locale = getLocale();
  return (
    <div className="App">
      <IntlProvider locale={locale}>
        <BlogPost post={EXAMPLE_POST} />
      </IntlProvider>
    </div>
  );
}

function getLocale() {
  return window.navigator.languages?.[0] ?? window.navigator.language;
}

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

如果效果不是由用户交互引起的,则用户将在效果运行之前(通常是短暂的)看到 UI。因此,如果效果修改了 UI,用户将在看到更新的 UI 版本之前非常快速地看到初始 UI 版本,从而产生视觉故障。

使用 useLayoutEffect 可确保 effect 在所有 DOM 更改后同步运行,从而防止初始渲染故障。

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

使用 useEffect,您可以在开头短暂地看到表正在发生变化。列在调整为正确大小之前,会以其默认大小呈现。

🏖 Sandbox

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

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

厌倦了想出 ID 或让它们发生冲突?你可以使用 useId 钩子在 React 组件中生成一个唯一的 ID,并确保你的应用程序是可访问的。

function Form() {
  const id = useId();
  return (
    <div className="App">
      <div>
        <label>
          Name{" "}
          <input type="text" aria-describedby={id} />
        </label>
      </div>
      <span id={id}>Make sure to include full name</span>
    </div>
  );
}

67. 使用 useSyncExternalStore 订阅外部 store

这是一个很少需要但超级强大的钩子💪。在以下情况下使用此钩子:

  • 你有一些 state 在 React 树中无法访问(即,不在 state 或 context 中)
  • 状态可能会更改,您需要将组件通知更改

在下面的示例中,我希望 Logger单例在我的整个应用程序中记录错误、警告、信息等。这些是要求

  • 我需要能够在我的 React 应用程序中的任何位置(甚至在非 React 组件中)调用 this,因此我不会将其放在 state/context 中。
  • 我想在 Logs 组件中向用户显示所有日志

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

function createLogger() {
  let logs = [];
  let listeners = [];

  const pushLog = (log) => {
    logs = [...logs, log];
    listeners.forEach((listener) => listener());
  };

  return {
    getLogs: () => Object.freeze(logs),
    subscribe: (listener) => {
      listeners.push(listener);
      return () => {
        listeners = listeners.filter((l) => l !== listener);
      };
    },
    info: (message) => {
      pushLog({ level: "info", message });
      console.info(message);
    },
    error: (message) => {
      pushLog({ level: "error", message });
      console.error(message);
    },
    warn: (message) => {
      pushLog({ level: "warn", message });
      console.warn(message);
    },
  };
}

export const Logger = createLogger();

🏖 Sandbox

68. 使用 useDeferredValue 钩子显示以前的查询结果,直到新结果可用

假设您正在构建一个在地图上表示国家/地区的应用程序。用户可以进行筛选以查看特定人口规模的国家/地区。每次 maxPopulationSize 更新时,都会重新渲染地图(请参阅下面的沙盒)。

🏖 Sandbox

 因此,请注意,当您将滑块移动得太快时,它是多么卡顿。这是因为每次滑块移动时都会重新渲染地图。为了解决这个问题,我们可以使用 useDeferredValue 钩子,以便滑块平滑更新。

<Map
    maxPopulationSize={deferredMaxPopulationSize}
    // …
/>

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

类别 #10:必须知道的 React 库/工具 🧰

69. 使用 react-router 将路由合并到您的应用程序中 

如果你需要你的应用程序支持多个页面,请查看 react-router。您可以在此处找到一个最小示例。

70. 使用 swr 或 React Query 在您的应用程序中实现一流的数据获取 

众所周知,数据获取可能非常棘手。但是,像 swr 或 React Query 这样的库让它变得容易得多。 我推荐 swr 用于简单的用例,而 React Query 用于更复杂的用例。

71. 使用 formikReact Hook Form 或 TanStack Form 等库简化表单状态管理

我曾经讨厌 React 🥲 中的表单管理。直到我发现了这样的库:

因此,如果您正在为表单而苦苦挣扎,请务必查看这些。

72. 使用 Format.js,Lingui, 或 react-i18next. 实现你的应用程序的国际化。

如果您的应用程序需要支持多种语言,则应将其国际化。您可以使用以下库来实现这一点:

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

动画可以使您的应用程序脱颖而出🔥。您可以使用 framer-motion 轻松创建它们。

74. 厌倦了重新发明带有定制钩子的轮子?查看 useHooks – The React Hooks Library

如果你和我一样,你已经一遍又一遍地写同样的钩子了。请先检查 usehooks.com,看看是否有人已经为您完成了这项工作。

75. 利用 Shadcdn 或无头 UI 等 UI 库简化应用程序开发

很难大规模构建可访问、响应迅速且美观的 UI。Shadcdn 或 Headless UI 等库使它变得更容易。

  • Shadcdn 提供了一组可访问、可重用和可组合的 React 组件,您可以将这些组件复制并粘贴到您的应用程序中。在撰写本文时,它需要 Tailwind CSS。
  • Headless UI 提供无样式、完全可访问的 UI 组件,您可以使用这些组件来构建自己的 UI 组件。

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

网站应该对每个人都开放。但是,很容易错过辅助功能问题。axe-core-npm 是一种快速、安全且可靠的方法,可以在开发网站时检查网站的可访问性。

💡 提示: 如果你是 VSCode 用户,你可以安装相关的扩展:axe Accessibility Linter

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

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

例如,React codemods 可以帮助你从代码库中删除所有 React 导入,更新代码以使用最新的 React 功能等等。因此,请务必在手动重构代码之前检查这些内容。

78. 使用 vite-pwa 将您的应用程序转换为渐进式 Web 应用程序 (PWA)

渐进式 Web 应用程序 (PWA) 的加载方式与常规网页类似,但提供脱机工作、推送通知和设备硬件访问等功能。你可以使用 vite-pwa 在 React 中轻松创建 PWA。

类别#11:React & Visual Studio Code 🛠️

79. 使用 Simple React Snippets snippets扩展提高您的工作效率

引导一个新的 React 组件可能很乏味😩。来自 Simple React Snippets 扩展的 Snippets 使它变得更容易。

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

我喜欢这个功能❤️。如果您有一个大文件,则可能很难找到当前组件。通过将 editor.stickyScroll.enabled 设置为 true,当前组件将始终位于屏幕顶部。

❌ 无粘性卷轴

✅ 带粘性卷轴 

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

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

类别#12:React & TypeScript 🚀

82. 使用 ReactNode 而不是 JSX.Element | null | undefined | ...使代码更紧凑

我经常看到这个错误。而不是像这样键入 leftElement 和 rightElement 属性:

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

 您可以使用 ReactNode 使代码更加紧凑。

const MyComponent = ({ leftElement, rightElement }: { leftElement: ReactNode; rightElement: ReactNode }) => {
  //   …
};

83. 使用 PropsWithChildren 简化 expecting children 属性的组件的键入

你不必手动键入 children 属性。实际上,您可以使用 PropsWithChildren 来简化类型。

// 🟠 Ok
const HeaderPage = ({ children,...pageProps }: { children: ReactNode } & PageProps) => {
  //   …
};

// ✅ Better
const HeaderPage = ({ children, ...pageProps } : PropsWithChildren<PageProps>) => {
//   …
};

 84. 使用 ComponentPropsComponentPropsWithoutRef 有效地访问元素 props

在某些情况下,你需要弄清楚组件的 props。例如,假设你想要一个按钮,该按钮在单击时将记录到控制台。你可以使用 ComponentProps 来访问 button 元素的 props,然后覆盖 click props。

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

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

const MyComponent = (props: { name: string }) => {
  //   …
};

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

85. 利用 MouseEventHandlerFocusEventHandler 等类型进行简洁的类型

 您可以使用像 MouseEventHandler 这样的类型来保持代码更简洁和可读,而不是手动键入事件处理程序。

// 🟠 Ok
const MyComponent = ({ onClick, onFocus, onChange }: {
  onClick: (e: MouseEvent<HTMLButtonElement>) => void;
  onFocus: (e: FocusEvent<HTMLButtonElement>) => void;
  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}) => {
  //   …
};

// ✅ Better
const MyComponent = ({ onClick, onFocus, onChange }: {
  onClick: MouseEventHandler<HTMLButtonElement>;
  onFocus: FocusEventHandler<HTMLButtonElement>;
  onChange: ChangeEventHandler<HTMLInputElement>;
}) => {
  //   …
};

86. 当类型不能或不应该从初始值推断时,在 useState、useRef 等中显式指定类型

当无法从初始值推断出类型时,请不要忘记指定类型。 例如,在下面的示例中,状态中存储了一个 selectedItemId。它应该是一个string或 undefined。由于没有指定类型,TypeScript 会将类型推断为 undefined,这不是我们想要的。

// ❌ Bad: `selectedItemId` will be inferred as `undefined`
const [selectedItemId, setSelectedItemId] = useState(undefined);

// ✅ Good
const [selectedItemId, setSelectedItemId] = useState<string | undefined>(undefined);

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

87. 利用 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 类型可使代码更加简洁和可读。此外,如果添加或删除了新的日志级别,它还有助于捕获任何错误。例如,如果我决定添加debug日志级别,TypeScript 将引发错误。

88. 使用 as const 技巧准确地输入你的钩子返回值 

假设我们有一个钩子 useIsHovered 来检测 div 元素是否悬停。钩子返回一个用于 div 元素的 ref 和一个指示 div 是否悬停的布尔值。

const useIsHovered = () => {
  const ref = useRef<HTMLDivElement>(null);
  const [isHovered, setIsHovered] = useState(false);
  // TODO : Rest of implementation
  return [ref, isHovered]
};

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

您可以通过显式键入返回类型来解决此问题,如下所示:

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

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

const useIsHovered = () => {
  // TODO : Rest of implementation
  return [ref, isHovered] as const;
};

89. Redux:通过参考 Usage with TypeScript | React Redux 确保正确输入您的 Redux state 和帮助程序

我喜欢使用 Redux 来管理繁重的客户端状态。它也适用于 TypeScript。你可以在这里找到有关如何将 Redux 与 TypeScript 一起使用的优秀指南。

90. 使用 ComponentType 简化类型

假设您正在设计一个像 Figma 这样的应用程序(我知道,您雄心勃勃😅)。该应用程序由小部件组成,每个小部件接受一个size。要重用逻辑,我们可以定义一个共享的 WidgetWrapper 组件,该组件接受 Widget 类型的 widget,定义如下:

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

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

WidgetWrapper 组件将呈现 widget 并将相关大小传递给它。

const WidgetWrapper = ({ widget }: { widget: Widget }) => {
  const { Component, title } = widget;
  const { onClose, size, onResize } = useGetProps(); //TODO: better name but you get the idea 😅
  return (
    <Wrapper onClose={onClose} onResize={onResize}>
      <Title>{title}</Title>
      {/* We can render the component below with the size */}
      <Component size={size} />
    </Wrapper>
  );
};

91. 使用 TypeScript 泛型使您的代码更具可重用性

如果你没有使用 TypeScript 泛型,则只会发生两种情况:

  • 您要么正在编写非常简单的代码
  • 您错过了

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

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

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

export interface User {
  username: string;
}

export interface Follower extends User {
  followingDate: Date;
}

每个列表都应该是可排序的。有个坏方法和好方法。

❌ Bad我创建了一个接受项联合的 list 组件。这很糟糕,因为:

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

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

function 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 (
    <div>
      <h3 className="title">{title}</h3>
      <div className={`list ${vertical ? "vertical" : ""}`}>
        {sortedItems.map((item) => (
          <div key={getItemKey(item)}>{renderItem(item)}</div>
        ))}
      </div>
    </div>
  );
}

function 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;
  }
}

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

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

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

我在下面的👇沙盒中做了一个例子。

🏖 Sandbox

 92. 确保使用 NoInfer 实用程序类型进行精确键入

想象一下你正在开发一个视频游戏🎮。游戏有多个地点(例如 LeynTirForinKarin 等)。您想要创建一个函数,将玩家传送到新位置。

function teleportPlayer<L extends string>(
  position: Position,
  locations: L[],
  defaultLocation: L,
) : L {
  // Teleport the player and return the location
}

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

const position = { x: 1, y: 2, z: 3 };
teleportPlayer(position, ["LeynTir", "Forin", "Karin"], "Forin");
teleportPlayer(position, ["LeynTir", "Karin"], "anythingCanGoHere"); // ❌ This will work, but it is wrong since "anythingCanGoHere" shouldn't be a valid location

第二个示例无效,因为 anythingCanGoHere 不是有效位置。

但是,TypeScript 不会引发错误,因为它从列表中推断出 L 的类型和默认位置。

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

function teleportPlayer<L extends string>(
  position: Position,
  locations: L[],
  defaultLocation: NoInfer<L>,
) : NoInfer<L> {
  // Teleport the player and return the location
}

现在 TypeScript 会抛出一个错误:

teleportPlayer(position, ["LeynTir", "Karin"], "anythingCanGoHere"); // ❌ Error: Argument of type '"anythingCanGoHere"' is not assignable to parameter of type '"LeynTir" | "Karin"

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

93. 使用 ElementRef 类型帮助程序轻松键入 refs

有一种简单的方法来键入 refs。困难的方法是记住元素的类型名称并直接🤣使用它。

const ref = useRef<HTMLDivElement>(null);

最简单的方法是使用 ElementRef 类型帮助程序。更直接,因为您应该已经知道元素的名称。

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

类别 #13:杂项提示 🎉 

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

如果你不使用 eslint-plugin-react😅,你就不能认真对待 React。它可以帮助您捕获潜在的错误并实施最佳实践。因此,请确保为您的项目安装和配置它。您还可以使用 Prettier 自动格式化代码并确保您的代码库一致。

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

你无法改进你不衡量📏的东西。如果您正在为生产应用程序寻找监控工具,请查看 Sentry 或 Grafana Cloud Frontend Observability

96. 使用 Code Sandbox 或 Stackblitz 等在线 IDE 快速开始编码

设置本地开发环境可能很痛苦。尤其是作为初学者 🐣 。因此,请从 Code Sandbox 或 Stackblitz 等在线 IDE 开始。这些工具允许您快速开始编码,而无需担心设置环境。

97. 寻找高级 react 技能?看看这些书 👇

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

98. 准备 React 面试?检查 reactjs-interview-questions

React 面试⚛️可能很棘手。幸运的是,您可以通过检查此 repo 来为它们做好准备。

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

如果您想及时了解最佳实践并学习提示,请务必遵循以下专家:

100. 通过订阅 This Week In React 或 ui.dev 等时事通讯,随时了解 React 生态系统的最新动态

React 是一个快速发展的生态系统。有许多工具、库和最佳实践需要跟上。要保持更新,请务必订阅以下时事通讯💌:

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

React 社区太棒了。您可以从其他开发人员那里学到很多东西并分享您的知识。因此,请在 r/reactjs 等平台上与社区互动。

  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值