写了一个妖 hook 也许解决了 React 状态控制的一个痛点

前言

前些日子我看到一篇掘金文章 React官方不推荐这样使用useImperativeHandle,我偏要用,和官方对着干!。这篇文章主要是介绍了怎么利用 refuseImperativeHandle 将组件内部方法进行暴露,不过我们今天不是讨论这一点,而是要讨论他这个解决方案面向的问题 ,而这个问题是很常见常规的 React 组件通信类的问题,让我长话短说,他写了一个业务 Modal 组件,然后面临一个简单的场景就是,外部按钮打开 Modal,然后用户经过一系列操作之后 Modal 自己关闭。

众所周知,React 组件按控制状态可以分为受控组件跟非受控组件,那么以上的场景就会面临这样一个问题,如果 Modal 按受控组件实现,那么它就不能关闭自己,而如果 Modal 按非受控组件实现,那他就无法被外部按钮打开。

常规解决方式

遇到这样的这样的场景业界里通常有两种方案。

一,受控组件提供回调

开关状态仍由外部保持,但是在组件关闭自身的时候冒出回调让外部关闭。

typescript 复制代码
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} />
        </>
    )
}

开关状态由 Modal 内部管理,但是暴露 show 方法给外部调用

typescript 复制代码
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 打开示例研究。

方案设计阶段

我一开始设想最直接的使用方式应该就像下面这样。

typescript 复制代码
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 一样使用它。

typescript 复制代码
function Parent () {
    const [state, setState] = useShareState(false)
    return ( 
        <>
            <button onClick={() => setState(true)}> by parent </button>
            <Children value={state} />
        </>
    )
}
function Children (props) {
    const { value } = props
    const [state, setState] = useShareValue(value)
    return (
        <button onClick={() => setState(false)} > by myself </button>
    )
}

也可以搭配使用 useControllableValue,让这个组件达到一种天人合一干啥都行的境界,想要受控想要非受控,想要共享都行。

typescript 复制代码
    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 中尝试着操作或者阅读组件之间的交互代码。

源码实现

typescript 复制代码
// 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];
}

最后

这个功能实现也就暂时跟大家告一段落了,其实这还只是一个开端的实现,有些地方还需要做一些兜底处理,方案也没经过大量验证,有意见发现问题的或者其他看法的可以在评论下方跟作者讨论哦~

这里是 Xekin(/zi:kin/),以上这就是本篇文章分享的全部内容了,喜欢的掘友们可以点赞关注点个收藏~

最近摸鱼时间比较多,写了一些奇奇怪怪有用但又不是特别有用的工具,不过还是非常有意思的,之后会一一写文章分享出来,感谢各位支持。

我还是喜欢写没人写过的东西~

相关推荐
大前端爱好者1 小时前
React 19 新特性详解
前端
随云6321 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
无知的小菜鸡1 小时前
路由:ReactRouter
react.js
寻找09之夏2 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
非著名架构师2 小时前
js混淆的方式方法
开发语言·javascript·ecmascript
多多米10053 小时前
初学Vue(2)
前端·javascript·vue.js
敏编程3 小时前
网页前端开发之Javascript入门篇(5/9):函数
开发语言·javascript
柏箱3 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑3 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz8563 小时前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序