以前,React API 只有一套,现在有两套:类(class)API 和基于函数的钩子(hooks) API。
任何一个组件,可以用类来写,也可以用函数来写。
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>
}
}
类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>
}
这个函数只做一件事,就是根据输入的参数,返回组件的 HTML 代码。这种只进行单纯的数据计算(换算)的函数,在函数式编程里面称为 "纯函数"(pure function)。
函数一般来说,只应该做一件事,就是返回一个值。 如果有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。 根据这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。
React 团队希望,组件不要变成复杂的容器,最好只是数据流的管道。开发者根据需要,组合管道即可。组件的最佳写法应该是函数,而不是类。
副作用是什么? 看到这里,你可能会产生一个疑问:如果纯函数只能进行数据计算,那些不涉及计算的操作(比如生成日志、储存数据、改变应用状态等等)应该写在哪里呢? 函数式编程将那些跟数据计算无关的操作,都称为 "副作用" (side effect) 。如果函数内部直接包含产生副作用的操作,就不再是纯函数了,我们称之为不纯的函数。
纯函数内部只有通过间接的手段(即通过其他函数调用),才能包含副作用。
Hook(钩子)是 React 函数组件的副作用解决方案,用来为函数组件引入副作用。 函数组件应写成纯函数,只用来返回组件的 HTML 代码,如果需要外部功能和副作用,就用钩子把外部代码 "钩" 进来。
你需要什么功能,就使用什么钩子。所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用use
前缀命名,便于识别。你要使用 xxx 功能,钩子就命名为 usexxx。
下面是 React 默认提供的四个最常用的钩子:
useState()
用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。
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>
}
运行结果:
上面代码中,Button 组件是一个函数,内部使用 useState()
钩子引入状态。
useState()
这个函数接受状态的初始值,作为参数,上例的初始值为按钮的文字。
该函数返回一个数组,数组的第一个成员是一个变量(上例是buttonText
),指向状态的当前值。第二个成员是一个函数,用来更新状态,约定是 set
前缀加上状态的变量名(上例是 setButtonText
)。
如果需要在组件之间共享状态,可以使用 useContext()
。
现在有两个组件 A 和 B,我们希望它们之间共享状态。
<div class>
<A/>
<B/>
</div>
第一步就是使用 React Context API,在组件外部建立一个 Context:
const AppContext = React.createContext({});
组件封装代码如下:
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div class>
<A/>
<B/>
</div>
</AppContext.Provider>
上面代码中,AppContext.Provider
提供了一个 Context 对象,这个对象可以被子组件共享。
A 组件的代码如下:
const A = () => {
const { username } = useContext(AppContext)
return (
<div class>
<span>A:</span>
<span>{username}</span>
</div>
)
}
上面代码中,useContext()
钩子函数用来引入 Context 对象,从中获取 username
属性。
B 组件的代码也类似。
const B = () => {
const { username } = useContext(AppContext)
return (
<div class>
<span>B:</span>
<span>{username}</span>
</div>
)
}
运行结果:
React 本身不提供状态管理功能,通常需要使用外部库。这方面最常用的库是 Redux。
Redux 的核心概念是,组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是 (state, action) => newState
useReducers()
钩子用来引入 Reducer 功能。
const [state, dispatch] = useReducer(reducer, initialState);
上面是 useReducer()
的基本用法,它接受 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的 dispatch
函数。
下面是一个计数器的例子。用于计算状态的 Reducer 函数如下。
const myReducer = (state, action) => {
switch (action.type) {
case 'countUp':
return {
...state,
count: state.count + 1
}
default:
return state
}
}
组件代码如下。
function App() {
const [state, dispatch] = useReducer(myReducer, { count: 0 })
return (
<div class>
<button onClick={() => dispatch({ type: 'countUp' })}>+1</button>
<p>Count: {state.count}</p>
</div>
)
}
运行结果:
由于 Hooks 可以提供共享状态和 Reducer 函数,所以它在这些方面可以取代 Redux。但是,它没法提供中间件(middleware)和时间旅行(time travel),如果要用这两个功能,还是要用 Redux。
useEffect()
用来引入具有副作用的操作,最常见的就是向服务器请求数据。以前,放在 componentDidMount
里面的代码,现在可以放在 useEffect()
。
举个例子,我们希望组件加载以后,网页标题(document.title
)会随之改变。那么,改变网页标题这个操作,就是组件的副作用,需要通过 useEffect()
来实现:
import React, { useEffect } from 'react'
function Welcome(props) {
useEffect(() => {
document.title = '加载完成'
})
return <h1>Hello, {props.name}</h1>
}
上面例子中,useEffect()
的参数是一个函数,它就是所要完成的副作用(改变网页标题)。组件加载以后,React 就会执行这个函数。
useEffect()
的作用就是指定一个副作用函数,组件每渲染一次,该函数就自动执行一次。组件首次在网页 DOM 加载后,副作用函数也会执行。
有时候,我们不希望 useEffect()
每次渲染都执行,这时可以使用它的第二个参数,使用一个数组指定副作用函数的依赖项,只有依赖项发生变化,才会重新渲染。
function Welcome(props) {
useEffect(() => {
document.title = `Hello, ${props.name}`
}, [props.name])
return <h1>Hello, {props.name}</h1>
}
上面例子中,useEffect()
的第二个参数是一个数组,指定了第一个参数(副作用函数)的依赖项(props.name
)。只有该变量发生变化时,副作用函数才会执行。
如果第二个参数是一个空数组,就表明副作用参数没有任何依赖项。因此,副作用函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。这很合理,由于副作用不依赖任何变量,所以那些变量无论怎么变,副作用函数的执行结果都不会改变,所以运行一次就够了。
只要是副作用,都可以使用 useEffect()
引入。它的常见用途有下面几种:
下面是从远程服务器获取数据的例子:
import React, { useState, useEffect } from 'react'
import axios from 'axios'
function App() {
const [data, setData] = useState({ hits: [] })
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux'
)
setData(result.data)
}
fetchData()
}, [])
return (
<ul>
{data.hits.map((item) => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)
}
export default App
上面例子中,useState()
用来生成一个状态变量(data
),保存获取的数据;useEffect()
的副作用函数内部有一个 async 函数,用来从服务器异步获取数据。拿到数据以后,再用 setData()
触发组件的重新渲染。
由于获取数据只需要执行一次,所以上例的 useEffect()
的第二个参数为一个空数组。
副作用是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副作用。
useEffect()
允许返回一个函数,在组件卸载时,执行该函数,清理副作用。如果不需要清理副作用,useEffect()
就不用返回任何值。
useEffect(() => {
const subscription = props.source.subscribe()
return () => {
subscription.unsubscribe()
}
}, [props.source])
上面例子中,useEffect()
在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。
实际使用中,由于副作用函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副作用函数重新执行之前,也会执行一次,用来清理上一次渲染的副作用。
使用 useEffect()
时,如果有多个副作用,应该调用多个 useEffect()
,而不应该合并写在一起。
function App() {
const [varA, setVarA] = useState(0)
const [varB, setVarB] = useState(0)
useEffect(() => {
const timeoutA = setTimeout(() => setVarA(varA + 1), 1000)
const timeoutB = setTimeout(() => setVarB(varB + 2), 2000)
return () => {
clearTimeout(timeoutA)
clearTimeout(timeoutB)
}
}, [varA, varB])
return (
<span>
{varA}, {varB}
</span>
)
}
上面的例子是错误的写法,副作用函数里面有两个定时器,它们之间并没有关系,其实是两个不相关的副作用,不应该写在一起。正确的写法是将它们分开写成两个 useEffect()
function App() {
const [varA, setVarA] = useState(0)
const [varB, setVarB] = useState(0)
useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000)
return () => clearTimeout(timeout)
}, [varA])
useEffect(() => {
const timeout = setTimeout(() => setVarB(varB + 2), 2000)
return () => clearTimeout(timeout)
}, [varB])
return (
<span>
{varA}, {varB}
</span>
)
}
上例的 Hooks 代码还可以封装起来,变成一个自定义的 Hook,便于共享。
const usePerson = (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])
return [loading, person]
}
上面代码中,usePerson()
就是一个自定义的 Hook。
Person 组件就改用这个新的钩子,引入封装的逻辑。
const Person = ({ personId }) => {
const [loading, person] = usePerson(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>
)
}