好长时间没写 React 的项目了,发现 React 发生了很大的变化,v16.8 版本引入了全新的 API,叫做 React Hooks,颠覆了以前的用法。现在就来了解一下相关用法。
之前 React 写组件时,都是通过类(class)来写的。比如:
import React, { Component } from 'react'
export default class Button extends Component { constructor() { super() this.state = { buttonText: 'Click me, please' } this.handleClick = this.handleClick.bind(this) } handleClick() { this.setState(() => { return { buttonText: 'Thanks, been clicked!' } }) } render() { const { buttonText } = this.state return <button onClick={this.handleClick}>{buttonText}</button> } }
|
现在我们使用 React Hooks 来写组件的话,是这样的:
import React, { useState } from 'react'
export default function Button() { const [buttonText, setButtonText] = useState('Click me, please')
function handleClick() { return setButtonText('Thanks, been clicked!') }
return <button onClick={handleClick}>{buttonText}</button> }
|
直观的就可以看到,代码量减少了一些。更重要的一点是使用函数实现的组件。
其实 React 早就支持函数组件:
function Welcome(props) { return <h1>Hello, {props.name}</h1> }
|
但是这种写法有重大限制,必须是纯函数,不能包含状态,也不支持生命周期方法,因此无法取代类。
React Hooks 的出现,就解决了这些问题,对函数组件进行了加强,现在不使用类,也能写出全功能的组件。
Hook 的 含义
Hook 这个单词的意思是“钩子”。
React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码”钩”进来。 React Hooks 就是那些钩子。
你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。
下面介绍一些 React 默认提高的常用钩子。
useState():状态钩子
useState()用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。
再看一下我们上面用函数实现的组件,这个组件里就使用了 useState()。
import React, { useState } from 'react'
export default function Button() { const [buttonText, setButtonText] = useState('Click me, please')
function handleClick() { return setButtonText('Thanks, been clicked!') }
return <button onClick={handleClick}>{buttonText}</button> }
|
useState()这个函数接受状态的初始值,作为参数,上例的初始值为按钮的文字。该函数返回一个数组,数组的第一个成员是一个变量,指向状态的当前值。第二个成员是一个函数,用来更新状态,约定是 set 前缀加上状态的变量名。
useContext():共享状态钩子
如果需要在组件之间共享状态,可以使用 useContext()。
下面代码中, 有两个组件 Navbar 和 Messages 需要共享状态,可以看看通过 useContext() 是如何实现的:
import React, { useContext } from 'react' import ReactDOM from 'react-dom'
const AppContext = React.createContext({})
const Navbar = () => { const { username } = useContext(AppContext)
return ( <div className="navbar"> <p>AwesomeSite</p> <p>{username}</p> </div> ) }
const Messages = () => { // useContext()钩子函数用来引入 Context 对象,从中获取username属性。 const { username } = useContext(AppContext)
return ( <div className="messages"> <h1>Messages</h1> <p>1 message for {username}</p> <p className="message">useContext is awesome!</p> </div> ) }
function App() { // AppContext.Provider提供了一个 Context 对象,这个对象可以被子组件共享。 return ( <AppContext.Provider value={{ username: 'superawesome', }} > <div> <Navbar /> <Messages /> </div> </AppContext.Provider> ) }
const rootElement = document.getElementById('root') ReactDOM.render(<App />, rootElement)
|
useReducer():action 钩子
React 本身不提供状态管理功能,通常需要使用外部库。这方面最常用的库是 Redux。
Redux 的核心概念是,组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是(state, action) => newState。
useReducers()钩子用来引入 Reducer 功能。
const [state, dispatch] = useReducer(reducer, initialState)
|
useReducer() 接受 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的 dispatch 函数。
const initialState = { count: 0 }
function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 } case 'decrement': return { count: state.count - 1 } default: throw new Error() } }
function Counter() { const [state, dispatch] = useReducer(reducer, initialState) return ( <> Count: {state.count} <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'increment' })}>+</button> </> ) }
|
useEffect():副作用钩子
useEffect()用来引入具有副作用的操作,最常见的就是向服务器请求数据。useEffect()的用法如下:
useEffect(() => { }, [dependencies])
|
useEffect()接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行 useEffect()。
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。
React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect
具体可以看个栗子:
const Person = ({ personId }) => { const [loading, setLoading] = useState(true) const [person, setPerson] = useState({})
useEffect(() => { setLoading(true) fetch(`https://swapi.co/api/people/${personId}/`) .then((response) => response.json()) .then((data) => { setPerson(data) setLoading(false) }) }, [personId])
if (loading === true) { return <p>Loading ...</p> }
return ( <div> <p>You're viewing: {person.name}</p> <p>Height: {person.height}</p> <p>Mass: {person.mass}</p> </div> ) }
|
上面代码中,每当组件参数 personId 发生变化,useEffect()就会执行。组件第一次渲染时,useEffect()也会执行。
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。
useEffect(() => { const subscription = props.source.subscribe() return () => { subscription.unsubscribe() } })
|
为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。
最后需要注意的事,使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。所以我们不应该把多个副效应合并写在一个 useEffect() 里,应该使用多个 effect,将不相关逻辑分离到不同的 effect 中。