Hook 的作用
Hook 是为更好地复用 React 状态逻辑代码而生的。注意这里说的不是模板代码,模板代码可以用组件来复用;而单纯的状态逻辑代码没法用组件复用
注意,其实普通的函数也可以复用逻辑代码,但是没法复用带状态的逻辑代码
什么是React的状态
举个例子:
const Comp = () => {
const [id, setId] = useState(0)
const [assets, setAssets] = useState()
useEffect(() => {
fetch(`https://google.com?id=${id}`).then(async response => {
const data = await response.json();
if (response.ok) {
setAssets(data)
} else {
return Promise.reject(data);
}
})
}, [])
return <div>{assets.map(a => a.name)}</div>
}
这里面的 id,assets就是状态,它的特征是它是由特定的API(useState)定义的,而且它改变的时候组件会做出相应的反应(比如重新render)
const sum = (a, b) => a + b
这个普通的函数就没有状态,sum的返回值无论怎么变,都不会让任何组件重新render
React团队是非常注重React 状态代码复用性的,从React被创造出来,他们就一直在优化代码复用的解决方案,大概经历了:Mixin → HOC → Render Props,一直到现在的 Hook
Mixin
Mixin 是最早的 React 代码复用方案
var SubscriptionMixin = {
getInitialState: function() {
return {
comments: DataSource.getComments()
};
},
componentDidMount: function() {
DataSource.addChangeListener(this.handleChange);
},
componentWillUnmount: function() {
DataSource.removeChangeListener(this.handleChange);
},
handleChange: function() {
this.setState({
comments: DataSource.getComments()
});
}
};
var CommentList = React.createClass({
mixins: [SubscriptionMixin],
render: function() {
// Reading comments from state managed by mixin.
var comments = this.state.comments;
return (
<div>
{comments.map(function(comment) {
return <Comment comment={comment} key={comment.id} />
})}
</div>
)
}
});
它的好处是简单粗暴,符合直觉,也确实起到了重用代码的作用;但是坏处也很明显,隐式依赖,名字冲突,不支持 class component,难以维护,总之,现在已经被完全淘汰了
HOC (higher-order component)
2015年,React团队判处Mixin死刑以后,推荐大家使用HOC模式,HOC是采用了设计模式里的装饰器模式
function withWindowWidth(BaseComponent) {
class DerivedClass extends React.Component {
state = {
windowWidth: window.innerWidth,
}
onResize = () => {
this.setState({
windowWidth: window.innerWidth,
})
}
componentDidMount() {
window.addEventListener('resize', this.onResize)
}
componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
}
render() {
return <BaseComponent {...this.props} {...this.state}/>
}
}
return DerivedClass;
}
const MyComponent = (props) => {
return <div>Window width is: {props.windowWidth}</div>
};
经典的 容器组件与展示组件分离 (separation of container presidential) 就是从这里开始的
// components/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'
class AddTodo extends React.Component {
// ...
handleAddTodo = () => {
// dispatches actions to add todo
this.props.addTodo(this.state.input)
// sets state back to empty string
this.setState({ input: '' })
}
render() {
return (
<div>
<input
onChange={(e) => this.updateInput(e.target.value)}
value={this.state.input}
/>
<button className="add-todo" onClick={this.handleAddTodo}>
Add Todo
</button>
</div>
)
}
}
export default connect(null, { addTodo })(AddTodo)
一个很经典的HOC使用案例是react redux 中的 connect 方法,AddTodo组件像一只无辜的小白兔,它的addTodo方法是connect方法给它注入进去的,它有以下优点:
- 可以在任何组件包括 Class Component 中工作
- 它所倡导的 容器组件与展示组件分离 原则做到了:关注点分离
但也有以下明细的缺点
- 不直观,难以阅读
- 名字冲突
- 组件层层层层层层嵌套
Render Props
2017年,render props流行起来
class WindowWidth extends React.Component {
propTypes = {
children: PropTypes.func.isRequired
}
state = {
windowWidth: window.innerWidth,
}
onResize = () => {
this.setState({
windowWidth: window.innerWidth,
})
}
componentDidMount() {
window.addEventListener('resize', this.onResize)
}
componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
}
render() {
return this.props.children(this.state.windowWidth);
}
}
const MyComponent = () => {
return (
<WindowWidth>
{width => <div>Window width is: {width}</div>}
</WindowWidth>
)
}
2017年,render props流行起来,它的缺点是,难以阅读,难以理解,下面是一个使用案例
Hook
在上面的两种方法中,它们最终的目的是为了向组件注入 windowWidth 这个状态,为了这一个目的它们用了复杂又不直观的方法,有没有办法直观呢?那就是 Hook 了,
还是上面相同的需求,以下为Hook的实现方式
import { useState, useEffect } from "react";
const useWindowsWidth = () => {
const [isScreenSmall, setIsScreenSmall] = useState(false);
let checkScreenSize = () => {
setIsScreenSmall(window.innerWidth < 600);
};
useEffect(() => {
checkScreenSize();
window.addEventListener("resize", checkScreenSize);
return () => window.removeEventListener("resize", checkScreenSize);
}, []);
return isScreenSmall;
};
export default useWindowsWidth;
import React from 'react'
import useWindowWidth from './useWindowWidth.js'
const MyComponent = () => {
const onSmallScreen = useWindowWidth();
return (
// Return some elements
)
}
Hook相比其他方案的优点:
- 提取逻辑出来非常容易
- 非常易于组合
- 可读性非常强
- 没有名字冲突问题
React 自带 Hook 详解
useState
useState 是最基础的一个Hook,为什么这么说呢,因为它是状态生产器。它产生的状态和普通变量有什么区别呢?
const [count, setCount] = useState(initialCount);
---------
const count = 1
const setCount = (value) => count = value
这两个有什么区别呢?区别就在于第一个useState产生的是状态,状态改变的时候组件会重新渲染,它是响应式的;而第二个,就是一个普通变量,它改变什么都不会发生,听起来是不是有点可怜呢
useEffect
有了useState产生的状态,就可以写一些简单的组件了,以下是一个简单的计数组件
const Count = () => {
const [count, setCount] = useState(0)
const add = setCount(count + 1)
return <button onClick={add}>add</button>
}
在真实的代码中,通常需要和这个组件外面的世界产生联系,组件的状态,也要和外面的世界同步,才能产生工业的价值。通常发生在外面的事情被称为副作用
比如说当想将count和服务器的代码同步,将count和手机的震动同步,这时候就需要用到useEffect了。要摒弃以前的生命周期的概念,useEffect的唯一作用就是同步副作用。
useContext
React 的组件化可以将不同的业务代码分割开,但是也带来了一个问题,那就是组件间共享状态是非常不方便的。比如,有个很多组件都会用到的状态,app 主题状态,如何让一个组件随时可以获取到这个状态呢?首先想到的可能是状态提升,但是它只能缓解却并不能解决这个问题。而context就是为了解决这个问题,context 可以被看作是React自带的Redux,实际上Redux就是用context实现的
useReducer
上文中提到useState是主要的状态生产器,那么useReducer就是另一个没那么常用的状态生产器。它适合状态逻辑很复杂的时候,或者下一个state值依赖于上一个state值,比如
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>
</>
);
}
这种情况用useState当然也可以,但是用useReducer就显得代码干净漂亮
useCallback/useMemo
现在有这样一个问题:父组件刷新,所有的子组件都会跟着刷新,这句话对吗?
这句话是对的,父组件刷新,所有的子组件都会刷新,这样听起来很耗性能,但是对于绝大多数组件来说,性能都是没有问题的,因为React真的很快
但是对于耗性能的组件来说,这样就有很大的问题了,耗性能的组件不希望被经常刷新,所以可以用 React.memo包裹住它们,这样只有在它们的props变化的时候它们才会刷新
这样又有一个问题,比如:
const TestComp = () => {
const value = {name: 'Jack'}
return <MemoExpensiveList value={value}/>
}
已知MemoExpensiveList是被React.memo给处理过的,它的props变化它才会刷新。但是在上面这个案例里,TestComp一刷新MemoExpensiveList就会刷新,这是为什么呢?原因就是,onClick在每次TestComp刷新时都会生成一个新的实例,{name: 'Jack'} ≠= {name: 'Jack'}
这就是 useMemo派上用场的时候了,我们可以用useMemo包裹住:
const value = useMemo(() => {}, [])
这样它只会生成一个实例,也就不会骚扰到MemoExpensiveList了
而useCallback就是一个特殊版本的useMemo,专门来处理函数的
useRef
上面详细讲解了状态的概念,有时候会面临这样的需求,希望创建一种类型的值,它不是状态,但是又可以在不同的render之间以同一个实例的形式存在。它有点类似于在class component里的 this.xxx
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
自定义 Hook
自定义Hook是目前最好的重用React逻辑的方法,它和普通的函数很像很像,自定义Hook的特殊之处在于,它是有状态的,它返回的也是状态。通常在需要抽象出处理状态的逻辑的时候可以使用自定义Hook,例如
const Comp = () => {
const [arr, setArr] = useState([1, 2])
return <button onClick={() => setArr([...arr, value])}>add</button>
}
如果项目中有好几处这种数组处理,则可以使用以下方式处理
export const useArray = <T>(initialArray: T[]) => {
const [value, setValue] = useState(initialArray);
return {
value,
setValue,
add: (item: T) => setValue([...value, item]),
clear: () => setValue([]),
removeIndex: (index: number) => {
const copy = [...value];
copy.splice(index, 1);
setValue(copy);
},
};
};
用这样一个自定义的Hook,不仅返回了状态,也返回了处理这个状态的方法
这个例子也展示了,自定义Hook可以以状态为核心,并将它和与它相关的东西封装在一起。这也符合关注点分离的原则
另一个示例:
const Comp = () => {
const [id, setId] = useState(0)
const [assets, setAssets] = useState()
useEffect(() => {
fetch(`https://google.com?id=${id}`).then(async response => {
const data = await response.json();
if (response.ok) {
setAssets(data)
} else {
return Promise.reject(data);
}
})
}, [])
return <div>{assets.map(a => a.name)}</div>
}
这里的fetch的内容和这个组件关系大吗?不大,因为这个组件其实不怎么在乎fetch的细节,它只在乎拿到result.data,那么按照以下方式使用hook来封装
// util.ts
const useAssets = (id) => {
const [assets, setAssets] = useState()
useEffect(() => {
fetch(`https://google.com?id=${id}`).then(async response => {
const data = await response.json();
if (response.ok) {
setAssets(data)
} else {
return Promise.reject(data);
}
})
}, [])
return assets
}
// comp.tsx
const Comp = () => {
const [id, setId] = useState(0)
const assets = useAssets(id)
return <div>{assets.map(a => a.name)}</div>
}