那些有用的 React 开发小建议

1,521 阅读7分钟

概述

React 开发中有很多值得注意的点,但是当我们形成自己固有的一套编码风格后,可能不会再去优化自己写法,这时就需要通过阅读官方文档、学习他人经验、阅读优秀开源库等方式提升自己编码水平。

以下提供一些编码小建议,有些点可能只是在某些场景下更适合使用,具体还需各位自己判断。

React 开发小建议

1. value && <Component />

有时我们需要根据某个条件决定是否要显示组件,如 value && <Component /> ,这种情况下要确保 value 是布尔值,以防在界面上显示意外值。

❌ 不建议的写法:当 items 为空数组时,界面上会显示 0

export function List({ items }) {
  return (
    <div className="list">
      {items.length && (
        <div>
          <p>List</p>
          <div>{items.map(item => item.name))}</div>
        </div>
      )}
    </div>
  );
}

✅ 建议写法: items 为空数组,界面不会渲染任何内容。

export function List({ items }) {
  return (
    <div className="list">
      {items.length > 0 && (
        <div>
          <p>List</p>
          <div>{items.map(item => item.name))}</div>
        </div>
      )}
    </div>
  );
}

2. 使用函数避免中间变量污染作用域

❌ 不建议的写法: 变量 gradeSumgradeCount 直接挂在组件的作用域下。

function Grade({ grades }) {
  let gradeSum = 0;
  let gradeCount = 0;

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

  const averageGrade = gradeSum / gradeCount;

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

✅ 建议写法: 变量 gradeSumgradeCountcomputeAverageGrade 函数中定义和使用。

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

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

上面代码中你也可以把 computeAverageGrade 函数在组件外定义,随后在组件中使用。

3. 将不依赖组件参数和状态的数据移至组件之外,使代码更简洁高效

❌ 不建议的写法:OPTIONSrenderOption 可以不再组件中定义,因为它们不依赖组件参数和状态。

此外,将它们放在组件内部意味着每次组件渲染时我们都会获得新的对象引用。如果我们将 OPTIONS 传递给一个用 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>
  );
}

✅ 建议的写法:把 OPTIONSrenderOption 移除组件,保持组件的简洁和变量的引用不变。

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

4. 将所有组件状态和全局状态集中到组件顶部

当所有状态都位于顶部时,就可以一眼看出哪些因素会触发组件的重新渲染。

❌ 不建议的写法:状态分散,难以跟踪。

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

✅ 建议的写法:所有状态都集中在顶部,便于查找。

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

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

5. 处理不同条件时,使用 value === case && <Component /> 避免组件保留旧状态

❌ 有问题的写法:以下代码中,切换选项时,Resource 状态不会重置。

function Resource({ type }) {
  const [likes, setLikes] = useState(0);
  const handleClick = () => {
    setLikes((prevLikes) => prevLikes + 1);
  };
  return (
    <Button onClick={handleClick}>
      {type == 1 ? "类型1" : "类型2"}点赞次数:{likes}
    </Button>
  );
}

const App = function () {
  const [value, setValue] = useState(1);
  const onChange = (e) => {
    setValue(e.target.value);
  };

  return (
    <div>
      <Radio.Group onChange={onChange} value={value}>
        <Radio value={1}>A</Radio>
        <Radio value={2}>B</Radio>
      </Radio.Group>
      <Resource type={value} />
    </div>
  );
};

✅ 处理方法:根据类型各自渲染组件,或者通过 key 来触发组件重新渲染。

function App() {
  ...
  return (
    <div>
      <Radio.Group onChange={onChange} value={value}>
        <Radio value={1}>A</Radio>
        <Radio value={2}>B</Radio>
      </Radio.Group>
      {value === 1 && <Resource type={1} />}
      {value === 2 && <Resource type={2} />}
    </div>
  );
}

// 使用 key
function App() {
  ...
  return (
    <div>
      <Radio.Group onChange={onChange} value={value}>
        <Radio value={1}>A</Radio>
        <Radio value={2}>B</Radio>
      </Radio.Group>
      <Resource type={value} key={value} />
    </div>
  );
}

6. 使用错误边界

默认情况下,如果应用在渲染过程中遇到错误,整个用户界面就会崩溃。

为了避免这种情况,可以使用 error boundaries ,建议直接使用社区的库:react-error-boundary

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

<ErrorBoundary fallback={<div>Something went wrong</div>}>
  <ExampleApplication />
</ErrorBoundary>

7. 确保列表项 key 值的稳定

渲染列表时,key 值应该唯一稳定,不然,React 可能会无用地重新渲染某些组件。

❌ 不建议的写法:每当 App 渲染时,id 都会发生变化。

function App() {
  const [list, setList] = useState([])
  
  useEffect(() => {
    setList(['apple', 'banana'])
  }, [])

  const listWithIds = list.map((item) => ({
    value: item,
    id: crypto.randomUUID(),
  }));

  return (
    <ul>
       {listWithIds.map(item => {
         return <li key={item.id}>{item.value}</li>
       })}
    </ul>
  );
}

✅ 建议的写法:id 在原数据源上添加。

function App() {
  const [list, setList] = useState([])
  
  useEffect(() => {
    setList(['apple', 'banana'].map((item) => ({
      value: item,
      id: crypto.randomUUID(),
    }))
  }, [])

  return (
    <ul>
       {list.map(item => {
         return <li key={item.id}>{item.value}</li>
       })}
    </ul>
  );
}

8. 可以从组件状态和参数推导出的值,不要再保存为状态值

状态越多,组件越复杂,我们编码应该遵守保持简单的原则。

每一个状态都可能触发重新渲染,状态的数量多起来后,要想查看组件是因哪个状态变化触发更新也会变的麻烦。因此,如果这个值可以从状态或参数中导出,就不要添加新的状态。

❌ 不建议的写法: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>
  );
}

✅ 建议的写法:filteredPostspostsfilters 计算得出。

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

9. 根据之前的状态更新状态,尤其是在使用 useCallback 缓存函数时

React useState 中返回的 set 方法支持传递一个更新函数,该更新函数可以使用当前状态来计算下一个状态。

useCallback 中使用更新函数时,就可以不必把状态加入到依赖项中。

❌ 不建议的写法:当 todos 变化时,handleAddTodohandleRemoveTodo 也会变化。

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

✅ 建议的写法:todos 变化时,handleAddTodohandleRemoveTodo 保持缓存版本。

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

10. 在 useState 调用时使用函数参数可实现懒初始化

useState 调用时使用函数参数可确保初始状态仅计算一次。

当初始状态有涉及性能消耗时(例如从本地存储读取、复杂耗时计算等),这种方式可以提高性能。

❌ 不建议的写法:每次组件渲染时,都会从本地存储读取主题。

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

✅ 建议的写法:仅在组件首次运行时执行函数,即从本地读取主题。

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

11. 使用 useImmeruseImmerReducer 简化状态更新

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

在状态是复杂对象时,更新状态就比较麻烦。

这时 useImmeruseImmerReducer 提供了一种更简单的实现方案。

❌ 不建议的写法:需要自己创建新的状态值,更新时还要注意保留其他字段。

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

  // ...
}

12. 在使用 useEffect 时记得清除副作用

当你的 useEffect 中有副作用时,记得返回一个函数用来清除副作用,不处理副作用的话可能会导致潜在的 bug 和内存泄漏。

❌ 不建议的写法:未清除定时器,这意味着即使组件被卸载后它仍会继续运行。

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

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

✅ 建议的写法:组件卸载后定时器就不再执行。

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

13. 优先使用函数而不是自定义 Hook

当可复用的逻辑可以使用函数实现时,就不要将逻辑放在 Hook 中。

函数对比 Hook 有以下优点:

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

❌ 不建议的写法:useLocale 是不必要的,因为它不涉及状态管理。

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

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

✅ 建议的写法:创建一个 getLocale 函数替代 Hook。

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

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

14. 使用 Simple React Snippets 代码片段插件来提高你的工作效率

创建一个新的 React 组件可能很繁琐,在 VSCode 中使用 Simple React Snippets 提高效率。

code-react

总结

如果你有好的建议,欢迎留言一起讨论。