React Hooks 快速入门:从一个数据请求开始

Posted 凯小默:树上的男爵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Hooks 快速入门:从一个数据请求开始相关的知识,希望对你有一定的参考价值。

前言

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

早期的写法以 Class 类组件为主,附带一些纯用于展示的函数组件,但是函数组件是不能控制自身的状态的。Hooks 写法引入之后,函数组件的写法开始流行起来。

函数组件引入了多种钩子函数如 useEffect、useState、useRef、useCallback、useMemo、useReducer 等等,通过这些钩子函数来管理函数组件的各自状态。

下面就通过一些例子来简单了解部分 hook。

1、新建 hooks-demo 工程项目

我的 node 版本是 v12.13.0,npm 版本是 6.12.0


执行下面命令:

npm init @vitejs/app hooks-demo --template react

执行完之后,安装依赖运行

npm install
npm run dev

2、基础 Hook

useState

函数内声明变量,可以通过 useState 方法,它接受一个参数,可以为默认值,也可以为一个函数。

修改 App.jsx,改成下面代码:

import React,  useState  from 'react'

function App() 
  const [data, setData] = useState([1, 2, 3, 4, 5])
  return (
    <div className="kaimo-app">
      
        data.map((item, index) => <div key=index>kaimo:item</div>)
      
    </div>
  )


export default App

页面显示结果如下:

useEffect

useEffect() 的作用就是指定一个副效应函数,组件每渲染一次,该函数就自动执行一次。组件首次在网页 DOM 加载后,副效应函数也会执行。

无需清除的 effect

在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。

需要清除的 effect

还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!

useEffect 的用法

我们先通过 useEffect 副作用,模拟请求一个接口数据。

import React,  useState, useEffect  from 'react'

// 模拟数据接口,2 秒后返回数据
const getDataList = () => 
  return new Promise((resolve, reject) => 
    setTimeout(() => 
      resolve([6, 7, 8, 9, 10])
    , 2000)
  )


function App() 
  const [data, setData] = useState([1, 2, 3, 4, 5])

  useEffect(() => 
    (async() => 
      const kaimo = await getDataList()
      console.log('useEffect:kaimo', kaimo)
      setData(kaimo)
    )()
  )

  return (
    <div className="kaimo-app">
      
        data.map((item, index) => <div key=index>kaimo:item</div>)
      
    </div>
  )


export default App

页面显示结果:

我们发现函数组件默认进来之后,会执行 useEffect 中的回调函数,但是当 setData 执行之后,App 组件再次刷新,刷新之后会再次执行 useEffect 的回调函数,变成一个死循环了。

useEffect 的第二个参数

有时候,我们不希望 useEffect() 每次渲染都执行,这时可以使用它的第二个参数,使用一个数组指定副效应函数的依赖项,只有依赖项发生变化,才会重新渲染。

function Welcome(props) 
  useEffect(() => 
    document.title = `Hello, $props.name`;
  , [props.name]);
  return <h1>Hello, props.name</h1>;

上面例子中,useEffect() 的第二个参数是一个数组,指定了第一个参数(副效应函数)的依赖项(props.name)。只有该变量发生变化时,副效应函数才会执行。

如果第二个参数是一个空数组,就表明副效应参数没有任何依赖项。

因此,副效应函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。这很合理,由于副效应不依赖任何变量,所以那些变量无论怎么变,副效应函数的执行结果都不会改变,所以运行一次就够了。

我们把副作用的部分加上空数组试试:

useEffect(() => 
  (async() => 
    const kaimo = await getDataList()
    console.log('useEffect:kaimo', kaimo)
    setData(kaimo)
  )()
, [])

我们发现只会执行一次,不会再出现死循环了。

给请求一个 query 参数

下面我们给请求加一个 query 参数,代码如下:

import React,  useState, useEffect  from 'react'

// 模拟数据接口,2 秒后返回数据
const getDataList = (query) => 
  return new Promise((resolve, reject) => 
    setTimeout(() => 
      console.log('kaimo-query', query);
      resolve([6, 7, 8, 9, 10])
    , 2000)
  )


function App() 
  const [data, setData] = useState([1, 2, 3, 4, 5])
  const [query, setQuery] = useState('')

  useEffect(() => 
    (async() => 
      const kaimo = await getDataList(query)
      console.log('useEffect:kaimo', kaimo)
      setData(kaimo)
    )()
  , [query])

  return (
    <div className="kaimo-app">
      <input type="text" placeholder='请输入查询参数' onChange=e => setQuery(e.target.value) />
      
        data.map((item, index) => <div key=index>kaimo:item</div>)
      
    </div>
  )


export default App

然后我输入 1 改变 query 的值,副作用函数便会被执行,结果如下:

如果你的接口有查询参数,可以将参数设置在 useEffect 的第二个参数的数组值中,这样改变查询变量的时候,副作用便会再次触发执行,相应的函数也会重新带着最新的参数,获取接口数据。

useContext

const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext providercontext value 值。即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

下面修改 App.jsx 代码为:

import React,  useContext  from 'react'

const themes = 
  light: 
    foreground: "#fff",
    background: "orange"
  ,
  dark: 
    foreground: "#000",
    background: "green"
  
;

const ThemeContext = React.createContext(themes.light);

function ThemedButton() 
  const theme = useContext(ThemeContext);
  console.log('ThemedButton', theme)
  return (
    <button style= background: theme.background, color: theme.foreground >
      Kaimo Theme Button
    </button>
  );


function Toolbar() 
  return (
    <div>
      <ThemedButton />
    </div>
  );


function App() 
  return (
    <ThemeContext.Provider value=themes.dark>
      <Toolbar />
    </ThemeContext.Provider>
  );


export default App

我们发现按钮用的是 dark 的主题:

3、自定义 Hook

接下来,我们把上面的模拟接口请求抽离成一个自定义 hook,新建 useKaimoApi.jsx,在里面添加

import  useState, useEffect  from 'react'

// 模拟数据接口,2 秒后返回数据
const getDataList = (query) => 
  return new Promise((resolve, reject) => 
    setTimeout(() => 
      console.log('kaimo-query', query);
      resolve([6, 7, 8, 9, 10])
    , 2000)
  )


// 自定义 hook
const useKaimoApi = () => 
  const [data, setData] = useState([1, 2, 3, 4, 5])
  const [query, setQuery] = useState('')

  useEffect(() => 
    (async() => 
      const kaimo = await getDataList(query)
      console.log('useEffect:kaimo', kaimo)
      setData(kaimo)
    )()
  , [query])

  return [data, setQuery]


export default useKaimoApi

App.jsx 中也修改一下:

import React from 'react'
import useKaimoApi from './useKaimoApi'

function App() 
  const [data, setQuery] = useKaimoApi()
  
  return (
    <div className="kaimo-app">
      <input type="text" placeholder='请输入查询参数' onChange=e => setQuery(e.target.value) />
      
        data.map((item, index) => <div key=index>kaimo:item</div>)
      
    </div>
  )


export default App

结果是一样的:

我们可通过自定义 Hook 的形式,把公共逻辑抽离出来复用,这也是之前 Class 类组件不能做到的。

4、额外的 Hook

useMemo

返回一个 memoized 值。

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

下面我们尝试修改一下 App.jsx 里的代码:

import React,  useState, useEffect  from 'react'

// 在内部新增一个子组件,子组件接收父组件传进来的一个对象,作为子组件的 useEffect 的第二个依赖参数。
function Child(data) 
  useEffect(() => 
    console.log('kaimo-child:查询条件', data)
  , [data])
  return <div className='kaimo-child'>子组件</div>


function App() 
  const [name, setName] = useState('')
  const [phone, setPhone] = useState('')
  const [kw, setKw] = useState('')

  const kaimoData = 
    name,
    phone
  

  return (
    <div className="kaimo-app">
      <input type="text" placeholder='请输入姓名' onChange=e => setName(e.target.value) />
      <input type="text" placeholder='请输入电话' onChange=e => setPhone(e.target.value) />
      <input type="text" placeholder='请输入关键词' onChange=e => setKw(e.target.value) />
      <Child data=kaimoData />
    </div>
  )


export default App

下面我们依次在输入框里输入1,2,3,发现输入 kw 为 3 的时候,发现也执行了 useEffect 内的回调函数,而子组件并没有监听 kw 的变化。

这个时候我们可以通过 useMemo 将 data 包装一下,告诉 data 它需要监听的值。

import React,  useState, useEffect, useMemo  from 'react'
...
const kaimoData = useMemo(() => (
  name,
  phone
), [name, phone])
...

添加 useMemo 之后,我们发现 kw 输入 3 的时候,不会在触发。它相当于把父组件需要传递的参数做了一个标记,无论父组件其他状态更新任何值,都不会影响要传递给子组件的对象。

useCallback

const memoizedCallback = useCallback(
  () => 
    doSomething(a, b);
  ,
  [a, b],
);

返回一个 memoized 回调函数。

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

useCallback 也是和 useMemo 有类似的功能。

下面我们传递一个函数给子组件,修改 App.jsx 如下所示:

import React,  useState, useEffect  from 'react'

// 在内部新增一个子组件,子组件接收父组件传进来的一个对象,作为子组件的 useEffect 的第二个依赖参数。
function Child(callback) 
  useEffect(() => 
    callback();
  , [callback])
  return <div className='kaimo-child'>子组件</div>


function App() 
  const [name, setName] = useState('')
  const [phone, setPhone] = useState('')
  const [kw, setKw] = useState('')

  const kaimoCallback = () => 
    console.log('kaimo-child:查询条件', 
      name,
      phone
    )
  

  return (
    <div className="kaimo-app">
      <input type="text" placeholder='请输入姓名' onChange=e => setName(e.target.value) />
      <input type="text" placeholder='请输入电话' onChange=e => setPhone(e.target.value) />
      <input type="text" placeholder='请输入关键词' onChange=e => setKw(e.target.value) />
      <Child callback=kaimoCallback />
    </div>
  )


export default App

当我们修改任何状态值,都会触发子组件的回调函数执行。

我们给要传递的函数,包裹一层 useCallback,如下所示:

import React,  useState, useEffect, useCallback  from 'react'
...
const kaimoCallback = useCallback(() => 
  console.log('kaimo-child:查询条件', 
    name,
    phone
  )
, [])
...

无论修改其他任何属性,都不会触发子组件的副作用:

useCallback 的第二个参数同 useEffect 和 useMemo 的第二个参数,它是用于监听你需要监听的变量,如在数组内添加 name、phone、kw 等参数,当改变其中有个,都会触发子组件副作用的执行。

其他更多可以查看Hook API 索引

5、重新认识 useEffect

先来看一个例子:我们修改一下 App.jsx

import React,  useEffect, useState  from 以上是关于React Hooks 快速入门:从一个数据请求开始的主要内容,如果未能解决你的问题,请参考以下文章

入门React 17 + Vite + ECharts 实现疫情数据可视化「03 学习 React Hooks」

使用 React Hooks 在不同页面中获取请求

使用 JSON 数据输出数组数组而不是一个数组来反应 useState Hooks

react自定义hooks-自动改变页面的title,Http请求hooks等..(持续更新)

翻译在 React Hooks 中如何请求数据?

React hooks之useEffect