React中useState值为对象时改变值不渲染问题

 更新时间:2023年02月13日 08:38:41   作者:luotouzi  
这篇文章主要介绍了React中useState值为对象时改变值不渲染问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

useState值为对象时改变值不渲染

问题

修改State并重新setState(arr)后,值改变,但并未重新渲染

const [arr, setArr] = useState([])
arr.push(1)
setArr(arr)

原因

React中默认浅监听,当State值为对象时,栈中存的是对象的引用(地址),setState改变的是堆中的数据

所以此时 setArr(arr) 后,栈中的地址还是原地址,React浅监听到地址没变,故会认为State并未改变,故没有重渲染页面

解决

思路:将栈中原arr所指向的地址改变即可

1)直接setState(要修改的值)

const [arr, setArr] = useState([])
setArr(1)

2)新创建一个数组newArr,将原数组的值赋值给新数组,并setState(newArr)

const [arr, setArr] = useState([])
const newArr = arr.slice(1)
setArr(newArr)

3)利用ES6的拓展符…

const [arr, setArr] = useState([])
setArr([...arr])

useState用法指南

useState

const [state, setState] = useState(initialState);

返回一个 state,以及更新 state 的函数。

在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

setState(newState);

在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。

函数式更新

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了 setState 的两种用法:

function Counter() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(count + 1)
  }
  function handleClickFn() {
    setCount((prevCount) => {
      return prevCount + 1
    })
  }
  return (
    <>
      Count: {count}
      <button onClick={handleClick}>+</button>
      <button onClick={handleClickFn}>+</button>
    </>
  );
}

两种方式的区别

注意上面的代码,handleClick和handleClickFn一个是通过一个新的 state 值更新,一个是通过函数式更新返回新的 state。现在这两种写法没有任何区别,但是如果是异步更新的话,那你就要注意了,他们是有区别的,来看下面例子:

function Counter() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setTimeout(() => {
      setCount(count + 1)
    }, 3000);
  }
  function handleClickFn() {
    setTimeout(() => {
      setCount((prevCount) => {
        return prevCount + 1
      })
    }, 3000);
  }
  return (
    <>
      Count: {count}
      <button onClick={handleClick}>+</button>
      <button onClick={handleClickFn}>+</button>
    </>
  );
}

当我设置为异步更新,点击按钮延迟到3s之后去调用setCount函数,当我快速点击按钮时,也就是说在3s多次去触发更新,但是只有一次生效,因为 count 的值是没有变化的。

当使用函数式更新 state 的时候,这种问题就没有了,因为它可以获取之前的 state 值,也就是代码中的 prevCount 每次都是最新的值。

其实这个特点和类组件中 setState 类似,可以接收一个新的 state 值更新,也可以函数式更新。如果新的 state 需要通过使用先前的 state 计算得出,那么就要使用函数式更新。

因为setState更新可能是异步,当你在事件绑定中操作 state 的时候,setState更新就是异步的。

class Counter extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  handleClick = () => {
    this.setState({ count: this.state.count + 1 })
    this.setState({ count: this.state.count + 1 })
    // 这样写只会加1
  }
  handleClickFn = () => {
    this.setState((prevState) => {
      return { count: prevState.count + 1 }
    })
    this.setState((prevState) => {
      return { count: prevState.count + 1 }
    })
  }
  render() {
    return (
      <>
        Count: {this.state.count}
        <button onClick={this.handleClick}>+</button>
        <button onClick={this.handleClickFn}>+</button>
      </>
    );
  }
}

当你在定时器中操作 state 的时候,而 setState 更新就是同步的。

class Counter extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  handleClick = () => {
    setTimeout(() => {
      this.setState({ count: this.state.count + 1 })
      this.setState({ count: this.state.count + 1 })
      // 这样写是正常的,两次setState最后是加2
    }, 3000);
  }
  handleClickFn = () => {
    this.setState((prevState) => {
      return { count: prevState.count + 1 }
    })
    this.setState((prevState) => {
      return { count: prevState.count + 1 }
    })
  }
  render() {
    return (
      <>
        Count: {this.state.count}
        <button onClick={this.handleClick}>+</button>
        <button onClick={this.handleClickFn}>+</button>
      </>
    );
  }
}

注意这里的同步和异步指的是 setState 函数。因为涉及到 state 的状态合并,react 认为当你在事件绑定中操作 state 是非常频繁的,所以为了节约性能 react 会把多次 setState 进行合并为一次,最后在一次性的更新 state,而定时器里面操作 state 是不会把多次合并为一次更新的。

注意:与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。

性能优化

React 使用 Object.is 比较算法来比较 state。

在 React 应用中,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。

function Child({ onButtonClick, data }) {
  console.log('Child Render')
  return (
    <button onClick={onButtonClick}>{data.number}</button>
  )
}

function App() {
  const [number, setNumber] = useState(0)
  const [name, setName] = useState('hello') // 表单的值
  const addClick = () => setNumber(number + 1)
  const data = { number }
  return (
    <div>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
      <Child onButtonClick={addClick} data={data} />
    </div>
  )
}

如要避免不必要的子组件的重渲染,使用 React.memo 仅检查 props 变更。 默认情况下其只会对复杂对象做浅层对比。所有使用 memo 优化后的代码如下:

function Child({ onButtonClick, data }) {
  console.log('Child Render')
  return (
    <button onClick={onButtonClick}>{data.number}</button>
  )
}
Child = memo(Child); // 在这里优化了
function App() {
  const [number, setNumber] = useState(0)
  const [name, setName] = useState('hello') // 表单的值
  const addClick = () => setNumber(number + 1)
  const data = { number }
  return (
    <div>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
      <Child onButtonClick={addClick} data={data} />
    </div>
  )
}

你以为代码中的Child = memo(Child);已经优化了吗,然而并没有,当你在更改了父组件的状态,子组件依然会重新渲染,因为这关系到了React是如何浅层比较的,在子组件中onButtonClick 和 data 都是引用类型,所以他们是始终都不相等的,也就是[]===[]这样比较时始终返回false,在基本数据类型比较时memo才会起作用。

关于如何解决这个问题,我们就要使用两个新的API,useMemo和useCallback的Hook。下面是经过优化之后的代码。

function Child({ onButtonClick, data }) {
  console.log('Child Render')
  return (
    <button onClick={onButtonClick}>{data.number}</button>
  )
}

Child = memo(Child)

function App() {
  const [number, setNumber] = useState(0)
  const [name, setName] = useState('hello') // 表单的值
  const addClick = useCallback(() => setNumber(number + 1), [number])
  const data = useMemo(() => ({ number }), [number])
  return (
    <div>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
      <Child onButtonClick={addClick} data={data} />
    </div>
  )
}

export default App;

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

useCallback返回一个 memoized 回调函数。useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useCallback 和 useMemo 参数相同,第一个参数是函数,第二个参数是依赖项的数组。主要区别是 React.useMemo 将调用 fn 函数并返回其结果,而 React.useCallback 将返回 fn 函数而不调用它。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • react源码层分析协调与调度

    react源码层分析协调与调度

    本文主要介绍了深入理解React协调与调度(Scheduler)原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-10-10
  • react清空ant.design中表单内容的方法实现

    react清空ant.design中表单内容的方法实现

    本文主要介绍了react清空ant.design中表单内容的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-12-12
  • React SSG实现Demo详解

    React SSG实现Demo详解

    这篇文章主要为大家介绍了React SSG实现Demo详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪<BR>
    2023-07-07
  • reactjs学习解决unknown at rule @tailwind css问题

    reactjs学习解决unknown at rule @tailwind css

    这篇文章主要介绍了reactjs学习解决unknown at rule @tailwind css问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • 详解使用create-react-app快速构建React开发环境

    详解使用create-react-app快速构建React开发环境

    这篇文章主要介绍了详解使用create-react-app快速构建React开发环境,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-05-05
  • useEffect中不能使用async原理详解

    useEffect中不能使用async原理详解

    这篇文章主要为大家介绍了useEffect中为什么不能使用async的原理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • React中完整实例讲解Recoil状态管理库的使用

    React中完整实例讲解Recoil状态管理库的使用

    这篇文章主要介绍了React中Recoil状态管理库的使用,Recoil的产生源于Facebook内部一个可视化数据分析相关的应用,在使用React的实现的过程中,因为现有状态管理工具不能很好的满足应用的需求,因此催生出了Recoil,对Recoil感兴趣可以参考下文
    2023-05-05
  • 浅谈React组件props默认值的设置

    浅谈React组件props默认值的设置

    本文主要介绍了浅谈React组件props默认值的设置,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • react Input组件Compositionstart和Compositionend事件

    react Input组件Compositionstart和Compositionend事件

    这篇文章主要为大家介绍了Compositionstart和Compositionend事件之于react组件库Input组件的坑解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • React 中使用 RxJS 优化数据流的处理方案

    React 中使用 RxJS 优化数据流的处理方案

    这篇文章主要为大家介绍了React 中使用 RxJS 优化数据流的处理方案示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02

最新评论