本文介绍一种 react 当中在一堆 jsx 中嵌入 hook 的方式 inlineHook。
场景 or 为什么
一、列表渲染
列表渲染中,你或许需要为每个元素准备其状态和副作用,这时候你就需要用一个新的组件,如果需要用到父组件的状态,还需要透传一堆 props 给它。也就是说,你为了使用一个组件,需要包一个新的组件出来。
ts
function Parent(){
// states
// effects
return <>
{list.map(item =>
<ChildComponentWrap
key={item.key}
// ...state
// ...callback
// 这里其实可能很多
/>
)}
</>
}
// 一个黑盒组件
function ChildComponent(props){
return <>...</>
}
// 为了维护它的状态而包装的组件
function ChildComponentWrap(props){
// states
// effects
return <ChildComponent
// ...state
// ...callback
/>
}
作为一个很擅长堆屎山的人,尝试过下面这样的代码,但是由于 react hook 的规则,元素数量发生变化的时候,必然报错。
tsx
function Parent(){
// states
// effects
return <>
{list.map(item =>{
useState()
useEffect(()=>{}, [xxx]);
return <ChildComponentWrap
key={item.key}
// ...state
// ...callback
// 这里其实可能很多
/>
}
)}
</>
}
所以需要某种方法,让它支持内联 hook,就有了 inlineHook 的这样一个工具,原理很简单,它创造了一个组件,并把你的代码块运行在里面,所以里面可以用 hook,当第一参数为 string 的时候,作为组件的 key。
tsx
function Parent(){
// states
// effects
return <>
{list.map(item =>
inlineHook(item.key, () => {
useState()
useEffect(()=>{}, [xxx]);
return <ChildComponent
// ...state
// ...callback
/>
})
)}
</>
}
二、条件副作用
错误代码
tsx
function AComponent(){
// 注意这里,condition false true 转换的时候会报错
if(condition){
useEffect(()=>{}, [])
}
return <>...</>
}
使用 inlineHook,基于条件挂载组件的方式就可以实现条件 hook。
tsx
function AComponent(){
return <>
{condition && inlineHook(()=>{
useEffect(()=>{}, []);
})}
...
</>
}
三、表单联动
想象这样一个场景,你有一个条件表单。
- A 会限制 B 的选项
- A 选 'a' 时,B 可选 '1'、'2'、'3';
- A 选 'b' 时,B 可选 '3'、'4'、'5';
- A 选 'b' 时,B 可选 '5'、'6'、'7';
- 当 A 切换的时候,你需要清除已选中的 B选项(注意上面的 B可选项是有重叠的,有些值可以保留)
tsx
return <Form>
<Form.Select
field='A'
optionList={['a','b','c']}
/>
{* 可能离得很远 *}
<Form.Select
field='B'
multiple // 多选
optionList={[...]} // 想办法映射一下选项
/>
</Form>
思考一下,怎么弄。
可以在 A 的 onChange 里面处理一下
tsx
<Form.Select
field='A'
optionList={['a','b','c']}
onChange={(v) => { // 可以提出去,但是逻辑也很跳跃
const BOptionList = getBOptionList(v);
const newValues = ...
const values = formApi.getValue('B')
if(!isEqual(values,newValues)){
formApi.setValue('B', newBValue)
}
}}
/>
{* 可能离得很远,不一定会关注到这两个表单项的关系 *}
<Form.Select
field='B'
multiple // 多选
optionList={[...]} // 想办法映射一下选项
/>
也可以拆组件,对吗?如我之前所说,你需要在组件间跳跃,上下文 props 需要传递。
tsx
function FieldB(props){
const AValue = useFieldValue("A");
const formApi = useFormApi();
const BOptionList = getBOptionList(AValue);
useEffect(() => {
const values = formApi.getValue("B");
// 检查一下 B 的值在不在 optionList 里面,不在就清除掉
const newValues = ... // 想办法处理出来
if(!isEqual(values, newValues)){
form.setValue("B", newValues);
}
}, [AValue]);
return <Form.Select
field='B'
optionList={BOptionList}
/>
}
使用 inlineHook 之后,大概就是这样。这并不是说这是什么好办法,这是一个偏好问题,但至少,不需要一个新的组件(当你需要频繁修改的时候,过多的组件跳转会影响编码效率),组件的使用与联动关系可以放得近。
tsx
return <Form>
<Form.Select
field='A'
optionList={['a','b','c']}
/>
{* 可能离得很远 *}
{inlineHook(()=>{
const AValue = useFieldValue("A");
const formApi = useFormApi();
useEffect(() => {
const BOptionList = getBOptionList(v);
const values = formApi.getValue("B");
// 检查一下 B 的值在不在 optionList 里面,不在就清除掉
const newValues = ... // 想办法处理出来
if(!isEqual(values, newValues)){
form.setValue("B", newValues);
}
}, [AValue]);
return <Form.Select
field='B'
optionList={[...]} // 想办法映射一下选项
/>
}}
</Form>
用法
tsx
return <>
{* 直接嵌入 *}
{inlineHook(()=>{
useState();
useEffect(()=>{}, []);
return <>可以返回elem渲染</>
})}
{* 条件hook *}
{
condition && inlineHook(()=>{
useEffect(()=>{},[])
return <>可以返回elem渲染</>
})
}
{* 列表 *}
{
list.map(item=>
inlineHook(item.key, ()=>{
useState();
useEffect(()=>{}, []);
return <>可以返回elem渲染</>;
}))
}
</>
好处
- 减少一次性 Wrapper 组件
- 在编码早期,频繁修改代码的时候,可以尽快验证,而不需要创建一个新的组件
坏处
- 你需要绕过这个文件的 react-hook eslint 检查,破坏了规范
- 代码会越写越长,人会越来越懒(用爽了都不想重构了)
源码
tsx
import React, { createElement, isValidElement } from 'react';
function Hook({ fn }: { fn: () => any }) {
const res = fn();
return isValidElement(res) ? res : null;
}
const NullComponent = () => null;
/**
* 挂载 hook,当你不想为它写一个组件或者不知挂哪个组件上的时候
* @param fn 传入一个 hook 函数
* @returns
*/
export function inlineHook(fn: () => any): React.ReactElement;
export function inlineHook(key: string, fn: () => any): React.ReactElement;
export function inlineHook(...args: any[]) {
const [a, b] = args;
if (typeof a === 'function') {
return createElement(Hook, {
fn: a,
});
} else if (typeof a === 'string') {
return createElement(Hook, {
key: a,
fn: b,
});
} else {
return createElement(NullComponent, {});
}
}