在 React Conf 2018 上,React团队提出了Hooks提案。html
若是你想知道什么是 Hooks,及它能解决什么问题,查看咱们的讲座(介绍),理解React Hooks(常见的误解)。react
最初你可能会不喜欢 Hooks:git
它们就像一段音乐,只有通过几回用心聆听才会慢慢爱上:程序员
当你阅读文档时,不要错过关于最重要的部分——创造属于你本身的Hooks!太多的人纠结于反对咱们的观点(class学习成本高等)以致于错过了Hooks更重要的一面,Hooks像 functional mixins
,可让你创造和搭建属于本身的Hook。github
Hooks受启发于一些现有技术,但在 Sebastian 和团队分享他的想法以后,我才知道这些。不幸的是,这些API和如今在用的之间的关联很容易被忽略,经过这篇文章,我但愿能够帮助更多的人理解 Hooks提案中争议较大的点。spring
接下来的部分须要你知道 Hook API 的useState
和如何写自定义Hook。若是你还不懂,能够看看早先的连接。数组
(免责说明:文章的观点仅表明我的想法,与React团队无关。话题大且复杂,其中可能有错误的观点。)浏览器
一开始当你学习时你可能会震惊,Hooks 重渲染时是依赖于固定顺序调用的,这里有说明。安全
这个决定显然是有争议的,这也是为何会有人反对咱们的提案。咱们会在恰当的时机发布这个提案,当咱们以为文档和讲座能够足够好的描绘它时。bash
若是你在关注 Hooks API 的某些点,我建议你阅读下 Sebastian对 1000+ 评论RFC的所有回复,足够透澈但内容很是多,我可能会将评论中的每一段都变成本身的博客文章。(事实上,我已经作过一次!)
我今天要关注一个具体部分。你可能还记得,每一个 Hook 能够在组件里被屡次使用,例如,咱们能够用 useState
声明多个state:
function Form() {
const [name, setName] = useState('Mary'); // State variable 1
const [surname, setSurname] = useState('Poppins'); // State variable 2
const [width, setWidth] = useState(window.innerWidth); // State variable 3
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
function handleNameChange(e) {
setName(e.target.value);
}
function handleSurnameChange(e) {
setSurname(e.target.value);
}
return (
<>
<input value={name} onChange={handleNameChange} />
<input value={surname} onChange={handleSurnameChange} />
<p>Hello, {name} {surname}</p>
<p>Window width: {width}</p>
</>
);
}
复制代码
注意咱们用数组解构语法来命名 useState()
返回的 state 变量,但这些变量不会链接到React组件上。相反,这个例子中,React将 name
视为“第一个state变量”,surname
视为“第二个state变量”,以此类推。它们在从新渲染时用 顺序调用 来保证被正确识别。这篇文章详细的解释了缘由。
表面上看,依赖于顺序调用只是 感受有问题,直觉是一个有用的信号,但它有时会误导咱们 —— 特别是当咱们尚未彻底消化困惑的问题。 这篇文章,我会提到几个常常有人提出修改Hooks的方案,及它们存在的问题。
这篇文章不会详尽无遗,如你所见,咱们已经看过十几种至数百种不一样的替代方案,咱们一直在考虑替换组件API。
诸如此类的博客很棘手,由于即便你涉及了一百种替代方案,也有人强行提出一个来:“哈哈,你没有想到 这个 ”!
在实践中,不一样替代方案提到的问题会有不少重复,我不会列举 全部 建议的API(这须要花费数月时间),而是经过几个具体示例展现最多见的问题,更多的问题就考验读者触类旁通的能力了。🧐
这不是说 Hooks 就是完美的,可是一旦你了解其余解决方案的缺陷,你可能会发现 Hooks 的设计是有道理的。
出乎意料的是,大多数替代方案彻底没有提到 custom hooks。多是由于咱们在“motivation”文档中没有足够强调 custom hooks,不过在弄懂 Hooks 基本原理以前,这是很难作到的。就像鸡和蛋问题,但很大程度上 custom hooks 是提案的重点。
例如:有个替代方案是限制一个组件调用屡次 useState()
,你能够把 state 放在一个对象里,这样还能够兼容class不是更好吗?
function Form() {
const [state, setState] = useState({
name: 'Mary',
surname: 'Poppins',
width: window.innerWidth,
});
// ...
}
复制代码
要清楚,Hooks 是容许这种风格写的,你没必要将state拆分红一堆state变量(请参阅参见问题解答中的建议)。
但支持屡次调用 useState()
的关键在于,你能够从组件中提取出部分有状态逻辑(state + effect)到 custom hooks 中,同时能够单独使用本地 state 和 effects:
function Form() {
// 在组件内直接定义一些state变量
const [name, setName] = useState('Mary');
const [surname, setSurname] = useState('Poppins');
// 咱们将部分state和effects移至custom hook
const width = useWindowWidth();
// ...
}
function useWindowWidth() {
// 在custom hook内定义一些state变量
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
// ...
});
return width;
}
复制代码
若是你只容许每一个组件调用一次 useState()
,你将失去用 custom hook 引入 state 能力,这就是 custom hooks 的关键。
一个常见的建议是让组件内 useState()
接收一个惟一标识key参数(string等)区分 state 变量。
和这主意有些出入,但看起来大体像这样:
// ⚠️ This is NOT the React Hooks API
function Form() {
// 咱们传几种state key给useState()
const [name, setName] = useState('name');
const [surname, setSurname] = useState('surname');
const [width, setWidth] = useState('width');
// ...
复制代码
这试图摆脱依赖顺序调用(显示key),但引入了另一个问题 —— 命名冲突。
固然除了错误以外,你可能没法在同一个组件调用两次 useState('name')
,这种偶然发生的能够归结于其余任意bug,可是,当你使用一个 custom hook 时,你总会遇到想添加或移除state变量和effects的状况。
这个提议中,每当你在 custom hook 里添加一个新的 state 变量时,就有可能破坏使用它的任何组件(直接或者间接),由于 可能已经有同名的变量 位于组件内。
这是一个没有针对变化而优化的API,当前代码可能看起来老是“优雅的”,但应对需求变化时十分脆弱,咱们应该从错误中吸收教训。
实际中 Hooks 提案经过依赖顺序调用来解决这个问题:即便两个 Hooks 都用 name
state变量,它们也会彼此隔离,每次调用 useState()
都会得到独立的 “内存单元”。
咱们还有其余一些方法能够解决这个缺陷,但这些方法也有自身的缺陷。让咱们加深探索这个问题。
给 useState
“加key”的另外一种衍生提案是使用像Symbol这样的东西,这样就不冲突了对吧?
// ⚠️ This is NOT the React Hooks API
const nameKey = Symbol();
const surnameKey = Symbol();
const widthKey = Symbol();
function Form() {
// 咱们传几种state key给useState()
const [name, setName] = useState(nameKey);
const [surname, setSurname] = useState(surnameKey);
const [width, setWidth] = useState(widthKey);
// ...
复制代码
这个提案看上去对提取出来的 useWindowWidth
Hook 有效:
// ⚠️ This is NOT the React Hooks API
function Form() {
// ...
const width = useWindowWidth();
// ...
}
/*********************
* useWindowWidth.js *
********************/
const widthKey = Symbol();
function useWindowWidth() {
const [width, setWidth] = useState(widthKey);
// ...
return width;
}
复制代码
但若是尝试提取出来的 input handling,它会失败:
// ⚠️ This is NOT the React Hooks API
function Form() {
// ...
const name = useFormInput();
const surname = useFormInput();
// ...
return (
<>
<input {...name} />
<input {...surname} />
{/* ... */}
</>
)
}
/*******************
* useFormInput.js *
******************/
const valueKey = Symbol();
function useFormInput() {
const [value, setValue] = useState(valueKey);
return {
value,
onChange(e) {
setValue(e.target.value);
},
};
}
复制代码
(我认可 useFormInput()
Hook 不是特别好用,但你能够想象下它处理诸如验证和 dirty state 标志之类,如Formik。)
你能发现这个bug吗?
咱们调用 useFormInput()
两次,但 useFormInput()
老是用同一个 key 调用 useState()
,就像这样:
const [name, setName] = useState(valueKey);
const [surname, setSurname] = useState(valueKey);
复制代码
咱们再次发生了冲突。
实际中 Hooks 提案没有这种问题,由于每次 调用 useState()
会得到单独的state。依赖于固定顺序调用使咱们免于担忧命名冲突。
从技术上来讲这个和上一个缺陷相同,但它的臭名值得说说,甚至维基百科都有介绍。(有些时候还被称为“致命的死亡钻石” —— cool!)
咱们本身的 mixin 系统就受到了伤害。
好比useWindowWidth()
和 useNetworkStatus()
这两个 custom hooks 可能要用像 useSubscription()
这样的 custom hook,以下:
function StatusMessage() {
const width = useWindowWidth();
const isOnline = useNetworkStatus();
return (
<>
<p>Window width is {width}</p>
<p>You are {isOnline ? 'online' : 'offline'}</p>
</>
);
}
function useSubscription(subscribe, unsubscribe, getValue) {
const [state, setState] = useState(getValue());
useEffect(() => {
const handleChange = () => setState(getValue());
subscribe(handleChange);
return () => unsubscribe(handleChange);
});
return state;
}
function useWindowWidth() {
const width = useSubscription(
handler => window.addEventListener('resize', handler),
handler => window.removeEventListener('resize', handler),
() => window.innerWidth
);
return width;
}
function useNetworkStatus() {
const isOnline = useSubscription(
handler => {
window.addEventListener('online', handler);
window.addEventListener('offline', handler);
},
handler => {
window.removeEventListener('online', handler);
window.removeEventListener('offline', handler);
},
() => navigator.onLine
);
return isOnline;
}
复制代码
这是一个真实可运行的示例。 custom hook 做者准备或中止使用另外一个 custom hook 应该是要安全的,而没必要担忧它是否已在链中某处“被用过了”。
(做为反例,遗留的 React createClass()
的 mixins 不容许你这样作,有时你会有两个 mixins,它们都是你想要的,但因为扩展了同一个 “base” mixin,所以互不兼容。)
这是咱们的 “钻石”:💎
/ useWindowWidth() \ / useState() 🔴 Clash
Status useSubscription()
\ useNetworkStatus() / \ useEffect() 🔴 Clash
复制代码
依赖于固定的顺序调用很天然的解决了它:
/ useState() ✅ #1. State
/ useWindowWidth() -> useSubscription()
/ \ useEffect() ✅ #2. Effect
Status
\ / useState() ✅ #3. State
\ useNetworkStatus() -> useSubscription()
\ useEffect() ✅ #4. Effect
复制代码
函数调用不会有“钻石”问题,由于它们会造成树状结构。🎄
或许咱们能够经过引入某种命名空间来挽救给 state 加“key”提议,有几种不一样的方法能够作到这一点。
一种方法是使用闭包隔离state的key,这须要你在 “实例化” custom hooks时给每一个 hook 裹上一层 function:
/*******************
* useFormInput.js *
******************/
function createUseFormInput() {
// 每次实例化都惟一
const valueKey = Symbol();
return function useFormInput() {
const [value, setValue] = useState(valueKey);
return {
value,
onChange(e) {
setValue(e.target.value);
},
};
}
}
复制代码
这种做法很是繁琐,Hooks 的设计目标之一就是避免使用高阶组件和render props的深层嵌套函数。在这里,咱们不得不在使用 任何 custom hook 时进行“实例化” —— 并且在组件主体中只能单次使用生产的函数,这比直接调用 Hooks 麻烦好多。
另外,你不得不操做两次才能使组件用上 custom hook。一次在最顶层(或在编写 custom hook 时的函数里头),还有一次是最终的调用。这意味着即便一个很小的改动,你也得在顶层声明和render函数间来回跳转:
// ⚠️ This is NOT the React Hooks API
const useNameFormInput = createUseFormInput();
const useSurnameFormInput = createUseFormInput();
function Form() {
// ...
const name = useNameFormInput();
const surname = useNameFormInput();
// ...
}
复制代码
你还须要很是精确的命名,老是须要考虑“两层”命名 —— 像 createUseFormInput
这样的工厂函数和 useNameFormInput
、useSurnameFormInput
这样的实例 Hooks。
若是你同时调用两次相同的 custom hook “实例”,你会发生state冲突。事实上,上面的代码就是这种错误 —— 发现了吗? 它应该为:
const name = useNameFormInput();
const surname = useSurnameFormInput(); // Not useNameFormInput!
复制代码
这些问题并不是不可克服,但我认为它们会比遵照 Hooks规则 的阻力大些。
重要的是,它们打破了复制粘贴的小算盘。在没有封装外层的状况下这种 custom hook 仍然可使用,但它们只能够被调用一次(这在使用时会产生问题)。不幸的是,当一个API看起来能够正常运行,一旦你意识到在链的某个地方出现了冲突时,就不得不把全部定义好的东西包起来了。
还有另一种使用密钥state来避免冲突的方法,若是你知道,可能会真的很生气,由于我不看好它,抱歉。
这个主意就是每次写 custom hook 时 组合 一个密钥,就像这样:
// ⚠️ This is NOT the React Hooks API
function Form() {
// ...
const name = useFormInput('name');
const surname = useFormInput('surname');
// ...
return (
<>
<input {...name} />
<input {...surname} />
{/* ... */}
</>
)
}
function useFormInput(formInputKey) {
const [value, setValue] = useState('useFormInput(' + formInputKey + ').value');
return {
value,
onChange(e) {
setValue(e.target.value);
},
};
}
复制代码
和其余替代提议比,我最不喜欢这个,我以为它没有什么价值。
一个 Hook 通过屡次调用或者与其余 Hook 冲突以后,代码可能 意外产出 非惟一或合成无效密钥进行传递。更糟糕的是,若是它是在某些条件下发生的(咱们会试图 “修复” 它对吧?),可能在一段时间后才发生冲突。
咱们想提醒你们,记住全部经过密钥来标记的 custom hooks 都很脆弱,它们不只增长了运行时的工做量(别忘了它们要转成 密钥 ),并且会渐渐增大 bundle 大小。但若是说咱们非要提醒一个问题,是哪一个问题呢?
若是非要在条件判断里声明 state 和 effects,这种方法多是有做用的,但按过去经验来讲,我发现它使人困惑。事实上,我不记得有人会在条件判断里定义this.state
或者componentMount
的。
这段代码到底意味着什么?
// ⚠️ This is NOT the React Hooks API
function Counter(props) {
if (props.isActive) {
const [count, setCount] = useState('count');
return (
<p onClick={() => setCount(count + 1)}>
{count}
</p>;
);
}
return null;
}
复制代码
当 props.isActive
为 false
时 count
是否被保留?或者因为 useState('count')
没有被调用而重置 count
?
若是条件为保留 state,effect 又会发生什么?
// ⚠️ This is NOT the React Hooks API
function Counter(props) {
if (props.isActive) {
const [count, setCount] = useState('count');
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
return (
<p onClick={() => setCount(count + 1)}>
{count}
</p>;
);
}
return null;
}
复制代码
无疑它不会在 props.isActive
第一次是 true
以前 运行,但一旦变成 true
,它会中止运行吗?当 props.isActive
转变为 false
时 interval 会重置吗?若是是这样,effect 与 state(咱们说不重置时) 的行为不一样使人困惑。若是 effect 继续运行,那么 effect 外层的 if
再也不控制 effect,这也使人感到困惑,咱们不是说咱们想要基于条件控制的 effects 吗?
若是在渲染期间咱们没有“使用” state 但 它却被重置,若是有多个 if
分支包含 useState('count')
但只有其中一个会在给定时间里运行,会发生什么?这是有效的代码吗?若是咱们的核心思想是 “以密钥分布”,那为何要 “丢弃” 它?开发人员是否但愿在这以后从组件中提早 return
以重置全部state呢? 其实若是咱们真的须要重置state,咱们能够经过提取组件使其明确:
function Counter(props) {
if (props.isActive) {
// Clearly has its own state
return <TickingCounter />; } return null; } 复制代码
不管如何这可能成为是解决这些困惑问题的“最佳实践”,因此无论你选择哪一种方式去解释,我以为条件里 声明 state 和 effect的语义怎样都很怪异,你可能会不知不觉的感觉到。
若是还要提醒的是 —— 正确地组合密钥的需求会变成“负担”,它并无给咱们带来任何想要的。可是,放弃这个需求(并回到最初的提案)确实给咱们带来了一些东西,它使组件代码可以安全地复制粘贴到一个 custom hook 中,且不须要命名空间,减少bundle大小及轻微的效率提高(不须要Map查找)。
慢慢理解。
Hooks 有个最好的功能就是能够在它们之间传值。
如下是一个选择信息收件人的模拟示例,它显示了当前选择的好友是否在线:
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
const handleStatusChange = (status) => setIsOnline(status.isOnline);
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
复制代码
当改变收件人时,useFriendStatus
Hook 就会退订上一个好友的状态,订阅接下来的这个。
这是可行的,由于咱们能够将 useState()
Hook 返回的值传给 useFriendStatus()
Hook:
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
复制代码
Hooks之间传值很是有用。例如:React Spring能够建立一个尾随动画,其中多个值彼此“跟随”:
const [{ pos1 }, set] = useSpring({ pos1: [0, 0], config: fast });
const [{ pos2 }] = useSpring({ pos2: pos1, config: slow });
const [{ pos3 }] = useSpring({ pos3: pos2, config: slow });
复制代码
(这是 demo。)
在Hook初始化时添加默认参数或者将Hook写在装饰器表单中的提议,很难实现这种状况的逻辑。
若是不在函数体内调用 Hooks,就不能够轻松地在它们之间传值了。你能够改变这些值结构,让它们不须要在多层组件之间传递,也能够用 useMemo
来存储计算结果。但你也没法在 effects 中引用这些值,由于它们没法在闭包中被获取到。有些方法能够经过某些约定来解决这些问题,但它们须要你在内心“核算”输入和输出,这违背了 React 直接了当的风格。
在 Hooks 之间传值是咱们提案的核心,Render props 模式在没有 Hooks 时是你最早能想到的,但像 Component Component 这样的库,是没法适用于你遇到的全部场景的,它因为“错误的层次结构”存在大量的语法干扰。Hooks 用扁平化层次结构来实现传值 —— 且函数调用是最简单的传值方式。
有许多提议处于这种范畴里。他们尽量的想让React摆脱对 Hooks 的依赖感,大多数方法是这么作的:让 this
拥有内置 Hooks,使它们变成额外的参数在React中无处不在,等等等。
我以为 Sebastian的回答 比个人描述,更能说服这种方式,我建议你去了解下“注入模型”。
我只想说这和程序员倾向于用 try
/catch
捕获方法中的错误代码是同样的道理,一样对比 AMD由咱们本身传入 require
的“显示”声明,咱们更喜欢 import
(或者 CommonJS require
) 的 ES模块。
// 有谁想念 AMD?
define(['require', 'dependency1', 'dependency2'], function (require) {
var dependency1 = require('dependency1'),
var dependency2 = require('dependency2');
return function () {};
});
复制代码
是的,AMD 可能更“诚实” 的陈述了在浏览器环境中模块不是同步加载的,但当你知道了这个后,写 define
三明治 就变成作无用功了。
try
/catch
、require
和 React Context API都是咱们更喜欢“环境”式体验,多于直接声明使用的真实例子(即便一般咱们更喜欢直爽风格),我以为 Hooks 也属于这种。
这相似于当咱们声明组件时,就像从 React
抓个 Component
过来。若是咱们用工厂的方式导出每一个组件,可能咱们的代码会更解耦:
function createModal(React) {
return class Modal extends React.Component {
// ...
};
}
复制代码
但在实际中,这最后会变得画蛇添足而使人厌烦。当咱们真的想以某种方式抓React时,咱们应该在模块系统层面上实现。
这一样适用于 Hooks。尽管如此,正如 Sebastian的回答 中提到的,在 技术上 能够作到从 react
中“直接”导入不一样实现的 Hooks。(我之前的文章有提到过。)
另外一种强行复杂化想法是把Hooks monadic(单子化) 或者添加像 React.createHook()
这样的class理念。除了runtime以外,其余任何添加嵌套的方案都会失去普通函数的优势:便于调试。
在调试过程当中,普通函数中不会夹杂任何类库代码,且能够清晰的知道组件内部值的流向,间接性很难作到这点。像启发于高阶组件(“装饰器” Hooks)或者 render props(adopt
提案 或 generators的yield
等)相似的方案,都存在这样的问题。间接性也使静态类型变得复杂。
如我以前提到的,这篇文章不会详尽无遗,在其余提案中有许多有趣的问题,其中有一些更加晦涩(例如于并发和高级编译相关),这多是在将来另外一篇文章的主题。
Hooks并不是完美无瑕,但这是咱们能够找到解决这些问题的最佳权衡。还有一些咱们仍然须要修复的东西,这些问题在 Hooks 中比在 class 中更加别扭,这也会写在别的文章里头。
不管我是否覆盖掉你喜欢的替换方案,我但愿这篇文章有助于阐述咱们的思考过程及咱们在选择API时考虑的标准。如你所见,不少(例如确保复制粘贴、移动代码、按但愿的方式进行增删依赖包)不得不针对变化而优化。我但愿React开发者们会看好咱们所作的这些决定。
翻译原文Why Do React Hooks Rely on Call Order?(2018-12-13)