引言 & 背景
在使用React开发相关页面或维护相关组件时,常常会遇到一些难以避免的问题,比如:
- 组件中与状态相关的逻辑很难复用,例如相似弹层组件的打开、关闭、loading等
- 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面。
- class组件中的this增加学习成本,class组件在基于现有工具的优化上存在些许问题。
- 由于业务变动,函数组件不得不改为类组件等等。
为了解决这类问题,目前常见的解决方法为render props 与 higher-order components,但这两种方式可能会很笨重,而且会导致JSX嵌套过深。
// 普通方法
import React from 'react'
import ReactDOM from 'react-dom'
const App = React.createClass({
getInitialState() {
return { x: 0, y: 0 }
},
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
})
},
render() {
const { x, y } = this.state
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
<h1>The mouse position is ({x}, {y})</h1>
</div>
)
}
})
ReactDOM.render(<App/>, document.getElementById('app'))
复制代码
// mixin
// 缺点ES6 classes,state修改来源很难追踪,多个mixin名称冲突
import React from 'react'
import ReactDOM from 'react-dom'
// This mixin contains the boilerplate code that
// you'd need in any app that tracks the mouse position.
// We can put it in a mixin so we can easily share
// this code with other components!
const MouseMixin = {
getInitialState() {
return { x: 0, y: 0 }
},
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
})
}
}
const App = React.createClass({
// Use the mixin!
mixins: [ MouseMixin ],
render() {
const { x, y } = this.state
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
<h1>The mouse position is ({x}, {y})</h1>
</div>
)
}
})
ReactDOM.render(<App/>, document.getElementById('app'))
复制代码
// HOC
// 缺点JSX嵌套,props来源很难确定,多个HOC导致props冲突以及props传递问题
const withMouse = (Component) => {
return class extends React.Component {
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
<Component {...this.props} mouse={this.state}/>
</div>
)
}
}
}
const App = React.createClass({
render() {
// Instead of maintaining our own state,
// we get the mouse position as a prop!
const { x, y } = this.props.mouse
return (
<div style={{ height: '100%' }}>
<h1>The mouse position is ({x}, {y})</h1>
</div>
)
}
})
// Just wrap your component in withMouse and
// it'll get the mouse prop!
const AppWithMouse = withMouse(App)
复制代码
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
// Instead of using a HOC, we can share code using a
// regular component with a render prop!
class Mouse extends React.Component {
static propTypes = {
render: PropTypes.func.isRequired
}
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
)
}
}
const App = React.createClass({
render() {
return (
<div style={{ height: '100%' }}>
<Mouse render={({ x, y }) => (
// The render prop gives us the state we need
// to render whatever we want here.
<h1>The mouse position is ({x}, {y})</h1>
)}/>
</div>
)
}
})
ReactDOM.render(<App/>, document.getElementById('app'))
// render props -> hoc
const withMouse = (Component) => {
return class extends React.Component {
render() {
return <Mouse render={mouse => (
<Component {...this.props} mouse={mouse}/>
)}/>
}
}
}
复制代码
React Hooks概述
基于上述原因,react团队在React 16.7.0-alpha
推出了React Hooks这一新的特性。
首先用一段简单的代码介绍一下什么是React Hooks
import { useState, useEffect } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
复制代码
什么是React Hooks? React Hooks是一种React中使用的特殊的函数,例如**useState**可以使我们在React函数组件中使用state,**useEffect**可以使我们在函数式组件中处理副作用。
React Hooks 带来的好处不仅是 “更 FP,更新粒度更细,代码更清晰”,还有如下三个特性:
- 多个状态不会产生嵌套,写法还是平铺的(renderProps 可以通过 compose 解决,可不但使用略为繁琐,而且因为强制封装一个新对象而增加了实体数量(每次
render
都会生成全新的组件,这对内存是一种挑战))。 - Hooks 可以引用其他 Hooks。
- 更容易将组件的 UI 与状态分离。
React Hooks生命周期
useState 什么时候执行? 它会在组件每次render的时候调用
useEffect 什么时候执行? 它会在组件 mount 和 unmount 以及每次重新渲染的时候都会执行,也就是会在 componentDidMount、componentDidUpdate、componentWillUnmount 这三个时期执行。
清理函数(clean up)什么时候执行? 它会在前一次 effect执行后,下一次 effect 将要执行前,以及 Unmount 时期执行
React Hooks 组合与自定义Hooks
import { useState, useEffect } from "react";
// 底层 Hooks, 返回布尔值:是否在线
function useFriendStatusBoolean(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
// 上层 Hooks,根据在线状态返回字符串:Loading... or Online or Offline
function useFriendStatusString(props) {
const isOnline = useFriendStatusBoolean(props.friend.id);
if (isOnline === null) {
return "Loading...";
}
return isOnline ? "Online" : "Offline";
}
// 使用了底层 Hooks 的 UI
function FriendListItem(props) {
const isOnline = useFriendStatusBoolean(props.friend.id);
return (
<li style={{ color: isOnline ? "green" : "black" }}>{props.friend.name}</li>
);
}
// 使用了上层 Hooks 的 UI
function FriendListStatus(props) {
const statu = useFriendStatusString(props.friend.id);
return <li>{statu}</li>;
}
复制代码
React Hooks 应用举例
利用 useState 创建 Redux
// 这就是 Redux
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
// 一个 Action
function useTodos() {
const [todos, dispatch] = useReducer(todosReducer, []);
function handleAddClick(text) {
dispatch({ type: "add", text });
}
return [todos, { handleAddClick }];
}
// 绑定 Todos 的 UI
function TodosUI() {
const [todos, actions] = useTodos();
return (
<>
{todos.map((todo, index) => (
<div>{todo.text}</div>
))}
<button onClick={actions.handleAddClick}>Add Todo</button>
</>
);
}
复制代码
利用 hooks取代生命周期
// react调用g2
class Component extends React.PureComponent<Props, State> {
private chart: G2.Chart = null;
private rootDomRef: React.ReactInstance = null;
componentDidMount() {
this.rootDom = ReactDOM.findDOMNode(this.rootDomRef) as HTMLDivElement;
this.chart = new G2.Chart({
container: document.getElementById("chart"),
forceFit: true,
height: 300
});
this.freshChart(this.props);
}
componentWillReceiveProps(nextProps: Props) {
this.freshChart(nextProps);
}
componentWillUnmount() {
this.chart.destroy();
}
freshChart(props: Props) {
// do something
this.chart.render();
}
render() {
return <div ref={ref => (this.rootDomRef = ref)} />;
}
}
// hooks调用g2
function App() {
const ref = React.useRef(null);
let chart: G2.Chart = null;
React.useEffect(() => {
if (!chart) {
chart = new G2.Chart({
container: ReactDOM.findDOMNode(ref.current) as HTMLDivElement,
width: 500,
height: 500
});
}
// do something
chart.render();
return () => chart.destroy();
});
return <div ref={ref} />;
}
复制代码
修改页面 title
function useDocumentTitle(title) {
useEffect(
() => {
document.title = title;
return () => (document.title = "大宝");
},
[title]
);
}
// 使用
useDocumentTitle("包裹复核");
复制代码
监听页面大小变化
function getSize() {
return {
innerHeight: window.innerHeight,
innerWidth: window.innerWidth,
outerHeight: window.outerHeight,
outerWidth: window.outerWidth
};
}
function useWindowSize() {
let [windowSize, setWindowSize] = useState(getSize());
function handleResize() {
setWindowSize(getSize());
}
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return windowSize;
}
// 使用
const windowSize = useWindowSize();
return <div>页面高度:{windowSize.innerWidth}</div>;
复制代码
其他
- DOM 副作用修改 / 监听。
- React组件辅助函数。
- 处理动画相关逻辑。
- 处理发送请求。
- 处理表单填写。
- 存数据。
注意
- 只能在顶层代码(Top Level)中调用 Hook
- 不要在循环,条件判断,嵌套函数里面调用 Hooks
- 只在 React 的函数里面调用 Hooks
- 命名时使用use*命名Hooks
- 使用 eslint-plugin-react-hooks 插件进行检查
参考文档
- React官方文档
- 精读《React Hooks》
- 精读《怎么用 React Hooks 造轮子》
- 10分钟了解 react 引入的 Hooks
- [译] React hooks: 不是魔法,只是数组
- Use a Render Prop!
码字不易,如有建议请扫码