React Hooks 是 React 16.7.0-alpha
版本推出的新特性,想尝试的同窗安装此版本便可。html
React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 以后的第三种状态共享方案,不会产生 JSX 嵌套地狱问题。前端
状态共享可能描述的不恰当,称为状态逻辑复用会更恰当,由于只共享数据处理逻辑,不会共享数据自己。react
不久前精读分享过的一篇 Epitath 源码 - renderProps 新用法 就是解决 JSX 嵌套问题,有了 React Hooks 以后,这个问题就被官方正式解决了。
为了更快理解 React Hooks 是什么,先看笔者引用的下面一段 renderProps 代码:git
function App() { return ( <Toggle initial={false}> {({ on, toggle }) => ( <Button type="primary" onClick={toggle}> Open Modal </Button> <Modal visible={on} onOk={toggle} onCancel={toggle} /> )} </Toggle> ) }
恰巧,React Hooks 解决的也是这个问题:github
function App() { const [open, setOpen] = useState(false); return ( <> <Button type="primary" onClick={() => setOpen(true)}> Open Modal </Button> <Modal visible={open} onOk={() => setOpen(false)} onCancel={() => setOpen(false)} /> </> ); }
能够看到,React Hooks 就像一个内置的打平 renderProps 库,咱们能够随时建立一个值,与修改这个值的方法。看上去像 function 形式的 setState,其实这等价于依赖注入,与使用 setState 相比,这个组件是没有状态的。npm
React Hooks 带来的好处不只是 “更 FP,更新粒度更细,代码更清晰”,还有以下三个特性:redux
第二点展开说一下: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>; }
这个例子中,有两个 Hooks:useFriendStatusBoolean
与 useFriendStatusString
, useFriendStatusString
是利用 useFriendStatusBoolean
生成的新 Hook,这两个 Hook 能够给不一样的 UI:FriendListItem
、FriendListStatus
使用,而由于两个 Hooks 数据是联动的,所以两个 UI 的状态也是联动的。框架
顺带一提,这个例子也能够用来理解 对 React Hooks 的一些思考 一文的那句话:“有状态的组件没有渲染,有渲染的组件没有状态”:函数
useFriendStatusBoolean
与 useFriendStatusString
是有状态的组件(使用 useState
),没有渲染(返回非 UI 的值),这样就能够做为 Custom Hooks 被任何 UI 组件调用。FriendListItem
与 FriendListStatus
是有渲染的组件(返回了 JSX),没有状态(没有使用 useState
),这就是一个纯函数 UI 组件,Redux 的精髓就是 Reducer,而利用 React Hooks 能够轻松建立一个 Redux 机制:
// 这就是 Redux function useReducer(reducer, initialState) { const [state, setState] = useState(initialState); function dispatch(action) { const nextState = reducer(state, action); setState(nextState); } return [state, dispatch]; }
这个自定义 Hook 的 value 部分看成 redux 的 state,setValue 部分看成 redux 的 dispatch,合起来就是一个 redux。而 react-redux 的 connect 部分作的事情与 Hook 调用同样:
// 一个 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> </> ); }
useReducer
已经做为一个内置 Hooks 了,在这里能够查阅全部 内置 Hooks。
不过这里须要注意的是,每次 useReducer
或者本身的 Custom Hooks 都不会持久化数据,因此好比咱们建立两个 App,App1 与 App2:
function App1() { const [todos, actions] = useTodos(); return <span>todo count: {todos.length}</span>; } function App2() { const [todos, actions] = useTodos(); return <span>todo count: {todos.length}</span>; } function All() { return ( <> <App1 /> <App2 /> </> ); }
这两个实例同时渲染时,并非共享一个 todos 列表,而是分别存在两个独立 todos 列表。也就是 React Hooks 只提供状态处理方法,不会持久化状态。
若是要真正实现一个 Redux 功能,也就是全局维持一个状态,任何组件 useReducer
都会访问到同一份数据,能够和 useContext 一块儿使用。
大致思路是利用 useContext
共享一份数据,做为 Custom Hooks 的数据源。具体实现能够参考 redux-react-hook。
在 useState 位置附近,可使用 useEffect 处理反作用:
useEffect(() => { const subscription = props.source.subscribe(); return () => { // Clean up the subscription subscription.unsubscribe(); }; });
useEffect
的代码既会在初始化时候执行,也会在后续每次 rerender 时执行,而返回值在析构时执行。这个更多带来的是便利,对比一下 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)} />; } }
用 React Hooks 能够这么作:
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} />; }
能够看到将细碎的代码片断结合成了一个完整的代码块,更维护。
如今介绍了 useState
useContext
useEffect
useRef
等经常使用 hooks,更多能够查阅:内置 Hooks,相信不久的将来,这些 API 又会成为一套新的前端规范。
Hook 函数必须以 "use" 命名开头,由于这样才方便 eslint 作检查,防止用 condition 判断包裹 useHook 语句。
为何不能用 condition 包裹 useHook 语句,详情能够见 官方文档,这里简单介绍一下。
React Hooks 并非经过 Proxy 或者 getters 实现的(具体能够看这篇文章 React hooks: not magic, just arrays),而是经过数组实现的,每次 useState
都会改变下标,若是 useState
被包裹在 condition 中,那每次执行的下标就可能对不上,致使 useState
导出的 setter
更新错数据。
虽然有 eslint-plugin-react-hooks 插件保驾护航,但这第一次将 “约定优先” 理念引入了 React 框架中,带来了史无前例的代码命名和顺序限制(函数命名遭到官方限制,JS 自由主义者也许会暴跳如雷),但带来的便利也是史无前例的(没有比 React Hooks 更好的状态共享方案了,约定带来提效,自由的代价就是回到 renderProps or HOC,各团队能够自行评估)。
笔者认为,React Hooks 的诞生,也许来自于这个灵感:“不如经过增长一些约定,完全解决状态共享问题吧!”
React 约定大于配置脚手架 nextjs umi 以及笔者的 pri 都经过有 “约定路由” 的功能,大大下降了路由配置复杂度, 那么 React Hooks 就像代码级别的约定,大大下降了代码复杂度。
由于 React Hooks 的特性,若是一个 Hook 不产生 UI,那么它能够永远被其余 Hook 封装,虽然容许有反作用,可是被包裹在 useEffect
里,整体来讲仍是挺函数式的。而 Hooks 要集中在 UI 函数顶部写,也很容易养成书写无状态 UI 组件的好习惯,践行 “状态与 UI 分开” 这个理念会更容易。
不过这个理念稍微有点蹩脚的地方,那就是 “状态” 究竟是什么。
function App() { const [count, setCount] = useCount(); return <span>{count}</span>; }
咱们知道 useCount
算是无状态的,由于 React Hooks 本质就是 renderProps 或者 HOC 的另外一种写法,换成 renderProps 就好理解了:
<Count>{(count, setCount) => <App count={count} setCount={setCount} />}</Count>; function App(props) { return <span>{props.count}</span>; }
能够看到 App 组件是无状态的,输出彻底由输入(Props)决定。
那么有状态无 UI 的组件就是 useCount
了:
function useCount() { const [count, setCount] = useState(0); return [count, setCount]; }
有状态的地方应该指 useState(0)
这句,不过这句和无状态 UI 组件 App 的 useCount()
很像,既然 React 把 useCount
成为自定义 Hook,那么 useState
就是官方 Hook,具备同样的定义,所以能够认为 useCount
是无状态的,useState
也是一层 renderProps,最终的状态实际上是 useState
这个 React 内置的组件。
咱们看 renderProps 嵌套的表达:
<UseState> {(count, setCount) => ( <UseCount> {" "} {/**虽然是透传,但给 count 作了去重,不可谓没有做用 */} {(count, setCount) => <App count={count} setCount={setCount} />} </UseCount> )} </UseState>
能肯定的是,App 必定有 UI,而上面两层父级组件必定没有 UI。为了最佳实践,咱们尽可能避免 App 本身维护状态,而其父级的 RenderProps 组件能够维护状态(也能够不维护状态,作个二传手)。所以能够考虑在 “有状态的组件没有渲染,有渲染的组件没有状态” 这句话后面加一句:没渲染的组件也能够没状态。
把 React Hooks 看成更便捷的 RenderProps 去用吧,虽然写法看上去是内部维护了一个状态,但其实等价于注入、Connect、HOC、或者 renderProps,那么如此一来,使用 renderProps 的门槛会大大下降,由于 Hooks 用起来实在是太方便了,咱们能够抽象大量 Custom Hooks,让代码更加 FP,同时也不会增长嵌套层级。
讨论地址是: 精读《React Hooks》 · Issue #111 · dt-fe/weekly
若是你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。