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

简介: 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

c94234bd7dd0419aa2f7cdf6b370cca7.png

执行下面命令:

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


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

npm install
npm run dev


c90fb3d64c2f4e14a8b5897e6937d513.png


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


页面显示结果如下:

ec2c4cf4dc7c48b998227fc6b5705a13.png



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



页面显示结果:

28b0081a4c5f44f3ab009e562e0cf3cf.gif


我们发现函数组件默认进来之后,会执行 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)
  })()
}, [])


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

18694d9bd32840a88f4fbc3052fc3538.png



给请求一个 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 的值,副作用函数便会被执行,结果如下:


2ce45b46572240afbea9c1e1c786c944.png

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



useContext

const value = useContext(MyContext);

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


当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 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


结果是一样的:

b9f6cbdfd11c481ab10b4f766c9b55c2.png

我们可通过自定义 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 的变化。

ada00197ab894cc59340203bb4aae4ab.png


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

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


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


9704980e300648efab5c3f70195f5684.png


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


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

4d48de5371114683b54235683efba20f.png



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

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


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

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


ae4b1ac50c2245258debfb27a5b82886.png

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



5、重新认识 useEffect

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

import React, { useEffect, useState } from 'react'
function App() {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setTimeout(() => {
      console.log('kaimo 点击次数: ' + count);
    }, 2000);
  }
  return (
    <div className="kaimo-app">
      <button onClick={() => setCount(count + 1)}>点击{count}次</button>
      <button onClick={handleClick}>展示点击次数</button>
    </div>
  )
}
export default App


我们先按下面的步骤操作:

  1. 点击增加按钮两次,将 count 增加到 2。
  2. 然后点击展示点击次数按钮。
  3. console.log 执行之前,再次点击新增按钮 2 次,将 count 增加到 4。

按照正常的思路,浏览器应该打印出 kaimo 点击次数: 4


7317a81b3b7e41bd811b6c2a3f7ceefb.gif


我们发现打印出来的是 kaimo 点击次数: 2


这是因为函数组件 App,在每一次渲染都会被调用,而每一次调用都会形成一个独立的上下文,可以理解成一个快照。每一次渲染形成的快照,都是互相独立的。


用一份伪代码来解释,大致如下:

// 默认初始化
function App() {
  const count = 0; // useState 返回默认值
  // ...
  function handleClick() {
    setTimeout(() => {
      console.log('kaimo 点击次数: ' + count);
    }, 2000);
  }
  // ...
}
// 第一次点击
function App() {
  const count = 1; // useState 返回值
  // ...
  function handleClick() {
    setTimeout(() => {
      console.log('kaimo 点击次数: ' + count);
    }, 2000);
  }
  // ...
}
// 第二次点击
function App() {
  const count = 2; // useState 返回值
  // ...
  function handleClick() {
    setTimeout(() => {
      console.log('kaimo 点击次数: ' + count);
    }, 2000);
  }
  // ...
}


我们可以知道,每次渲染函数组件时,useEffect 都是新的,都是不一样的。

我们改造一下代码:

import React, { useEffect, useState } from 'react'
function App() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    setTimeout(() => {
      console.log('kaimo 点击次数: ' + count);
    }, 2000);
  })
  return (
    <div className="kaimo-app">
      <button onClick={() => setCount(count + 1)}>点击{count}次</button>
    </div>
  )
}
export default App


我们连续点击4次,每一次点击,都会重新执行 useEffect 内的回调,并且 count 值也是当时的快照的一个常量值。

2491f3ab2531410a92d194ff7a0945cf.gif


参考资料



拓展资料




目录
相关文章
|
26天前
|
存储 缓存 前端开发
react怎么只让接口请求一次
react怎么只让接口请求一次
21 0
|
18天前
|
存储 前端开发 JavaScript
React中有效地使用props和state来管理组件的数据和行为
React中有效地使用props和state来管理组件的数据和行为
|
25天前
|
前端开发
react通过上下文深入传递数据
react通过上下文深入传递数据
|
9天前
|
缓存 前端开发 JavaScript
React Hooks 一步到位
React Hooks 一步到位
|
2月前
|
存储 前端开发 JavaScript
React Hooks 的替代方案有哪些?
【5月更文挑战第28天】React Hooks 的替代方案有哪些?
28 2
|
2月前
|
前端开发 JavaScript 开发者
React Hooks 的应用场景有哪些?
【5月更文挑战第28天】React Hooks 的应用场景有哪些?
17 1
|
2月前
|
前端开发 JavaScript 开发者
React Hooks 是在 React 16.8 版本中引入的一种新功能
【5月更文挑战第28天】React Hooks 是在 React 16.8 版本中引入的一种新功能
27 1
|
2月前
|
前端开发
React Hooks - useState 的使用方法和注意事项(1),web前端开发前景
React Hooks - useState 的使用方法和注意事项(1),web前端开发前景
|
2月前
|
缓存 前端开发 JavaScript
React和Next.js开发常见的HTTP请求方法
React和Next.js开发常见的HTTP请求方法
27 0
|
2月前
|
前端开发
掌握React中的useContext:访问父组件数据的神奇技巧
掌握React中的useContext:访问父组件数据的神奇技巧