超性感的React Hooks(三):useState

简介: 我们可以在父组件中定义state,并通过props的方式传递到子组件。如果子组件想要修改父组件传递而来的状态,则只能给父组件发送消息,由父组件改变,再重新传递给子组件。在React中,state与props的改变,都会引发组件重新渲染。如果是父组件的变化,则父组件下所有子组件都会重新渲染。

微信图片_20220509195702.gif



今天分享的内容,是React Hooks第一个api,useState,阅读本文需要有具备最基础的React知识。


单向数据流


和angular双向绑定不同,React采用自上而下单向数据流的方式,管理自身的数据与状态。在单向数据流中,数据只能由父组件触发,向下传递到子组件。


微信图片_20220509195714.jpg


我们可以在父组件中定义state,并通过props的方式传递到子组件。如果子组件想要修改父组件传递而来的状态,则只能给父组件发送消息,由父组件改变,再重新传递给子组件。


在React中,state与props的改变,都会引发组件重新渲染。如果是父组件的变化,则父组件下所有子组件都会重新渲染。


在class组件中,组件重新渲染,是执行render方法。


而在函数式组件中,是整个函数重新执行。


函数式组件


函数式组件与普通的函数几乎完全一样。只不过函数执行完毕时,返回的是一个JSX结构。


function Hello() {
  return <div>hello world.</div>
}


函数式组件非常简单,也正因如此,一些特性常常被忽略,而这些特性,是掌握React Hooks的关键。


1. 函数式组件接收props作为自己的参数


import React from 'react';
interface Props {
  name: string,
  age: number
}
function Demo({ name, age }: Props) {
  return [
    <div>name: {name}</div>,
    <div>age: {age}</div>
  ]
}
export default Demo;


2. props的每次变动,组件都会重新渲染一次,函数重新执行


3. 没有this。那么也就意味着,之前在class中由于this带来的困扰就自然消失了。


Hooks


Hooks并不是神秘,它就是函数式组件。更准确的概述是:有状态的函数式组件。


useState


每次渲染,函数都会重新执行。我们知道,每当函数执行完毕,所有的内存都会被释放掉。因此想让函数式组件拥有内部状态,并不是一件理所当然的事情。


当然,也不是完全没有办法,useState就是帮助我们做这个事情。


从上一章再谈闭包中我们知道,useState利用闭包,在函数内部创建一个当前函数组件的状态。并提供一个修改该状态的方法。


我们从react中引入useState


import { useState } from 'react';


利用数组解构的方式得到一个状态与修改状态的方法。


// 利用数组解构的方式接收状态名及其设置方法
// 传入0作为状态 counter的初始值
const [counter, setCounter] = useState(0);


每当setCounter执行,就会改变counter的值。


基于这个知识点,我们可以创建一个最简单的,有内部状态的函数式组件。


import React, { useState } from 'react';
export default function Counter() {
  const [counter, setCounter] = useState(0);
  return [
    <div key="a">{counter}</div>,
    <button key="b" onClick={() => setCounter(counter + 1)}>
      点击+1
    </button>
  ]
}


利用useState声明状态,每当点击时,setCounter执行,counter递增。


需要注意的是,setCounter接收的值可以是任意类型,无论是什么类型,每次赋值,counter得到的,都是新传入setCounter中的值。


举个例子,如果counter是一个引用类型。


// counter默认值为 { a: 1, b: 2 }
const [counter, setCounter] = useState({ a: 1, b: 2 });
// 此时counter的值被改为了 { b: 4 }, 而不是 { a: 1, b: 4 }
setCounter({ b: 4 });
// 如果想要得到 { a: 1, b: 4 }的结果,就必须这样
setCounter({ ...counter, b: 4 });


那么一个思考题:用下面的例子修改状态,会让组件重新渲染吗?


const [counter, setCounter] = useState({ a: 1, b: 2 });
// 修改counter的值
counter.b = 4;
setCounter(counter);


useState接收一个值作为当前定义的state的初始值。并且初始操作只有组件首次渲染才会执行。


// 首次执行,counter初始值为10
// 再次执行,因为在后面因为某种操作改变了counter,则获取到的便不再是初始值,而是闭包中的缓存值
const [counter, setCounter] = useState(10);
setCounter(20);


如果初始值需要通过较为复杂的计算得出,则可以传入一个函数作为参数,函数返回值为初始值。该函数也只会在组件首次渲染时执行一次。


const a = 10;
const b = 20
// 初始值为a、b计算之和
const [counter, setCounter] = useState(() => {
  return a + b;
})


如果是在typescript中使用,我们可以用如下的方式声明状态的类型。


const [counter, setCounter] = useState<number>(0);


但是通常情况下,基础数据类型typescript能够很容易推导出来,因此我们不需要专门设置,只有在相对复杂的场景下才会需要专门声明。


// 能根据 0 推导为number类型
const [counter, setCounter] = useState(0);
// 能根据 false 推导为 boolean 类型
const [visible, setVisible] = useState(false);
// 能根据 [] 推导为 any[] 类型,因此此时还需要专门声明any为何物
const [arr, setArr] = useState<number[]>([]);


实践


接下来,我们完成一个稍微复杂一点的例子。文章头部的动态图还有印象吗?


微信图片_20220509195717.gif

多个滑动条控制div元素的不同属性,如果使用useState来实现,应该怎么做?


代码如下:


import React, { useState } from 'react';
import { Slider } from 'antd-mobile';
import './index.scss';
interface Color {
  r: number,
  g: number,
  b: number
}
export default function Rectangle() {
  const [height, setHeight] = useState(10);
  const [width, setWidth] = useState(10);
  const [color, setColor] = useState<Color>({ r: 0, g: 0, b: 0 });
  const [radius, setRadius] = useState<number>(0);
  const style = {
    height: `${height}px`,
    width: `${width}px`,
    backgroundColor: `rgb(${color.r}, ${color.g}, ${color.b})`,
    borderRadius: `${radius}px`
  }
  return (
    <div className="container">
      <p>height:</p>
      <Slider
        max={300}
        min={10}
        onChange={(n) => setHeight(n || 0)}
      />
      <p>width:</p>
      <Slider
        max={300}
        min={10}
        onChange={(n) => setWidth(n || 0)}
      />
      <p>color: R:</p>
      <Slider
        max={255}
        min={0}
        onChange={(n = 0) => setColor({ ...color, r: n })}
      />
      <p>color: G:</p>
      <Slider
        max={255}
        min={0}
        onChange={(n = 0) => setColor({ ...color, g: n })}
      />
      <p>color: B:</p>
      <Slider
        max={255}
        min={0}
        onChange={(n = 0) => setColor({ ...color, b: n })}
      />
      <p>Radius:</p>
      <Slider
        max={150}
        min={0}
        onChange={(n = 0) => setRadius(n)}
      />
      <div className="reatangle" style={style} />
    </div>
  )
}


仔细体会一下,代码是不是比想象中更简单?需要注意观察的地方是,当状态被定义为引用数据类型时,例子中是如何修改的。


原则上来说,useState的应用知识差不多都聊完了。不过,还能聊点高级的。


无论是在class中,还是hooks中,state的改变,都是异步的。


如果对事件循环机制了解比较深刻,那么异步状态潜藏的危机就很容易被意识到并解决它。如果不了解,可以翻阅我的JS基础进阶。详解事件循环[1]


状态异步,也就意味着,当你想要在setCounter之后立即去使用它时,你无法拿到状态最新的值,而之后到下一个事件循环周期执行时,状态才是最新的值。


const [counter, setCounter] = useState(10);
setCounter(20);
console.log(counter);  // 此时counter的值,并不是20,而是10

实践中有许多的错误使用,因为异步问题而出现bug。


例如我们想要用一个接口,去请求一堆数据,而这个接口接收多个参数。


当改变各种过滤条件,那么就势必会改变传入的参数,并在参数改变时,立即重新去请求一次数据。


利用hooks,会很自然的想到使用如下的方式。


import React, { useState } from 'react';
interface ListItem {
  name: string,
  id: number,
  thumb: string
}
// 一堆各种参数
interface Param {
  current?: number,
  pageSize?: number,
  name?: string,
  id?: number,
  time?: Date
}
export default function AsyncDemo() {
  const [listData, setListData] = useState<ListItem[]>([]);
  // 定义一个状态缓存参数,确保每次改变后都能缓存完整的参数
  const [param, setParam] = useState<Param>({});
  function fetchListData() {
    // @ts-ignore
    listApi(param).then(res => {
      setListData(res.data);
    })
  }
  function searchByName(name: string) {
    setParam({ ...param, name });
    // 改变param之后立即执行请求数据的代码
    // 这里的问题是,因为异步的原因,param并不会马上发生变化,
    // 此时直接发送请求无法拿到最新的参数
    fetchListData();
  }
  return [
    <div>data list</div>,
    <button onClick={() => searchByName('Jone')}>search by name</button>
  ]
}


这是一个不完整的示例。需要大家在阅读时结合自身开发经验去意会。


关键的代码在于searchByName方法。当使用setParam改变了param之后,立即去请求数据,在当前事件循环周期,param并没有改变。请求的结果,自然无法达到预期。

如何解决呢?


首先我们要考虑的一个问题是,什么样的变量适合使用useState去定义?


当然是能够直接影响DOM的变量,这样我们才会将其称之为状态。


因此param这个变量对于DOM而言没有影响,此时将他定义为一个异步变量并不明智。好的方式是将其定义为一个同步变量。


export default function AsyncDemo() {
  const [listData, setListData] = useState<ListItem[]>([]);
  // 定义为同步变量
  let param: Param = {}
  function fetchListData() {
    // @ts-ignore
    listApi(param).then(res => {
      setListData(res.data);
    })
  }
  function searchByName(name: string) {
    param = { ...param, name };
    fetchListData();
  }
  return [
    <div>data list</div>,
    <button onClick={() => searchByName('Jone')}>search by name</button>
  ]
}


不过,等一下,这样好像也有一点问题


还记得函数式组件的特性吗?每次状态改变,函数都会重新执行一次,那么此时param也就被重置了。状态无法得到缓存。


那么怎么办?


好吧,利用闭包。上一篇文章我们知道,每一个模块,都是一个执行上下文。因此,我们只要在这个模块中定义一个变量,并且在函数组件中访问,那么闭包就有了。


因此,将变量定义到函数的外面。如下


// 定义为同步变量
let param: Param = {}
export default function AsyncDemo() {
  const [listData, setListData] = useState<ListItem[]>([]);
  function fetchListData() {
    // @ts-ignore
    listApi(param).then(res => {
      setListData(res.data);
    })
  }
  function searchByName(name: string) {
    param = { ...param, name };
    fetchListData();
  }
  return [
    <div>data list</div>,
    <button onClick={() => searchByName('Jone')}>search by name</button>
  ]
}


这样似乎能够解决一些问题。


但也不是完全没有隐患,因为善后工作还没有做,因为这个闭包中的变量,即使在组件被销毁了,它的值还会存在。当新的组件实例被渲染,param就无法得到初始值了。因此这样的方式,我们必须在每一个组件被销毁时,做好善后工作。


那还有没有更好的方式呢?答案就藏在我们上面的知识点中。


我们知道useState其实也是利用闭包缓存了状态,并且即使函数多次执行,也只会初始化一次。之前的问题在于我们使用了setParam去改变它的值,如果我们换一种思路呢?仔细体会一下代码就知道了。


export default function AsyncDemo() {
  const [param] = useState<Param>({});
  const [listData, setListData] = useState<ListItem[]>([]);
  function fetchListData() {
    // @ts-ignore
    listApi(param).then(res => {
      setListData(res.data);
    })
  }
  function searchByName(name: string) {
    param.name = name;
    fetchListData();
  }
  return [
    <div>data list</div>,
    <button onClick={() => searchByName('Jone')}>search by name</button>
  ]
}


没有想到吧,useState还能这么用!


OK,useState相关的应用知识就基本分享完了,接下来的文章聊聊useEffect。

今天帮助一位同学优化了hooks实践代码,同样的功能,优化结果代码量减少了40行左右!!快到群里来!


微信图片_20220509195720.jpg

相关文章
|
21天前
|
前端开发 JavaScript API
探究 React Hooks:如何利用全新 API 优化组件逻辑复用与状态管理
本文深入探讨React Hooks的使用方法,通过全新API优化组件逻辑复用和状态管理,提升开发效率和代码可维护性。
|
24天前
|
前端开发
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
21 3
|
29天前
|
前端开发 JavaScript
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
|
1月前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
34 2
|
2月前
|
前端开发 开发者
React 提供的其他重要 Hooks
【10月更文挑战第20天】React 提供了一系列强大的 Hooks,除了 `useRef` 之外,还有许多其他重要的 Hooks,它们共同构成了函数式组件开发的基础。
37 6
|
18天前
|
前端开发 JavaScript
React Hooks 深入解析
React Hooks 深入解析
20 0
|
18天前
|
前端开发
React Hooks:从基础到进阶的深入理解
React Hooks:从基础到进阶的深入理解
25 0
|
21天前
|
缓存 前端开发 开发者
深入理解React Hooks,打造高效响应式UI
深入理解React Hooks,打造高效响应式UI
29 0
|
2月前
|
前端开发 JavaScript 开发者
React Hooks
10月更文挑战第13天
37 1
|
缓存 前端开发 JavaScript
React Hooks使用心得!真香
React Hooks使用心得!真香
React Hooks使用心得!真香