1. 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
所以 Hook 并不是一拍脑门横空出世的产物,不理解这段思路也是无法完全理解 Hook的
下面我会发很多代码截图,为了让大家跟上节奏,大家只需要结合我讲的话题大概浏览这些代码截图,不需要关注太多细节
1. 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,难以维护,总之,现在已经被完全淘汰了
给大家看一下官方判决书:reactjs.org/blog/2016/0…
2. 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)
复制代码
经典的 容器组件与展示组件分离 (separation of container presidential) 就是从这里开始的
一个很经典的HOC使用案例是react redux 中的 connect 方法,AddTodo组件像一只无辜的小白兔,它的addTodo方法是connect方法给它注入进去的
- 可以在任何组件包括 Class Component 中工作
- 它所倡导的 容器组件与展示组件分离 原则做到了:关注点分离
缺点:
- 不直观,难以阅读
- 名字冲突
- 组件层层层层层层嵌套
3. 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流行起来,它的缺点是,难以阅读,难以理解,下面是一个使用案例
4. 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相比其他方案的优点:
- 提取逻辑出来非常容易
- 非常易于组合
- 可读性非常强
- 没有名字冲突问题
Hook分两种,React自带Hook和自定义Hook,自定义Hook是有自带Hook组合而成的,所以我们先讲一下自带Hook
2. React 自带 Hook 详解
1. useState
useState 是最基础的一个Hook,为什么这么说呢,因为它是状态生产器。它产生的状态和普通变量有什么区别的?
const [count, setCount] = useState(initialCount);
---------
const count = 1
const setCount = (value) => count = value
复制代码
这两个有什么区别呢?区别就在于第一个useState产生的是状态,状态改变的时候组件会重新渲染,它是响应式的;而第二个,就是一个普通变量,它改变什么都不会发生,听起来是不是有点可怜呢
2. useEffect
有了useState产生的状态,我们就可以写一些简单的组件了,比如
const Count = () => {
const [count, setCount] = useState(0)
const add = setCount(count + 1)
return <button onClick={add}>add</button>
}
复制代码
这样一个简单的计数组件
但是,这终归是自娱自乐,我们写的代码,要和这个组件外面的世界产生联系,我们的状态,要和外面的世界同步,才能产生工业的价值。我们将发生在外面的事情统称为副作用
比如说你想将count和服务器的代码同步,你想将count和手机的震动同步,这时候就需要用到useEffect了。要摒弃以前的生命周期的概念,useEffect的唯一作用就是同步副作用。
3. useContext
React 的组件化让我们可以将不同的业务代码分割开,但是也带来了一个问题,那就是组件间共享状态是非常不方便的。比如,你有个很多组件都会用到的状态,app 主题状态,如何让一个组件随时可以获取到这个状态呢?大家可能听过状态提升,它缓解但是并没有解决这个问题。而context就是为了解决这个问题,大家可以把它理解成是React自带的Redux,实际上Redux就是用context实现的
4. 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就显得代码干净漂亮
5. useCallback/useMemo
这里React官网文档讲的非常不清楚。
给大家出一个判断题:父组件刷新,所有的子组件都会跟着刷新,这句话对吗?
这句话是对的,父组件刷新,所有的子组件都会刷新,这样听起来很耗性能,但是对于绝大多数组件来说,性能都是没有问题的,应为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,专门来处理函数的
6. 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>
</>
);
}
复制代码
3. 自定义 Hook
自定义Hook是目前最好的重用React逻辑的方法,它和普通的函数很像很像,自定义Hook的特殊之处在于,它是有状态的,它返回的也是状态。所以在什么时候我们应该用到自定义Hook?那就是,我们想要抽象出处理状态的逻辑的时候
给大家举一个例子
const Comp = () => {
const [arr, setArr] = useState([1, 2])
return <button onClick={() => setArr([...arr, value])}>add</button>
}
复制代码
如果你发现你的app里有好几处这种数组处理,你可以
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可以以状态为核心,并将它和与它相关的东西封装在一起。这也符合我们编程的seperation of concert,也就是关注点分离的原则。关注点分离是大家写代码时一定要注意的事情,也就是说无关的代码不要放在一起,不然关注点混在一起,会让维护难度大大加大。
大家明天去看一下自己的代码,很可能会发现,有一些面条代码其实是可以用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>
}
复制代码
大家看,这样就实现了逻辑的分离
作者:太凉
链接:https://juejin.cn/post/6995889352400338951/
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。