Hook 简介
React Hooks 是 React 16.7.0-alpha 版本推出的新特性,目的是解决 React 的状态共享问题。称之为状态共享可能描述的并不是很恰当,称为状态逻辑复用可能会更恰当,因为 React Hooks 只共享数据处理逻辑,并不会共享数据本身。
在 React 应用开发中,状态管理是组件开发必不可少的内容。以前,为了对状态进行管理,最通常的做法是使用类组件或者直接使用 redux 等状态管理框架。例如:
class Hook extends React.Component {constructor(props) {super(props);
this.state = {count: 0};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1})}>
Click me
</button>
</div>
);
}
}
现在,开发者可以直接使用 React Hooks 提供的 State Hook 来处理状态,针对那些已经存在的类组件,也可以使用 State Hook 很好地进行重构。例如:
import React, {useState} from 'react';
function Hook() {const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
从上面的示例可以发现,Example 变成了一个函数组件,此函数组件有自己的状态,并且还可以更新自己的状态。之所以可以如此操作,是因为使用了 useState,useState 是 react 自带的一个 hook 函数, 它的作用就是用来声明状态变量。
Hook API
一直以来,React 就在致力于解决一个问题,即状态组件的复用问题。在 React 应用开发中,我们都是通过组件和自上而下传递的数据流来将大型的视图拆分成独立的可复用组件。但是在实际项目开发中,复用一个带有业务逻辑的组件依然是一个难题。
众所周知,React 提供了两种创建组件的方式,即函数组件和类组件。函数组件是一个普通的 JavaScript 函数,接受 props 对象并返回 React 元素,函数组件更符合 React 数据驱动视图的开发思想。不过,函数组件一直以来都因为缺乏类组件诸如状态、生命周期等种种特性,也因为这些原因函数组件得不到开发者的青睐,而 Hooks 的出现就是让函数式组件拥有类组件的特性。
为了让函数组件拥有类组件的诸如状态、生命周期等特性,React 提供了 3 个核心的 api,即 State Hooks、Effect Hooks 和 Custom Hooks。
useState 就是 React 提供最基础、最常用的 Hook,主要用来定义和管理本地状态。例如,下面是使用 useState 实现一个最简单的计数器,代码如下:
import React, {useState} from 'react'
function App() {const [count, setCount] = useState(0);
return (
<div>
<button onClick={()=> setCount(count + 1)}>+</button>
<span>{count}</span>
<button onClick={() => setCount((count) => count - 1)}>-</button>
</div>
);
}
export default App;
在上面的示例中,使用 useState 来定义一个状态,与类组件的状态不同,函数组件的状态可以是对象也可以是基础类型值。useState 返回的是一个数组,数组的第一个对象表示当前状态的值,第二个对象表示用于更改状态的函数,类似于类组件的 setState。
函数组件中如果存在多个状态,既可以通过一个 useState 声明对象类型的状态,也可以通过 useState 多次声明状态。例如:
// 声明对象类型状态
const [count, setCount] = useState({
count1: 0,
count2: 0
});
// 多次声明
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
可以发现,相比于声明对象类型状态,多次声明状态的方式更加方便,主要是因为更新函数是采用的替换的方式而不是合并的方式。
不过,如果要在函数组件中处理多层嵌套数据逻辑时,使用 useState 就显得力不从心了。值得庆幸的是,开发者可以使用 React 提供的 useReducer 来处理此类问题。例如:
import React, {useReducer} from 'react'
const reducer = function (state, action) {switch (action.type) {
case "increment":
return {count: state.count + 1};
case "decrement":
return {count: state.count - 1};
default:
return {count: state.count}
}
};
function Example() {const [state, dispatch] = useReducer(reducer, {count: 0});
const {count} = state;
return (
<div>
<button onClick={() => dispatch({type: "increment"})}>+</button>
<span>{count}</span>
<button onClick={() => dispatch({type: "decrement"})}>-</button>
</div>
);
}
export default Example;
可以发现,useReducer 接受 reducer 函数和默认值两个参数,并返回当前状态 state 和 dispatch 函数的数组,使用方式与 Redux 基本一致,不同之处在于,Redux 的默认值是通过给 reducer 函数设置默认参数的方式给定的。
useReducer 之所以没有采用 Redux 的方式设置默认值,是因为 React 认为状态的的默认值可能是来自于函数组件的 props。例如:
function Example({initialState = 0}) {const [state, dispatch] = useReducer(reducer, { count: initialState});
...
}
解决了函数组件中内部状态的问题,接下来亟待解决的就是函数组件中生命周期函数的问题。在 React 的函数式编程思想中,生命周期函数是沟通函数式和命令式的桥梁,根据生命周期函数开发者可以执行相关的操作,如网络请求和操作 DOM。
import React, {useState, useEffect} from 'react';
function Example() {const [count, setCount] = useState(0);
useEffect(() => {console.log('componentDidMount...')
console.log('componentDidUpdate...')
return () => {console.log('componentWillUnmount...')
}
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Example;
执行上面的代码,然后点击按钮执行加法操作时,生命周期函数调用情况下图所示。
可以看到,每次执行组件更新时 useEffect 中的回调函数都会被调用。并且在点击按钮执行更新操作时还会触发 componentWillUnmount 生命周期,之所以在重新绘制前执行销毁操作,是为了避免造成内存泄露。
因此可以将 useEffect 视为 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合,并用它关联函数组件的生命周期。
需要说明是,类组件的 componentDidMount 或 componentDidUpdate 生命周期函数都是在 DOM 更新后同步执行的,但 useEffect 并不会在 DOM 更新后同步执行。因此,使用 useEffect 的并不会阻塞浏览器更新界面。如果需要模拟生命周期的同步执行,可以使用 React 提供的 useLayoutEffect Hook。
自定义 Hook
众所周知,要在类组件之间共享一些状态逻辑是非常麻烦的,常规的做法都是通过高阶组件或者是函数的属性来解决。不过,新版的 React 允许开发者创建自定义 Hook 来封装共享状态逻辑,且不需要向组件树中增加新的组件。
所谓的自定义 Hook,其实就是指函数名以 use 开头并调用其他 Hook 的函数,自定义 Hook 的每个状态都是完全独立的。例如,下面是使用 axios 实现网络请求的示例,代码如下:
import axios from 'axios'
export const useAxios = (url, dependencies) => {const [isLoading, setIsLoading] = useState(false);
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {setIsLoading(true);
axios.get(url).then((res) => {setIsLoading(false);
setResponse(res);
}).catch((err) => {setIsLoading(false);
setError(err);
});
}, dependencies);
return [isLoading, response, error];
};
如上所示,就是使用 axios 开发请求数据的自定义 Hook。使用方法和系统提供的 Hook 类似,直接调用即可。例如:
function Example() {
let url = 'http://api.douban.com/v2/movie/in_theaters';
const [isLoading, response, error] = useAxios(url, []);
return (
<div>
{isLoading ? <div>loading...</div> :
(error ? <div> There is an error happened </div> : <div> Success, {response} </div>)}
</div>
)
}
export default Example;
可以发现,相比于函数属性和高阶组件等方式,自定义 Hook 则更加的简洁易读,不仅于此,自定义 Hook 也不会引起之组件嵌套地狱问题。
虽然 React 的 Hooks 有着诸多的优势。不过,在使用 Hooks 的过程中,需要注意以下两点:
• 不要在循环、条件或嵌套函数中使用 Hook,并且只能在 React 函数的顶层使用 Hook。之所以要这么做,是因为 React 需要利用调用顺序来正确更新相应的状态,以及调用相应的生命周期函数函数。一旦在循环或条件分支语句中调用 Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
• 只能在 React 函数式组件或自定义 Hook 中使用 Hook。
同时,为了避免在开发中造成一些低级的错误,可以安装一个 eslint 插件,安装命令如下:
yarn add eslint-plugin-react-hooks --dev
然后,在 eslint 的配置文件中添加如下一些配置。
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
借助于 React 提供的 Hooks API,函数组件可以实现绝大部分的类组件功能,并且 Hooks 在共享状态逻辑、提高组件复用性上也具有一定的优势。可以预见的是,Hooks 将是 React 未来发展的重要方向。