- 盘点可用 Hooks 重写的几个场景
- ES2018 新特性梳理之 Promise.prototype.finally
- 为什么我不再使用 export default
- Hooks PR has just been merged
- 2019 React 技术栈学点什么
React Hooks 之 Hooks 之所以可以设计为 Hooks 的原因
盘点可用 Hooks 重写的几个场景
今天我们来探讨下,如何使用 React Hooks 来简化和统一我们的代码逻辑。
对样式 class 的切换说不
通常情况下,如果你对「贫血模型」有一定的恪守,就不会(也不应该)在组件内找到很多业务逻辑,它的最佳归宿应该是为我们提供视图渲染的能力。即使存在一些逻辑,通常也应该是样式计算、向后兼容性、一些最小的DOM操作等等。
基于这一点,我们来看一个以函数式组件的形式实现的对话框组件:import React from 'react'
const Dialog = ({ isOpen, title, content }) => {
return isOpen && (
{title}
{content}
)
}
当然,这是一个非常简单的示例,它看起来会有所不同,这具体取决于我们对样式具体的实现。
上面的逻辑就是,当我们传入的 propsisOpen
改变时,如果其为true
,则展示出来;如果为false
,则隐藏起来。
现在我们考虑一种需求,当我们打开对话框时,要锁定滚动。现如今,如果不使用新的特性,我们的实现方式自然是 class 组件的形式了。首先我们把上面的函数式组件改为下面的形式:import React from 'react'
class Dialog extends React.Component {
render() {
const { isOpen, title, content } = this.props
return isOpen && (
{title}
{content}
)
}
}
然后我们使用生命周期钩子来改变文档上的
body
元素的 class 或 style:import React from 'react'
class Dialog extends React.Component {
componentDidMount() {
const { isOpen } = this.props
document.body.style.overflow = isOpen ? 'hidden' : 'visible'
}
componentDidUpdate() {
const { isOpen } = this.props
document.body.style.overflow = isOpen ? 'hidden' : 'visible'
}
render() {
const { isOpen, title, content } = this.props
return isOpen && (
{title}
{content}
)
}
}
上面的场景其实很常见,比如最近我也遇到了切换路由时要动态修改 title 的情形,这些需求都是类似的。
就这种小事情,过去场景简单时所写的函数式组件,就要面临无情的重构。我们还会发现,在组件中的两个不同生命周期方法上编写的代码,可能在类似场景下是基本相同。
这里我们就可以用useEffect
来重写:import React, { useEffect } from 'react'
const Dialog = ({ isOpen, title, content }) => {
useEffect(() => {
document.body.style.overflow = isOpen ? 'hidden' : 'visible'
return () => (document.body.style.overflow = 'visible')
}, [isOpen])
return isOpen && (
{title}
{content}
)
}
妙不可言啊!每次 Dialog 实例传入的 props
isOpen
发生变化,useEffect
就会被调用。在其内部,我们可以考虑用isOpen
来做些什么变化。
返回的方法将会在组件被卸载的时候执行,这里我们将清理我们的body
元素的样式,或者做些其他的小的 DOM 复原操作。
最后,我们传入的第二个参数isOpen
,正是告诉isOpen
我们只希望在isOpen
改变的时候执行useEffect
。
通过这种方式,我们就避免了必须要用 class 来重构函数式组件的麻烦,当然,这样更为优雅。
如果你想复习useEffect
,文档见这里。到处都要用到的小操作
这一场景说的是那些重复使用了一遍又一遍的小操作。
我们非常有可能遇到的一个情况是,我们上面说的那个锁定滚动的功能,遮罩层组件要,抽屉组件要,模态框还要,但他们要的东西,不都是一样的吗?使用 class 组件,我们不能共享生命周期方法。但是,当我们使用 Hooks 将这些逻辑抽离出来时,我们就可以在各个需要用到这些逻辑的地方将 Hooks import 进来用就好了!
真骚,Hooks 天生就是抽离逻辑用的。
我们基于上面的例子,对锁定滚动的逻辑进行抽离:import { useEffect } from 'react'
function useLockBodyScroll(toggle) {
useEffect(
() => {
document.body.style.overflow = toggle ? 'hidden' : 'visible'
return () => (document.body.style.overflow = 'visible')
},
[toggle]
);
}
export default useLockBodyScroll
逻辑被抽出来了,原先的组件要怎么使用呢?我们回退到最初的函数式组件来使用抽离的 Hooks:
import React from 'react'
import useLockBodyScreen from './hooks/useLockBodyScreen'
const Dialog = ({ isOpen, title, content }) => {
useLockBodyScreen(isOpen)
return isOpen && (
{title}
{content}
)
}
在这里看下 demo
现在,这一共有的行为就被抽离出来,并可以被各个组件所共享。
这样做的好处有很多。比如说原先这些本可共享的逻辑,因为散落在各个组件中,我们的测试就需要覆盖的比较多了。如今,这一逻辑被抽离并复用,我们的测试可以只针对这一小块进行了。
如果你想复习自定义 Hooks,文档见这里。向笨重的 state 说再见
state 本身并不笨重,但是,如果我们搞了一大堆 class 组件,而使用 class 的原因仅仅是因为我们需要一些条件判断来渲染树的一部分货另一部分,那确实是有些坑爹了。
我们看个菜单及下拉的例子:import React from 'react'
class Menu extends React.Component {
state = { open: false }
toggleMenu = () => {
this.setState({ open: !this.state.open })
}
render() {
const { open } = this.state
const { options } = this.props
return (
Menu
{open && (
{options.map(item => (
{item.text}
))}
)}
)
}
}
像这个组件,我们会使用 class 来写的唯一原因,就是因为我们需要一个状态来提供展示或隐藏的选项,但是这个问题,我们可以用
useState
来轻松替代:import React, { useState } from 'react'
const Menu = ({ options }) => {
const [open, setOpen] = useState(false)
return (
setOpen(!open))} >
Menu
{open && (
{options.map(item => (
{item.text}
))}
)}
)
}
我们传递给
useState
的参数是初始值,而每次调用setOpen
都会改变isOpen
。做到这一切,我们压根不需要 class。
如果你想复习 useState,文档见这里。最后
不管复杂场景里 Hooks 能否经受住考验,至少上述三种模式非常适合使用 Hooks。尝试着用起来吧!
源地址:https://jeremenichelli.io/2019/01/how-hooks-might-shape-design-systems-built-in-react/ES2018 新特性梳理之 Promise.prototype.finally
Promise.prototype.finally
ES2018 中的另一个有趣的东西就是
finally()
方法,一些 JavaScript 已经实现了类似的功能,而且在实践中证实了这一方法的有效性,这就激励着 ECMA 技术委员会官方增加了对finally()
的支持。有了这一方法,我们就可以像 try-catch-finally 一样,无论 promise 会走向何方,都可以执行一段想要的代码。
我们来看一个简单的例子:fetch('https://www.google.com')
.then((response) => {
console.log(response.status);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
document.querySelector('#spinner').style.display = 'none';
});
如果我们不管 promise 是否成功,都要在完成后进行一些清理,
finally()
方法会派上用场。在上面的代码中,finally()
方法在获取和处理数据后将小菊花隐藏了起来。我们不需要在then()
和catch()
中复制上相同的逻辑,而是在 promise resolve 或 reject 后执行一次即可。
当然,你也可以使用promise.then(func, func)
的写法来代替上述实现,方法如下:fetch('https://www.google.com')
.then((response) => {
console.log(response.status);
})
.catch((error) => {
console.log(error);
})
.then(final, final);
function final() {
document.querySelector('#spinner').style.display = 'none';
}
和
then()
及catch()
一样,finally()
也总是返回一个 promise,所以我们能够继续链式调用。通常情况下,finally()
应该会是我们的最后一环,但总有一些特定场景,例如 HTTP 请求,就是一个适合继续跟着一个链式调用的场景:跟着一个catch()
来处理在finally()
中可能发生的错误。兼容性
| Chrome | Firefox | Safari | Edge | | —- | —- | —- | —- | | 63 | 58 | 11.1 | 18 |
Chrome Android | Firefox Android | iOS Safari | Edge Mobile | Samsung Internet | Android Webview |
---|---|---|---|---|---|
63 | 58 | 11.1 | No | 8.2 | 63 |
对应 Node.js,兼容性如下:
- 10.0.0 (完全支持)
源地址:https://css-tricks.com/new-es2018-features-every-javascript-developer-should-know
为什么我不再使用 export default
大佬 Nicholas C. Zakas 上周发推表示,他不会再在 CommonJS/ES6 中使用 export default 的写法。
大佬就是大佬,一条推特引发争议,我就是说 JS 是最烂的语言肯定也是零回复。
下面就来介绍下大佬是如何澄清他的观点的。如果想直接看原因,直接去我所遇到的问题。
一些澄清
- 无法知道 export 的是一个函数还是一个类是他言论的理由之一,而这一问题可以通过命名了的 export 解决,且命名 export 可以应对的问题不止于此。
- 不仅在自己的项目中会遇到这类问题,在其他依赖的库或工具模块中也会遇到。这说明文件命名的约定并不能解决所有的问题。
- 并不是说所有人都应该舍弃默认 export,而是说他在自己所写的模块中,宁肯不用默认 export。
export default:首选项
默认 export 最早在 CommonJS 的时候火起来的,在 CommonJS 中,export 一个默认值的写法如下:
这段代码 exports 了class LinkedList {}
module.exports = LinkedList;
LinkedList
类,但是并没有指定名字。如果文件名是linked-list.js
,你就可以在其他 CommonJS 模块内这样引入默认值:const LinkedList = require("./linked-list");
require()
函数会返回linked-list.js
中的内容,并用LinkedList
来命名,但我也同样可以用其他名字如foo
、Mountain
这类标识符来命名。
CommonJS 的默认模块导出的流行,意味着 JavaScript 模块被设计成支持如下模式:ES6 favors the single/default export style, and gives the sweetest syntax to importing the default. — David Herman June 19, 2014
所以在 JavaScript 模块中,你可以像下面一样 export 一个默认值:
export default class LinkedList {}
然后像这样 import:
import LinkedList from "./linked-list.js";
named exports:可选项
除了默认导出,CommonJS 和 JavaScript 模块都支持命名导出。命名导出允许模块将函数名、类或变量传入到引用它们的文件中。
在 CommonJS 里,你可以通过在 exports
对象上附加一个名字来创建一个命名导出,如下:
exports.LinkedList = class LinkedList {};
然后可以在其他文件中这样引入:
const LinkedList = require("./linked-list").LinkedList;
当然,我可以使用任何我想要的名字,只是这里我选择使用了和导出名 LinkedList
相同的变量名。
在 JavaScript 模块中,一个命名导出是下面这种写法:
import { LinkedList } from "./linked-list.js";
在这里,我们就不能随便指定 LinkedList
的标识符,而是必须与导出的名字匹配。这就是与 CommonJS 的一个显著差异。
所以,两种模块类型的能力都支持默认和命名导出。
个人偏好
在更进一步之前,我想明确下一些个人的喜好。下面的是一些我个人写代码的原则,无论是什么语言:
- 清晰而非隐晦。一个东西做了什么,什么东西应该被调用等等,无论如何都应该保证清晰。
- 所有文件中用到的名字都要保持一致。
- 抛异常的时机要早且频繁。
- 更少的决策意味着更迅捷的开发。这许多的偏好都是为了消灭开发过程中所要做的不必要的决策。每个你做的决策都会让你减速,这也就是为什么代码约定会带来更高的开发效率。我希望能够都决定好,然后尽情开干。
- 回过去看代码会降低开发效率。就是类似做阅读理解,不要总是回过去反复看,会浪费时间。我们要尝试着写一些不要总是回过去查看的代码。
- 过度认知会降低开发效率。简单就好,过多的细节会使得你耗费记忆力,从而降低开发效率。
我所遇到的问题
使用 default export 所遇到的问题是什么呢?是什么
正如开始时所讲的那样,当我们使用一个不熟悉的模块或文件,我们可能很难搞清楚返回的东西究竟是什么,举例如下:
在这个上下文中,const list = require("./list");
list
究竟是什么呢?不太可能是个基本类型值,逻辑上讲应该是函数、类或其他类型的对象。那我该怎么确认呢?我需要回过去看看了。
- 如果我有
list.js
的源码,那么我们可以打开源码看看 export 了什么 - 如果没有,我可能得打开什么文档看看
无论如何,这都带来了额外的记忆量来避免下次再次遇到这种问题。如果我们使用 export default,这类额外的脑力成本就会持续困扰我们。
命名匹配的问题
命名导出需要模块至少指定导出内容的名字,好处就是我可以在各个地方搜索到 LinkedList
并且知道所有指向该量的地方。而默认导出则会带来一些额外的思考。你需要决定正确的命名规则,这同样耗费精力。你需要确保每个在这个应用上开发的人都是用相同的命名。
引入一个命名的导出量,就能保证至少引用到各个地方用到的同一个东西的同一个名字。即使你选择重命名,这一重命名的过程也是清晰显式的。在 CommonJS 中:
const MyList = require("./list").LinkedList;
而在 JavaScript 模块中:
import { LinkedList as MyList } from "./list.js"
不管哪种模块格式,我们都会显式声明 LinkedList
。
如果命名在代码中一致,你就可以很轻松的:
- 通过使用信息搜索代码
- 在整个代码库里重命名
当然,即使使用默认导出,也不会就做不到这些,但毫无疑问的是,更为复杂且更容易出错。
import 错误的东西
命名导出的一个明显的优点是,当我们试图去引入一个并不存在于模块内的东西时,会报错,比如下面这种:
import { LinkedList } from "./list.js";
如果 LinkedList
在 list.js
中不存在,那么就会报错。当然,像是 IDE 或者 ESLint 会很轻易地检测出 missing reference。
结论
杜绝 export default,提升开发效率。
源地址:https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/
Hooks PR has just been merged
千呼万唤始出来,Hooks 合并了 3 个 commits,为接下来的 release 做好了准备。
根据 size-bot 提供的数据,React 和 ReactDOM 的体积都有一定的增加,这主要是因为 Hooks API 是全新的接口,并不包含在过去稳定 builds 中。
源地址:https://github.com/facebook/react/pull/14679
React developer in 2019
React Hooks 之 Hooks 之所以可以设计为 Hooks 的原因
Overreacted 专栏又囍出一篇,今天来学习下 Dan 大佬的文章。
前言
自 React Hooks 的第一个 alpha 版本发布后,一个经常被讨论到的问题是:为什么一些其他的 API 并不是一个 Hook 呢?
这里我们简单回忆一下 Hooks:
- useState() 让我们声明一个状态变量
- useEffect() 让我们声明一个副作用
- useContext() 让我们获取一个上下文
但在 React 中,我们有其他的一些 API,例如 React.Demo()
和 <Context.Provider>
,它们并不是 Hooks。一般对这些 API 的关于 Hooks 版本的提案都是非组件化或者反模块的。这篇文章会帮助你理解原因。
有两个特性是 React API 需要具备的:
- Composition:很大程度上讲,自定义 Hooks 才是我们对 Hooks API 感到兴奋的原因。我们希望人们经常建立自己的 Hooks,我们需要确保不同人编写的 Hooks 不会发生冲突。
- Debugging: 我们希望随着应用的增长,定位 bug 能够保持的较为简单。React 的一个特别赞的能力是如果你看到什么东西渲染错误,你可以沿着渲染树往上找,知道你找到发生错误的那个 props 或 state。
这两个约束条件放在一起,就可以回答什么可以设计成 Hooks、什么不可以设计成 Hooks 的问题了。我们来看个例子。
真实 Hook: useState()
Composition
多组 custom Hooks 各自使用 useState() 不会发生冲突:
function useMyCustomHook1() {
const [value, setValue] = useState(0);
// What happens here, stays here.
}
function useMyCustomHook2() {
const [value, setValue] = useState(0);
// What happens here, stays here.
}
function MyComponent() {
useMyCustomHook1();
useMyCustomHook2();
// ...
}
新增一个无条件的 useState()
调用总是安全的。 你只需要声明一个新的状态变量,而不需要关心组件所使用到的其他 Hooks。你也不需要担心因为更新其中一个而破坏了其他状态变量。
总结: ✅ useState()
本身可以保持自定义组件的健壮性。
Debugging
Hooks 非常善于相互之间进行传值:
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
// ...
return width;
}
function useTheme(isMobile) {
// ...
}
function Comment() {
const width = useWindowWidth();
const isMobile = width < MOBILE_VIEWPORT;
const theme = useTheme(isMobile);
return (
{/* ... */}
);
}
如上所示,useWindowWidth()
返回的 width 作为 useTheme()
的参数传入。那么如果我们写出 bug 了怎么办呢?这个调试的过程是怎么样的呢?
假设我们从 theme.comment
得到的 CSS 类是错误的,我们该如何进行调试呢?我们可以在组件内部打断点或是打上一些 log。
可能我们看到 theme
是错误的,但是 width
和 isMobile
是正确的。这就意味着问题在 useTheme()
内部。又或者,我们发现 width
本身就有问题。这就告诉我们问题发生在 useWindowWidth()
。
简单看下一些中间量,我们就可以断定顶层的 Hooks 中哪个包含 bug,我们不需要再去看所有 Hooks 的实现了。然后我们只需要进入对应的 Hooks 继续查找,并重复上述的过程。
如果自定义 Hooks 嵌套的层数越来越深,这种定位方法就会越来越重要。想象下我们嵌套了 3 层的自定义 Hook,每一层使用了 3 个不同的自定义 Hooks,那么挨个排查过来,最惨需要看 3 + 3×3 + 3×3×3 = 39 处。幸运的 是,useState()
并不会影响到其他 Hooks 或组件,bug 一定是由 bug 引起的,这样我们也就需要 3 次就可找到问题。
总结: ✅ useState()
并不会掩盖代码之间的逻辑关系,我们只要拆分开一层层向下定位 bug 即可。
并不是 Hook: useBailout()
使用 Hooks 的组件可以避免重新渲染来进行性能优化。
一种方法是在整个组件周围包一个 React.memo()
。如果 props 与我们在上一次渲染过程中的 props 浅相等,就不再重新渲染,这类似于 class 中的 PureComponent
。React.memo()
接收一个组件并返回一个组件:
function Button(props) {
// ...
}
export default React.memo(Button);
那么为什么 React.memo() 没有设计成一个 Hook 呢?
或者我们可以叫它 useShouldComponentUpdate()
、useBailout()
、usePure()
或者 useShouldComponentUpdate()
,提案看上去就像这样:
function Button({ color }) {
// ⚠️ Not a real API
useBailout(prevColor => prevColor !== color, color);
return (
OK
)
}
Composition
我们试着在两个自定义 Hooks 里使用 useBailOut()
:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ⚠️ Not a real API
useBailout(prevIsOnline => prevIsOnline !== isOnline, isOnline);
useEffect(() => {
const handleStatusChange = status => setIsOnline(status.isOnline);
ChatAPI.subscribe(friendID, handleStatusChange);
return () => ChatAPI.unsubscribe(friendID, handleStatusChange);
});
return isOnline;
}
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
// ⚠️ Not a real API
useBailout(prevWidth => prevWidth !== width, width);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
return width;
}
那么,如果我们在同一个组件中同时使用这两个 Hooks,它将如何处理重新渲染的问题呢:
function ChatThread({ friendID, isTyping }) {
const width = useWindowWidth();
const isOnline = useFriendStatus(friendID);
return (
{isTyping && 'Typing...'}
);
}
如果每次调用 useBailout()
的时候都可能跳过 update,那么 useWindowWidth()
的更新就可能被 useFriendStatus()
干掉,反之亦然。这些 Hooks 会互相破坏对方的功能。
但是,如果仅在单个组件内的所有对 useBailout()
的调用都「同意」阻止更新时才真正使用 useBailout()
,那么我们的 ChatThread 将无法更新 isTyping prop 的更改。
更糟糕的是,使用这些语义,如果他们不调用 useBailout()
,任何新添加到 ChatThread 的 Hook 都会失败。
总结: 🔴 useBailout()
破坏了 composition。如果将它添加为一个 Hook,就会破坏其他 Hooks 的状态更新。
Debugging
一个像 useBailout()
的 Hook 会如何影响 debugging?
同样的例子:
function ChatThread({ friendID, isTyping }) {
const width = useWindowWidth();
const isOnline = useFriendStatus(friendID);
return (
{isTyping && 'Typing...'}
);
}
function ChatThread({ friendID, isTyping }) {
const width = useWindowWidth();
const isOnline = useFriendStatus(friendID);
return (
{isTyping && ‘Typing…’}
);
}
假设 typing… 的字样没有符合我们预期的出现,即使在 props 上的许多层正在发生变化。我们如何调试它?
一般来说,我们可以非常有信心的向上看。如果 ChatThread 并没得到一个新的 isTyping 值,我们就可以打开渲染 <ChatThread isTyping={myVar} />
的组件并检查 myvar
的值。我们既可能发现一个实现存在问题的 shouldComponentUpdate()
,也有可能发现一个错误的传递下去的 isTyping
值。对于每个组件,沿着链路通常就可以定位到问题。
然而,如果 useBailout()
真的是个 Hook,你就永远无法知道为什么一次更新被跳过,直到你检查了每一个哪怕是嵌套在深处使用的 Hook 才好断定。因为每个父组件都可能使用自定义 Hooks,所以随着代码嵌套的越来越多且深,问题会变得严重。
这就好像你在抽屉里寻找一把螺丝刀一样,每个抽屉里都装着一堆更小的抽屉柜,你不知道兔子洞有多深。
总结: 🔴 useBailout()
Hook 不仅破坏了 composition, 也会指数式的增加调试步骤及定位到问题未知。
总结
我们对一个真正的 Hook — useState()
和故意不是 Hook 的 useBailout()
进行了对比。我们通过 composition 和 debugging 的角度对二者进行了比较,并讨论了为什么其中一个可以设计为 Hook 而另一个不行。
虽然没有 memo()
或 shouldComponentUpdate()
的「Hook 版本」,但 React 确实提供了一个名为 useMemo()
的 Hook。它有类似的用途,但它的语义不同,不会遇到上述陷阱。useBailout()
只是一个不能作为 Hook 使用的例子,还有一些其他的东西不能用作 Hooks。例如, useProvider
、useCatch()
和 useSuspense()
。
源地址:https://overreacted.io/why-isnt-x-a-hook/
「每日一瞥」是团队内部日常业界动态提炼,发布时效可能略有延后。 文章可随意转载,但请保留此 原文链接。 非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。