keep alive in react is offscreen

2,618 阅读3分钟

前言

在react中组件只有挂载和卸载两种状态,而在vue中还存在第三种状态失活。这种状态在日常业务开发中是很有用的,比如tabs切换时,我们想保持住切换至后台的tab的组件状态,做法一般来说有两种,第一种即保持组件挂载,而后通过css的方式控制DOM的隐藏,这种方式的缺点是当react应用更新时,隐藏组件仍会参与IDFF;第二种是获取当前tab的状态,然后控制组件jsx的返回,这种方式会将tab的显隐状态耦合进组件。下面我将介绍我是如何利用offscreen component解决该问题的。

前置知识

提起Suspense想必大家都不陌生,它是在react16.6中引入的新组件。它可以配合着lazy来做组件的懒加载,在RSC中也可以用于数据加载。当组件或者数据没有准备好的时候,它将展示fallback,下面是一个例子。

import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./lazy-compoent'));

function App() {
    return (
        <Suspense fallback={<span>loading</span>}>
            <LazyComponent />
        </Suspense>
    )
}

在LazyComponent加载完毕之前,它将展示loading fallback。 我们思考几个问题

  1. react是如何切换fallback和component的呢
  2. 我们能否控制Suspense的状态切换呢
  3. 切换至后台后组件的状态会保持吗

带着以上三个问题,我们请求我们本期内容的主角@ivliu/react-offscreen

安装

以pnpm为例

pnpm add @ivliu/offscreen

基本使用

codesandbox.io/s/lucid-mar… 代码如下

import { useState } from 'react';
import { Offscreen } from '@ivliu/react-offscreen';

function Child1() {
    const [count, setCount] = useState(0);
    
    return (
        <button type="button" onClick={() => setCount(count + 1)}>
            {count}
        </button>
    )
}

function Child2() {
    const [count, setCount] = useState(0);
    
    return (
        <button type="button" onClick={() => setCount(count + 1)}>
            {count}
        </button>
    )
}

function App() {
    const [bool, setBool] = useState(false);

    return (
        <>
            <Offscreen mode={bool === true ? 'visible' : 'hidden'}>
                <Child1 />
            </Offscreen>
            <Offscreen mode={bool === false ? 'visible' : 'hidden'}>
                <Child2 />
            </Offscreen>
        </>
    )
}

例子很简单,当bool为true时展示Child1,为false时展示Child2。 它使用很简单,我们可以把它看做content-visibility的react版本,mode值为visible时即组件激活,为hidden时即组件失活。

实现原理

回到第一节我们思考的三个问题,我来回答一下

  1. react是通过子组件是否throw promise来控制fallback和component的切换
  2. 我们可以控制Suspense的状态切换
  3. 切换至后台的组件仍然可以保持其状态

我们看一下它的简单实现

import { useRef, Suspense } from 'react';

function Repeater(props) => {
  // props
  const { mode, children } = props;
  // refs
  const resolveRef = useRef();
  // destroy promise
  if (resolveRef.current) {
    resolveRef.current();
    resolveRef.current = void 0;
  }
  if (mode === 'hidden') {
    throw new Promise((resolve) => (resolveRef.current = resolve));
  }
  return <>{children}</>;
};


function Offscreen(props) {
    // props
    const { mode, children } = props;
    
    return (
        <Suspense>
            <Repeater mode={mode}>{children}</Repeater>
        </Suspense>
    )
}

这其实就是它的完整实现了,之所以称之简单实现是因为它的实现太简单了。 我简单解释下,Offscreen组件接受mode和children属性,它返回了Suspense,fallback不做设置,将mode和children属性传递给Repeater组件;Repeater组件接受到mode和children属性后,判断mode是否为hidden,如果为hidden则抛出一个promise,否则正常返回jsx。当Repeater组件throw promise的时候,Suspense会捕捉到该promise,然后展示fallback,但是因为我们没有设置,因此展示为null,当正常返回jsx时则正常渲染,从而实现对Suspense状态的控制。

总结

以上就是对@ivliu/react-offscreen的全部介绍了,它的使用和实现都很简单,大家如果遇到需要的场景可以拿来使用。 如果大家感兴趣的话我可以再写一篇与市面上其他类似组件的对比。

github + npm

github.com/IVLIU/react…

www.npmjs.com/package/@iv…

希望大家可以不吝小星星,更希望可以踊跃pr