优秀的编程知识分享平台

网站首页 > 技术文章 正文

使用 React useState Hooks 六个误区

nanyue 2024-08-23 18:32:43 技术文章 6 ℃

家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

本文部分内容来自于 Akshay Kaushik 发表的文章《Common Mistakes When Using React useState Hook (With Code Examples)》,文章链接已经在文末给出。

1.什么是 React Hooks

React hooks 是 React 16.8 中添加的函数,其允许函数组件保存状态、管理生命周期事件,并利用以前仅在基于类的组件中可用的其他 React 功能。 Hooks 使得以更清晰、更直接的方式开发可重用和模块化代码变得更加容易。

比如下面代码使用 Hooks 来更新 state 状态:

import React, { useState } from 'react';
const Counter = () => {
	const [count, setCount] = useState(0);
	const incrementCount = () => {
		setCount(count + 1);
	};
	return (
		<div>
			<h1>Count: {count}</h1>
			<button onClick={incrementCount}>
				Increment
			</button>
		</div>
	);
};
export default Counter;


使用 Hook 相对于 Class 有以下好处:

  • 可读性和简单性:与基于类的组件相比,Hooks 提供了更简单和更短的语法。 使用 Hooks 可以创建函数组件而无需类,从而使代码更易于阅读和理解。 同时,Hooks 通过消除 this 关键字、构造函数和生命周期函数等生成更好的代码。
  • 代码可重用性:Hooks 通过允许跨多个组件提取和重用逻辑来提高代码重用性。 在不使用高阶组件或渲染属性的情况下,可以使用自定义 Hooks 隔离有状态行为并与之交互。
  • 更有效的管理:通过使用 Hooks,开发者可以更精确地分组和管理相关代码。 要单独处理不同问题,例如:状态管理、副作用和上下文可以在单个组件内使用各种 Hooks。
  • 防止与类相关的问题在:React 组件中使用类可能会导致函数绑定、生命周期方法的使用和性能改进方面出现不清楚的情况。 这些问题可以通过 Hooks 解决,它提供了一种更简单的处理状态和效果的方法。
  • 增强性能: Hooks 可以更轻松地提高性能, 利用 useCallback 和 useMemo 等 Hooks 可以缓存函数和值从而减少额外渲染迭代的需要并增强组件性能。

本文重点介绍 useState 这个最经常用到的最经典的 Hooks,谈谈 useState 的常见误区。

2.useState 常见误区

2.1 不理解 useState 的工作原理

useState Hooks 返回一个包含 2 个元素的数组,第一个元素是当前状态值,第二个元素是设置状态的函数。

重要的是要了解 useState 不会自动合并对象状态更新而是完全覆盖状态。

import React, {useState} from 'react';
function MyComponent() {
  const [state, setState] = useState({
    name: 'John',
    age: 20
  });
  const handleUpdate = () => {
    // 这样会覆盖已经存在的 state
    setState({
      name: 'Sam'
    });
  }

  return (
    <div>
      {/* 调用 handleUpdate 后会渲染'Sam' 而不是'John'*/}
      <h1>{state.name}</h1>
      <h2>{state.age}</h2>
      <button onClick={handleUpdate}>Update Name</button>
    </div>
  );
}

要正确合并状态更新,开发者可以将设置函数传递给 setState:

setState(prevState => {
    // prevState 是初始状态
  return {...prevState, name: 'Sam'}
});

2.2 内联传递函数

将内联函数传递给事件处理程序或 Effect 很方便,但这样做会破坏 state 更新状态:

import {useState, useEffect} from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    // 闭包 Closures 阻止代码更新
    document.addEventListener('click', () => setCount(count + 1));
  }, []);

  return <h1>{count}</h1>;
}

由于函数闭包是在每次渲染时创建,因此 count 永远不会正确更新。 所以,建议仅当函数不更新状态时才传递内联函数,也可以在外部或 useCallback() 中声明:

// 外部声明
function increment() {
  setCount(count + 1);
}

//  useCallback() 中声明
const increment = useCallback(() => {
  setCount(count + 1);
}, []);

注意:React useCallback Hooks 是一个回调,接受要优化的组件和回调变量。 JavaScript 会记住该变量,并在每次渲染时创建它以保持不变 ,从而消除不必要地重新计算值的需要。

2.3 没有正确处理数组和对象

另一个错误是直接改变状态数组或对象:

const [tags, setTags] = useState(['react', 'javascript']);

const addTag = () => {
  // 直接更新已经存在的数组
  tags.push('nodejs');
}

return <div>{tags}</div>

由于 tags 数组是直接修改的,React 无法检测到这种变化,也不会正确触发重新渲染,可以在传递给 setter 函数之前创建数组、对象的新副本:

const addTag = () => {
  setTags([...tags, 'nodejs']);
}

这里的 spread 语法 [...array] 是浅复制数组,从而可以让 React 检测到变化。关于获取对象副本的更深入的了解可以阅读我的另外一篇文章《 JS 克隆对象八种技术,为何少不了 StructuredClone?

2.4 进行不必要的 useState 调用

无需将每个字段或数据点分离到其自己的状态 hooks 中,这样做可能会不必要地损害性能:

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  // 注意:这里 firstName、lastName没必要单独放在一个Hooks

  return (
    <>
      <input
        value={firstName}
        onChange={(e) => setFirstName(e.target.value)}
      />

      <input
        value={lastName}
        onChange={(e) => setLastName(e.target.value)}
      />
    </>
  );
}

相反,开发者可以将相关 state 分组为单个对象:

function Form() {
  // 注意:这里将多个 hooks 作为一个对象
  const [fields, setFields] = useState({
    firstName: '',
    lastName: ''
  })
  // 一次调用更新所有字段
  const handleChange = (e) => {
    setFields({
      ...fields,
      [e.target.name]: e.target.value
    })
  }

  return (
    <>
      <input
        name="firstName"
        value={fields.firstName}
        onChange={handleChange}
      />
      <input
        name="lastName"
        value={fields.lastName}
        onChange={handleChange}
      />
    </>
  );
}

2.5 不正确的状态比较

处理对象或数组时,避免直接比较状态变化。

// 不正确,无法确定数组或者对象的变化
if (user !== newUser) {
  // ...

// 正确的示例
if (user.id !== newUser.id) {
  // ...
}

2.6 没有设置正确的初始状态

根据 props 初始化

可以根据 props 初始化而不是硬编码值,比如下面的示例:

// 硬编码
const [count, setCount] = useState(0);

// 根据 props 初始化 state
const [count, setCount] = useState(props.initialCount);

处理卸载的数据

可以在 useEffect 中获取数据,设置加载状态:

const [user, setUser] = useState(null);

useEffect(() => {
  fetchUser().then(user => setUser(user))
}, []);

// 渲染之前检查是否为 null
return <div>{user ? user.name : 'Loading...'}</div>

这可以避免渲染 undefined 状态时出现错误。

指定 state 的数据类型

指定 state 的类型有助于避免出现问题:

const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<{name: string} | null>(null);
// 使用 typescript 指定类型

以上示例可确保防止将无效的更新状态值传递给 setter 函数。

3.本文总结

本文主要和大家介绍使用 useState Hooks 的六个常见疑问。因为篇幅问题,关于 useState Hooks 只是做了一个简短的介绍,但是文末的参考资料以及个人主页提供了大量优秀文档以供学习,如果有兴趣可以自行阅读。如果大家有什么疑问欢迎在评论区留言。

参考资料

https://blog.akshaykaushik.eu.org/common-mistakes-when-using-react-usestate-hook

https://www.geeksforgeeks.org/why-to-use-react-hooks-instead-of-classes-in-reactjs/

https://daveceddia.com/usestate-hook-examples/

https://www.youtube.com/watch?v=P24EuJlVKb0

最近发表
标签列表