本文主要讲解关于写了一个妖 hook 也许解决了 React 状态控制的一个痛点相关内容,让我们来一起学习下吧!
前言
前些日子我看到一篇掘金文章 React官方不推荐这样使用useImperativeHandle,我偏要用,和官方对着干!。这篇文章主要是介绍了怎么利用 ref
和 useImperativeHandle
将组件内部方法进行暴露,不过我们今天不是讨论这一点,而是要讨论他这个解决方案面向的问题,而这个问题是很常见常规的 React 组件通信类的问题。
长话短说就是,他写了一个业务 Modal 组件,然后面临一个简单的场景——外部按钮打开 Modal,然后用户经过一系列操作之后 Modal 自己关闭。 众所周知,React 组件按控制状态可以分为受控组件跟非受控组件,那么以上的场景就会面临这样一个问题,如果 Modal 按受控组件实现,那么它就不能关闭自己,而如果 Modal 按非受控组件实现,那他就无法被外部按钮打开。
常规解决方式
遇到这样的这样的场景业界里通常有以下两种方案。
一,受控组件提供回调给父组件
开关状态仍由外部保持,但是在组件关闭自身的时候冒出回调让外部关闭。
function Modal() { ... }
function App () {
const [show, setShow] = useState(false)
const onModalClose = () => setShow(false)
const openModal = () => setShow(false)
return (
<>
<button click={openModal}> open </button>
<Modal visible={show} onClose={onModalClose} />
</>
)
}
二,非受控组件用 useImperativeHandle 将组件方法暴露
开关状态由 Modal 组件内部管理,但是暴露 show 方法给外部父组件使用。
const Modal = forwardRef((props, ref) => {
const [visible, setVisible] = useState(false)
const show = () => setVisible(true)
const close = () => setVisible(close)
useImperativeHandle(ref, () => ({ show, close }))
return (
<InnerModal visible={visible}>
<button onClick={close}>close by myself</button>
</InnerModal>
);
}, []);
function App () {
const modalRef = useRef(null)
return (
<>
<button click={modalRef.current?.show}> open </button>
<Modal ref={modalRef} />
</>
)
}
上面两种方式你会喜欢选择哪一种解决方式呢?如果是我的话,我也会跟前言当中的作者一样,即使官方推荐用第一种,相比起来我也更愿意采用第二种方案,因为第二种对外部的代码侵入最少,不用每次用个 Modal 就要写两个平平无奇的方法备着。
不过这仍然是有些 别扭 的,第一种方案很绕,当你想封装一个逻辑自洽的 Modal 时却发现仍有部分功能必须要交到外部执行,这很奇怪。第二种方案则是用 ref
有一个风险就是,当这个组件还未渲染出来时,ref
里的功能就会失效,这会使你有时候甚至能碰上像 vue
什么场景使用 vif
或者 vshow
一样的问题。
我发现一种有点妖孽的解决方式
就在昨天我刷到光哥写了一篇讲怎么实现类似 ahooks 里的 useControllableValue
这个 API 的公众号推文, 但这个方案只是让受控组件兼容非受控形式,却让我又想起了这个场景这个问题。
之前我写过一个 最简易 React 状态管理的 hook,这类打破 React 传统上下文的 hook 我都愿称之为 妖 hook,它让我隐隐感觉到能够想到一些思路去尝试,不过即便无法实现,也期望能找到无法实现的原因是什么,于是我马上操起 VSCode 打开示例研究。
方案设计阶段
我一开始设想最直接的使用方式应该就像下面这样。
function Parent () {
const [state, setState] = useState(false)
return (
<>
<button onClick={() => setState(true)}> by parent </button>
<Children value={state} />
</>
)
}
function Children (props) {
const { value } = props
const [state, setState] = useState(value)
return (
<button onClick={() => setState(false)} > by myself </button>
)
}
就是要实现既能在外部控制某个组件内部状态,又能组件自己控制。所以要做到这点需要做到什么?
我看着看着就 突发奇想,能不能把父组件里 useState
返回的 setState
跟组件里的 setState
进行合并,这样父组件就能够在调用 setState
的时候自动也调用了组件内部的 setState
,反之亦然。
这样子组件只要找到某个时机或许就可以在加载时进行合并方法,这还不够,我希望的是父组件和子组件都能共享使用和改变这个状态,所以我需要让父组件也能同时得到这个值以及能检测到值的更新,不然就跟非受控组件一个样子了,照这个思路实现下去,居然就真的诞生了这两个 hook 。
组合使用 useShareState & useShareValue
这两个 hook 各司其职其中一个是组件实现的时候可以将 props.value
传给 useShareValue
,它内部就是做了合并多个 setState
的处理。另一个则是外部声明使用的,你可以像使用 useState
一样使用它,有一些需要注意的是,你需要从 state.value
里才能拿到存进去的状态。
function Parent () {
const [state, setState] = useShareState(false)
return (
<>
<button onClick={() => setState(true)}> by parent </button>
<Children value={state} />
<span>{ state.value ? 'on' : 'off' }</span>
</>
)
}
function Children (props) {
const { value } = props
const [state, setState] = useShareValue(value)
return (
<button onClick={() => setState(false)} > by myself </button>
)
}
也可以搭配使用 useControllableValue
,让这个组件达到一种天人合一干啥都行的境界,想要受控想要非受控,想要共享都行。
function Children (props) {
const [value, setValue] = useControllableValue(props)
const [state, setState] = useShareValue(value)
return (
<button onClick={() => setState(false)} > by myself </button>
)
}
显而易见的是,它的这种写法比起前面两种常规的解决方案写起来合理很多,但是就是中间有掺杂了一点点黑盒的诡异。只靠封装 hook,真的能达到这样的结果吗?我自己也觉得 book41
结果
结果就是非常的 amazing 啊,为此我创建了一个 demo 可以让你体验到这种奇妙的感觉。
你可以在 demo 中尝试着操作或者阅读组件之间的交互代码。
原理 & 源码实现
它的原理是将简单 state
值存为 { _share: true, value: value, signalRecord: {} }
形式,在 useShareValue
里判断了如果 value._share
为 true
,则将 update
方法写入 signalRecord
里,并在组件销毁时一并销毁,同时在返回的 setState
方法里调用了 signalRecord
里的所有 update
方法,使所有“共享”该 state
的组件同时更新。
以下是它的简易源码——
// use-share.ts
import { useRef } from "react";
import { useUpdate, useUnmount } from "ahooks";
type Signal = ReturnType<typeof useUpdate>;
interface ShareState<T = any> {
_share: true;
value: T;
signalRecord: Record<string, Signal>;
}
function isShareState(state: any): state is ShareState {
return state._share === true;
}
function callSignals(state: ShareState) {
if (state.signalRecord === null) return;
Object.values(state.signalRecord).forEach((signal) => signal());
}
function createSeed() {
return String(Math.random());
}
function disposeShareValue(value: ShareState) {
value.signalRecord = Object.create(null);
}
export function useShareState<T>(defaultValue: T) {
const update = useUpdate();
const seed = useRef(createSeed());
const value = useRef<ShareState<T>>({
_share: true,
value: defaultValue,
signalRecord: Object.create(null),
});
Reflect.set(value.current.signalRecord, seed.current, update);
const setState = function (state: T) {
value.current.value = state;
callSignals(value.current);
};
useUnmount(() => {
disposeShareValue(value.current);
});
return [value.current, setState];
}
export function useShareValue<T>(value: T | ShareState<T>) {
const seed = useRef(createSeed());
const update = useUpdate();
const isShare = isShareState(value);
useUnmount(() => {
if (isShare) {
const shareState = value;
Reflect.deleteProperty(shareState.signalRecord, seed.current);
}
});
if (isShare) {
const shareState = value;
Reflect.set(shareState.signalRecord, seed.current, update);
function setState(state: T) {
shareState.value = state;
callSignals(shareState);
}
return [shareState.value, setState, true];
}
return [value, (...args: any[]) => {}, false];
}
最后
这个功能实现也就暂时跟大家告一段落了,其实这还只是一个开端的实现,有些地方还需要做一些兜底处理(例如支持 useShareState
里传入 getter
形式的 state
),方案暂时没经过大量验证,有意见发现问题的或者其他看法的同学希望可以在评论下方跟作者讨论哦~
这里是 Xekin(/zi:kin/),以上这就是本篇文章分享的全部内容了,喜欢的掘友们可以点赞关注点个收藏~
最近摸鱼时间比较多,写了一些奇奇怪怪有用但又不是特别有用的工具,不过还是非常有意思的,之后会一一写文章分享出来,感谢各位支持。
我还是喜欢写没人写过的东西~
以上就是关于写了一个妖 hook 也许解决了 React 状态控制的一个痛点相关的全部内容,希望对你有帮助。欢迎持续关注程序员导航网,学习愉快哦!