Re: 新手让网页 act 起来: Day11 - React Hooks 之 useState (2)

在上一篇介绍了使用 hooks 的基本规则,以及 useState 的使用方式。今天来针对 initialValue 以及 setState 再做了解。

Pass the function

写过一阵子的 JavaScript 的话,应该都会知道,我们能够将 function 当作参数传递进另一个 function 中。而 useState 的 initialValue 其实除了可以传入基本型别以外,我们也能够传入一个 function , 但必须回传一个值作为初始值,如下

const [state, setState] = useState(() => 'initialValue')

在 React 中这样设定初始值的方式称为 Lazy initialization 。

为什麽需要 Lazy initialization ?

先来看看昨天的 Counter 元件的练习

const Counter = () => {
  const [count, setCount] = React.useState(0)
  //使用 useState 建立 Count 状态,并将 0 设为初始状态

  const increaseHandler = () => {   
    setCount(count + 1)
    // 点击 plus 按钮,将 count 加 1 ,并使用 setCount 改变 Count 状态
  }

  const decreaseHandler = () => {
    if (count === 0) return

    setCount(count - 1)
    // 点击 minus 按钮,将 count 减 1 ,并使用 setCount 改变 Count 状态
  }

  return (
    <div className="container">
      <button className="minus" onClick={decreaseHandler}>-</button>
      <span className="number">{count}</span>
      <button className="plus" onClick={increaseHandler}>+</button>
    </div>
  )
};

ReactDOM.render(<Counter />, document.getElementById('root'))

回想一下,当我们使用按下按钮呼叫 setState 更新状态的时候都会触发 Counter 元件 re-render 。 换句话说,每次 state 更新的时候,都会重新呼叫一次 Counter function,所以 Counter 里面的变数或是函式都会重新被定义及呼叫。

假设今天有个情况是我们的 initialValue 必须经过资料格式的转换或计算等等。我们可能会这样子写:

function transformData(rawData){
   //  将 rawData 进行复杂的换算...
   return initialValue
}
const App = (props) => {
  const initialValue = transformData(props)
  const [state, setState] = useState(initialValue)
}

但这样就有个问题,同上面所提到的,每次 state 更新的时候,变数或是函式都会重新被定义及呼叫。这个 transformData 每一次都会再被呼叫,但是我们只希望function 中的运算只在第一次的时候执行并给我们 initialValue 而已。

这个时候我们就需要用到 Lazy initialization 了,我们就可以改写成这样

function transformData(rawData){
  return () => {
   //  将 rawData 进行复杂的换算...
   return initialValue
  }
}
const App = (props) => {
  const [state, setState] = useState(transformData(props))
}

透过 Lazy initialization 比较复杂或是昂贵的计算就只会在第一次 render 的时候执行,之後当 state 改变的时候,就不会做多余的计算了。

setState

上面提到了 initialValue 可以是个 function ,其实 setState 我们也可以传入一个 function 给它,写起来如下

setCount((previousCount) => { return previousCount + 1 }) 
// 传入一个 function 会接收当下的状态,回传新的值做为新的状态

那什麽时候需要这样子写,跟昨天介绍的直接传入一个基本型别到差异是什麽呢?

我们将上面的 Counter 改写,把 increaseHandler 中的 setCount 用 setTimeout 包起来如下

const Counter = () => {
  const [count, setCount] = React.useState(0)

  const increaseHandler = () => {
    setTimeout(() => {
      setCount(count + 1)
    }, 1000)
    // 点击 plus 按钮,将 count 加 1 ,并使用 setCount 改变 Count 状态
  }

这个时候我们在画面连续按五下 + 的时候就会发现 count 只有加 1 次

会造成这样的原因主要是,这是一个 stale closure 的问题,有兴趣可以看看这篇。总之在这一秒内,每一次在 setCount 的时候拿到的 count 都是 0,所以画面最後也只会显示 1 。

如果说我们今天期望显示的结果是,我们按下几次按钮就会纪录次数的话,我们就必须使用传入 function 的解法,才能解决上面 stale closure 的问题。

const Counter = () => {
  const [count, setCount] = React.useState(0)

  const increaseHandler = () => {
    setTimeout(() => {
      setCount(previousCount => previousCount + 1)
    }, 1000)
    // 点击 plus 按钮,将 count 加 1 ,并使用 setCount 改变 Count 状态
  }

当我们改写後再看看功能,我们能确保每一次执行 setCount , previousCount 都会取得最新的值,并且做更新。功能也会如期显示正确的次数了。

所以当我们想要针对前一次的 state 做更新的时候都会建议使用传入 function 的方式来确保我们不会遇到 stale closure 的问题。

以上就是今天关於 useState 的补充介绍,有问题欢迎在下方留言!

该文章同步发布於:我的部落格


<<:  JavaScript入门 Day21_if判断1

>>:  Day11 - 使用 Rails Routes 识别用户输入

30天零负担轻松学会制作APP介面及设计【DAY 08】

大家好,我是YIYI,今天我要制作APP登入後的介面。 先制作登入後介面的原因 因为我画的封面一直达...

Day-09 版面配置Layout

本篇内容想介绍<activity_main.xml>配置档,版面配置档让使用者可在这个环...

容器化基本概念

容器映像(container image)是开发人员创建并注册的程序包(package),包含在容...

Day 30 | 很像 Vue 的 AlpineJS(五): 与 Livewire 共享资料

前面三篇关於 AlpineJs 的文章都是在控制前端的页面而跟 Livewire 比较无关,那今天就...

【Day 29】Class

在铁人赛中,最後一个要来介绍的章节是 Class!我觉得这是所有章节中最难懂、最抽象的部分,真的是需...