useState 钩子函数
介绍
用于为函数组件引入状态。
通常情况下,函数中的变量在函数执行完就会被释放掉,useState
方法通过闭包(将数据存储在组件函数外)让函数型组件实现保存和变更状态。
useState
接收一个初始状态的值作为参数,返回一个数组,数组的第一个元素是状态数据,第二个元素是设置状态数据的方法,该方法接收一个参数用于修改状态数据。
可以通过数组解构的方式将数组中的元素解构出来。
组件重新渲染时,useState
会获取状态的值,忽略设置的初始值。
使用
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
return <div>
<span>{count}</span>
<button onClick={() => setCount(count+1)}>+ 1</button>
</div>
}
export default App
useState 使用细节
- 接收唯一的参数,即状态初始值,初始值可以是任意数据类型
- 返回值为数组,数组中存储状态值和更改状态值的方法,方法名称约定以
set
开头,后面加上状态名称 - 方法可以被调用多次,用于保存不同状态值
- 参数可以是一个函数,函数返回什么,初始状态就是什么,函数只会被调用一次,用在初始值是动态值得情况
// 参数是函数的场景,优先取组件接收的数据,这样只会在挂载时获取一次 props 中的数据
function App(props) {
// 错误写法:每次渲染都会获取 props.count
// const propCount = props.count
// const [count, setCount] = useState(propCount || 0)
// 正确写法:只会在挂载时执行一次
const [count, setCount] = useState(() => {
return props.count || 0
})
return <div>
<span>{count}</span>
<button onClick={() => setCount(count+1)}>+ 1</button>
</div>
}
设置状态值方法的使用细节
- 接收唯一的参数参数可以是一个值也可以是一个回调函数
- 参数即新的状态值或返回新的状态值的函数,它会完全替换状态值,不会像
setState
一样合并对象类型的状态值。 - 方法本身是异步的,若要同步执行一些业务,业务代码要写在回调函数参数内。
import { useState } from 'react'
function App(props) {
const [count, setCount] = useState(() => {
return props.count || 0
})
function handlePower(number) {
setCount(() => {
const power = number * number
// 同步代码在 setCount 参数内定义
// document.title = count
return power
})
// count 为 setCount 执行前的值
document.title = count
}
return <div>
<span>{count}</span>
<button onClick={() => setCount(count+1)}>+ 1</button>
<button onClick={() => handlePower(count)}>求平方</button>
</div>
}
export default App
useReducer 钩子函数
介绍
useState
的替代方案,是另一种让函数组件引入状态的方式。
useReducer
的方式类似 Redux,组件的状态被保存在特定的地方,要想改变状态,需要通过 dispatch
方法触发一个 Action,这个 Action 会被 Reducer 函数接收,Reducer 内部要根据 Action 的类型决定如何处理状态,最后通过返回值的方式更新状态。
使用
useReducer
方法的参数:
- 第一个参数:接收一个形如
(state, action) => newState
的 Reducer 函数 - 第二个参数:默认的状态初始值
返回:当前的 state 和配套的 dispatch 方法。
import { useReducer } from 'react'
function reducer(state, action) {
switch (action.type) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
default:
return state
}
}
function App() {
const [count, dispatch ] = useReducer(reducer, 0)
return <div>
<span>{count}</span>
<button onClick={() => dispatch({type: 'increment'})}>+ 1</button>
<button onClick={() => dispatch({type: 'decrement'})}>- 1</button>
</div>
}
export default App
相对于 useState 的好处
- 对逻辑较复杂且包含多个子值的 state,使用
useReducer
方便根据 Action 的类型修改部分数据。 - 适合下一个 state 依赖上一个 state 的场景
- 给那些会触发深更新的组件做性能优化
- 可以向下级组件传递
dispatch
方法,而不是组件内定义的函数 - 这样当组件重新渲染时,如果传给下级组件的状态未发生变化,下级组件就不会重新渲染
- 因为
dispatch
的引用是固定的,而组件内定义的函数在组件重新渲染时被重新定义,因此也会触发下级组件的渲染
- 可以向下级组件传递
useContext 钩子函数
在使用 createContext 跨组件层级传递数据时,简化获取数据的代码。
useContext
接收一个 context 对象(React.createContext 的返回值),返回该 context 的当前值。
调用了 useContext
的组件总会在 context 值变化时重新渲染。
context 类组件 static 使用
import React, { createContext } from 'react'
const ThemeContext = createContext()
class Foo extends React.Component {
static contextType = ThemeContext
render() {
return <div>{ this.context }</div>
}
}
function App() {
return (
<ThemeContext.Provider value="dark">
<Foo />
</ThemeContext.Provider>
)
}
export default App
Consumer 嵌套组件使用
import { createContext } from 'react'
const ThemeContext = createContext()
function Foo() {
return <ThemeContext.Consumer>
{
theme => <div>{ theme }</div>
}
</ThemeContext.Consumer>
}
function App() {
return (
<ThemeContext.Provider value="dark">
<Foo />
</ThemeContext.Provider>
)
}
export default App
useContext 使用
使函数型组件中不适用组件嵌套(Consumer)就可以订阅 Context。
import { createContext, useContext } from 'react'
const ThemeContext = createContext()
function Foo() {
const theme = useContext(ThemeContext)
return <div>{ theme }</div>
}
function App() {
return (
<ThemeContext.Provider value="dark">
<Foo />
</ThemeContext.Provider>
)
}
export default App
useEffect 钩子函数
介绍
让函数型组件拥有处理副作用的能力,类似生命周期函数。
- 在类组件中使用生命周期函数处理副作用。
- 在函数型组件中使用
useEffect
处理副作用。
useEffect 执行时机
useEffect
可以看作 componentDidMount
、componentDidUpdate
和 componentWillUnmount
三个函数的组合:
useEffect(() => {}) ==> componetDidMount, componentDidUpdate
- 当组件挂载完成和状态更新完成时会调用传入的函数
useEffect(() => {}, []) ==> componetDidMount
- 第二个参数接收一个数组,用于指定监听的状态,当组件挂载完成和指定的状态变更完成时执行传入的函数
- 如果第二个参数是空数组,表示没有要监听的状态,则只会在挂载完成时执行一次
useEffect(() => () => {}) ==> componetWillUnmount
- 传入的函数如果返回了一个回调函数,组件更新前和被卸载前会执行这个回调函数
- 这个函数用于清理上一个副作用,以保证一致性(避免上一个副作用的效果残留)
当组件重新渲染时,useEffect
会用新的副作用函数替换之前的副作用函数。
使用
import { useState, useEffect } from 'react'
import ReactDom from 'react-dom'
function Foo(props) {
useEffect(() => {
console.log('1. 组件挂载完成和状态更新完成时执行')
})
useEffect(() => {
console.log('2. 仅在组件挂载完成时执行一次')
}, [])
useEffect(() => {
return () => {
console.log('3. 在组件更新前和卸载前执行', 'count: '+ count)
}
})
return <div>{props.count}</div>
}
function App(props) {
const [count, setCount] = useState(0)
return <div>
<Foo count={count} />
<button onClick={() => setCount(count+1)}>+ 1</button>
<button onClick={() => ReactDom.unmountComponentAtNode(document.getElementById('root'))}>卸载组件</button>
</div>
}
export default App
ReactDom.unmountComponentAtNode(container)
用于卸载容器中渲染的组件。
示例
- 为 window 对象添加滚动事件
- 设置定时器,让 count 数值每秒 +1
index.html 文件中给 body 添加高度:
<body style="height: 2000px;">
<div id="root"></div>
</body>
import { useState, useEffect } from 'react'
import ReactDom from 'react-dom'
function App() {
function onScroll() {
console.log('页面滚动了')
}
useEffect(() => {
window.addEventListener('scroll', onScroll)
return () => {
window.removeEventListener('scroll', onScroll)
}
}, [])
const [count, setCount] = useState(0)
useEffect(() => {
const timerId = setInterval(() => {
// 这里需使用函数,将 count 作为参数传入,否则数值只会更新一次
setCount(count => {
document.title = count + 1
return count + 1
})
}, 1000)
return () => {
clearInterval(timerId)
}
}, [])
return (
<div>
<span>{count}</span>
<button onClick={() => ReactDom.unmountComponentAtNode(document.getElementById('root'))}>卸载组件</button>
</div>
);
}
export default App;
在处理副作用上 useEffect 相比类组件的优势
- 按照用途将代码进行分类
- 由于
useEffect
可以被多次调用,所以将一组相干的业务逻辑归置到同一个副作用函数中,将不相干的业务逻辑分置到不同的副作用函数中
- 由于
- 简化重复代码,使组件内部代码更加清晰
- 例如避免在
componentDidMount
和componentDidUpdate
中编写重复代码
- 例如避免在
useEffect 的第二个参数
useEffect
的第二个参数是依赖项数组,作用是:只有指定数据发生变化时才会触发副作用(effect)。
- 当不传递依赖项数组的时候,会在组件数据(所有)发生变化的时候触发副作用函数。
- 如果传递一个空数组,则只会在初始加载时执行副作用函数,不会监听任何数据的变化。
原理是接收一个给定值的数组,组件每次渲染,用新的数组和旧的数组去对比,有任何一项不相等则执行副作用。
import { useState, useEffect } from 'react'
function App() {
const [count, setCount] = useState(0)
const [person, setPerson] = useState({name:'张三'})
useEffect(() => {
console.log('只有当 count 变化时才会执行回调函数')
document.title = count
}, [count])
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+ 1</button>
<br/>
<span>{person.name}</span>
<button onClick={() => setPerson({name: '李四'})}>更名</button>
</div>
);
}
export default App;
异步操作
使用 await async 关键字
在 useEffect
回调函数中执行异步操作,例如:
import { useEffect } from 'react'
function App() {
useEffect(() => {
getData().then(result => {
console.log(result)
})
}, [])
return (
<div>App</div>
);
}
// 模拟的异步操作
function getData() {
return new Promise(resolve => {
resolve({msg: 'Hello Async'})
})
}
export default App;
如果想使用await
关键字,则需要添加 async
关键字:
useEffect(() => {
const asyncFn = async () => {
const result = await getData()
console.log(result)
}
asyncFn()
}, [])
但是这样写,会出现问题:
# 在 React 16 中会报错打断运行(Error)
An effect function must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately:<推荐写法>
# 副作用函数必须返回一个用于清理的普通函数。
# 看起来你编写了 useEffect(async () => ...) 或返回了一个 Promise。相反,你可以在副作用函数中编写异步函数,并立即调用它:<推荐写法>
# 在 React 17 中会警告(Warning)
Effect callbacks are synchronous to prevent race conditions. Put the async function inside:<推荐写法>
# 为了防止竞态条件(异步创建的任务无法确定执行顺序),副作用回调函数都是同步的。将异步函数像这样写入:<推荐写法>
错误原因
How to fetch data with React Hooks?
如控制台提示的,副作用函数必须返回一个用做清理资源(组件销毁时调用)的普通函数。
如果使用 async
关键字声明函数,则会声明一个异步函数,异步函数会返回一个 Pormise,副作用函数返回 Promise,违反了使用规则。
React 17 这种写法虽然不会阻塞程序,但也不建议这样使用。
推荐写法
如官方推荐的写法,在普通函数中执行异步操作,将普通函数传给 useEffect
。
useEffect(() => {
const asyncFn = async () => {
const result = await getData()
console.log(result)
}
asyncFn()
}, [])
或在自执行函数中执行:
useEffect(() => {
(async () => {
const result = await getData()
console.log(result)
})()
}, [])