动机
一直在考虑为什么要使用,我们明明使用state使用的好好的,那会发明HOOK的动机是什么呢?
- 在组件中复用状态逻辑很复杂
使用Hooks,可以从组件中提取有状态逻辑,以便可以独立测试并复用。Hooks允许在不更改组件层次结构的情况下复用有状态的逻辑。 这样可以轻松地在许多组件之间共享Hooks。 - 复杂组件变得难以理解
使用state会导致相关的逻辑被切分,一些相关的代码却被分割在不同的函数文件中,这样导致代码的可读性变弱,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分,也可以使用reducer来进行相关逻辑的复用 - 难以理解的 class
class使用的时候,要先了解JavaScript的this使用,而且还要进行事件的绑定,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。hook 使用非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。
概览
hooks其实就是一种特殊的函数,这个函数有勾住状态和生命周期的功能,但我们希望在组件中使用状态的时候,我们就会使用hook用来代替class中的state。Hook 不能在 class 组件中使用。
hook和class的对比
使用class的形式来写组件的方法
import React from 'react'
class Person extends React.Component {
constructor(props) {
super(props);
this.state = {
username: 'kim'
}
}
componentDidMount() {
console.log('组件挂载后做的操作')
}
componentWillUnmount() {
console.log('组件将要卸载')
}
componentDidUpdate(prevProps, prevState) {
// 当username发生改变的时候进行渲染
if (prevState.username !== this.state.username) {
console.log('组件更新后的操作')
}
}
render() {
return (<Input onChange={(event) => this.setState({ username: event.target.value })} />)
}
}
用hook来写函数组件
import React, { useEffect, useState } from 'react'
export const Person = () => {
const [name, setName] = useState('');
useEffect(() => {
console.log('组件挂载后要做的操作');
return () => {
console.log('组件卸载要做的操作')
}
}, [])
useEffect(() => {
console.log('当name组件发生改变的时候显示的样式')
}, [name])
return (<div>
<p>欢迎 {name}</p>
<input type="text" placeholder="input a username" onChange={(event) => setName(event.target.value)}></input>
</div>
)
}
API
useState
- 参数:是一个常量,组件初始化的时候就会进行定义
- 参数:是一个函数,只有开始渲染的时候函数才会执行
- 返回值:长度为2的数组,第一项是返回的state的值,第二项是改变该state的函数
用来初始化状态,在函数的内部调用,为函数添加内部的state,useState会返回一对值,分别是要更新的状态和一个让你更新他的函数,useState唯一的参数就是初始化的值。
// 声明多个 state 变量!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// 声明一个叫 "count" 的 state 变量,初始值为0,后续通过setCount改变它能让视图重新渲染
export const Count = () => {
const [count, setCount] = useState(0);
// initiState 只会在组件初渲染的时候起作用,后续的渲染会被忽略不计
const [value,setValue] = setValue(()=>{
const initialCount = someExpensiveComputation(props);
return initialState;
})
return (<div>
<p> 你点击了{count}次</p>
<button onClick={() => setCount(count + 1)}> Click me</button>
</div>)
}
useEffect
- 参数:第一个是含有副作用的命令式的代码,
- 参数:第二个参数是一个数组,数组中的值用来控制第一个参数中的函数是否执行,当第二个参数不传的时候,是每一次有数据更新的时候,执行第一个参数中的函数,相当于class组件中的componentDidMount和componentDidupdate的生命周期。
export const Count = () => {
const [count, setCount] = useState(0);
// 功能类似componentDidMount and componentDidUpdate
useEffect(() => {
document.title = `You clicked ${count} times`;
});
// 只有count改变时才会执行
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
// 组件挂载时只执行一次
useEffect(() => {
console.log("只执行一次,类似componentDidMount")
}, []);
return (<div>
<p> 你点击了{count}次</p>
<button onClick={() => setCount(count + 1)}> Click me</button>
</div>)
}
可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,比如说全局设定鼠标监听事件。
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
useContext
当hook接受到一个context的对象的时候并返回context的当前值,当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext(themes.light);
const App = () => {
// 在root中传入这个值,使用context进行包裹,便于后续取值
return (
<ThemeContext.Provider value={themes.dark}>
<ToolBar />
</ThemeContext.Provider>
)
}
// 中间层不用传递参数
const ToolBar = () => {
return (
<ThemedButton />
)
}
// 在使用层直接进行使用就可以了
const ThemedButton = () => {
const theme = useContext(ThemeContext)
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
)
}
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
我们看一下上面的代码以class的形式编写,会是什么样子的:
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
useReducer
- 第一个参数是reducer纯函数
- 第二个参数是初始的state
- 第三个参数可以修改初始state,将初始 state 设置为 init(initialArg)
是useState的一种替代场景,如果我们都是使用useState就会散落到程序的各个地方,我们通过代码比较一下二者的差别:
我们使用setState来完成这一段代码
const LoginPage = () => {
const [name, setName] = useState(''); // 用户名
const [err, setError] = useState(''); //错误信息
const [pwd, setPwd] = useState(''); // 密码
const [isLoading, setIsLoading] = useState(false); // 发送请求之后数据是否已经返回
const [isLogged, setIsLogged] = useState(false) // 是否已经登陆
const login = (e) => {
e.preventDefault();
setError('');
setIsLoading(true);
login({ name, pwd }).then(() => {
setIsLoggedIn(true); // 标示登陆成功
setIsLoading(false); // 标示loading完成
}).catch((error) => {
setError(error.message);
setName('');
setPwd('');
setIsLoading(false);
})
}
return(<div>jsx的界面</div>)
}
我们发现setState散落到方法的各个地方,导致代码的已读性大大的减弱了,我们再看一下 我们使用useReducer来进行这个操作:
const loginReducer = (state, action) => {
switch (action.type) {
case 'login':
return {
...state,
isLoading: false,
error: ''
}
case 'success':
return {
...state,
isLoggedIn: true,
isLoading: false,
}
case 'error':
return {
...state,
error: action.payload.error,
name: '',
pwd: '',
isLoading: false
}
default:
return state;
}
}
function LoginPage() {
const [state, dispatch] = useReducer(loginReducer, initState);
const { name, pwd, isLoading, error, isLoggedIn } = state;
const login = (event) => {
event.preventDefault();
dispatch({ type: 'login' });
login({ name, pwd })
.then(() => {
dispatch({ type: 'success' });
})
.catch((error) => {
dispatch({
type: 'error',
payload: { error: error.message }
});
});
}
return (
<div></div>
// 返回页面JSX Element
)
}
- 我们发现使用reducer写的代码,虽然代码长度变长了,但是代码的可读性大大的增加了,我们可以更加清晰的看到这么写代码的意图
- LoginPage不需要关心如何处理这几种行为,那是loginReducer需要关心的,表现和业务分离。
- 所有的state处理都集中到了一起,使得我们对state的变化更有掌控力,同时也更容易复用state逻辑变化代码,比如在其他函数中也需要触发登录error状态,只需要dispatch({ type: 'error' })。
总结一下使用state的场景:
- 如果你的state是一个数组或者对象
- 如果你的state变化很复杂,经常一个操作需要修改很多state
- 如果你希望构建自动化测试用例来保证程序的稳定性
- 如果你需要在深层子组件里面去修改一些状态(关于这点我们下篇文章会详细介绍)
- 如果你用应用程序比较大,希望UI和业务能够分开维护