• Stars
    star
    1,506
  • Rank 31,140 (Top 0.7 %)
  • Language
    JavaScript
  • Created about 5 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Hack <KeepAlive /> for React

NOTICE

  • DO NOT use <React.StrictMode />
  • (React v18+) DO NOT use ReactDOMClient.createRoot, use ReactDOM.render instead, #225 (comment)

React Activation

size dm

English | 中文说明

HACK Implementation of the <keep-alive /> function in Vue For React

Please also pay attention to official support <Offsreen /> in React 18.x


More stable <KeepAlive /> function with babel pre-compilation

Online Demo


More examples


Compatibility

  • React v16 / v17 / v18

  • Preact v10+

  • Compatible with SSR


Install

yarn add react-activation
# or
npm install react-activation

Usage

1. (Optional, Recommended) Add react-activation/babel plugins in .babelrc

Why is it needed?

The plugin adds a _nk attribute to each JSX element during compilation to help the react-activation runtime generate an unique identifier by render location base on react-node-key.

{
  "plugins": [
    "react-activation/babel"
  ]
}

(0.11.0+) If you don't want to use Babel, it is recommended that each <KeepAlive> declare a globally unique and invariant cacheKey attribute to ensure the stability of the cache, as follows:

<KeepAlive cacheKey="UNIQUE_ID" />

2. Wrap the components that need to keep states with <KeepAlive>

Like the <Counter> component in the example

// App.js

import React, { useState } from 'react'
import KeepAlive from 'react-activation'

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count => count + 1)}>Add</button>
    </div>
  )
}

function App() {
  const [show, setShow] = useState(true)

  return (
    <div>
      <button onClick={() => setShow(show => !show)}>Toggle</button>
      {show && (
        <KeepAlive>
          <Counter />
        </KeepAlive>
      )}
    </div>
  )
}

export default App

3. Place the <AliveScope> outer layer at a location that will not be unmounted, usually at the application entrance

Note: When used with react-router or react-redux, you need to place <AliveScope> inside <Router> or <Provider>

// index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { AliveScope } from 'react-activation'

import App from './App'

ReactDOM.render(
  <AliveScope>
    <App />
  </AliveScope>,
  document.getElementById('root')
)

Lifecycle

ClassComponent works with withActivation decorator

Use componentDidActivate and componentWillUnactivate to correspond to the two states of "activate" and "unactivate" respectively.

FunctionComponent uses the useActivate and useUnactivate hooks respectively

...
import KeepAlive, { useActivate, useUnactivate, withActivation } from 'react-activation'

@withActivation
class TestClass extends Component {
  ...
  componentDidActivate() {
    console.log('TestClass: componentDidActivate')
  }

  componentWillUnactivate() {
    console.log('TestClass: componentWillUnactivate')
  }
  ...
}
...
function TestFunction() {
  useActivate(() => {
    console.log('TestFunction: didActivate')
  })

  useUnactivate(() => {
    console.log('TestFunction: willUnactivate')
  })
  ...
}
...
function App() {
  ...
  return (
    {show && (
      <KeepAlive>
        <TestClass />
        <TestFunction />
      </KeepAlive>
    )}
  )
}
...

Cache Control

Manually control the cache

  1. Add the name attribute to the <KeepAlive> tag that needs to control the cache.

  2. Get control functions using withAliveScope or useAliveController.

    • drop(name): (drop can only be used for nodes in the cache state. If the node is not cached but needs to clear the cache state, please use refresh)

      Unload the <KeepAlive> node in cache state by name. The name can be of type String or RegExp. Note that only the first layer of content that hits <KeepAlive> is unloaded and will not be uninstalled in <KeepAlive>. Would not unload nested <KeepAlive>.

    • dropScope(name): (dropScope can only be used for nodes in the cache state. If the node is not cached but needs to clear the cache state, please use refreshScope)

      Unloads the <KeepAlive> node in cache state by name. The name optional type is String or RegExp, which will unload all content of <KeepAlive>, including all <KeepAlive> nested in <KeepAlive>.

    • refresh(name):

      Refresh the <KeepAlive> node in cache state by name. The name can be of type String or RegExp. Note that only the first layer of content that hits <KeepAlive> is refreshed and will not be uninstalled in <KeepAlive>. Would not refresh nested <KeepAlive>.

    • refreshScope(name):

      Refresh the <KeepAlive> node in cache state by name. The name optional type is String or RegExp, which will refresh all content of <KeepAlive>, including all <KeepAlive> nested in <KeepAlive>.

    • clear():

      will clear all <KeepAlive> in the cache

    • getCachingNodes():

      Get all the nodes in the cache

...
import KeepAlive, { withAliveScope, useAliveController } from 'react-activation'
...
<KeepAlive name="Test">
  ...
    <KeepAlive>
      ...
        <KeepAlive>
          ...
        </KeepAlive>
      ...
    </KeepAlive>
  ...
</KeepAlive>
...
function App() {
  const { drop, dropScope, clear, getCachingNodes } = useAliveController()

  useEffect(() => {
    drop('Test')
    // or
    drop(/Test/)
    // or
    dropScope('Test')

    clear()
  })

  return (
    ...
  )
}
// or
@withAliveScope
class App extends Component {
  render() {
    const { drop, dropScope, clear, getCachingNodes } = this.props

    return (
      ...
    )
  }
}
...

Automatic control cache

Add the when attribute to the <KeepAlive /> tag that needs to control the cache. The value is as follows

When the when type is Boolean

  • true: Cache after uninstallation
  • false: Not cached after uninstallation
<KeepAlive when={false}>

When the when type is Array

The 1th parameter indicates whether it needs to be cached at the time of uninstallation.

The 2th parameter indicates whether to unload all cache contents of <KeepAlive>, including all <KeepAlive> nested in <KeepAlive>.

// For example:
// The following indicates that it is not cached when uninstalling
// And uninstalls all nested `<KeepAlive>`
<KeepAlive when={[false, true]}>
  ...
  <KeepAlive>
    ...
    <KeepAlive>...</KeepAlive>
    ...
  </KeepAlive>
  ...
</KeepAlive>

When the when type is Function (Recommended)

The return value is the above Boolean or Array, which takes effect as described above.

The final calculation time of when is adjusted to componentWillUnmount lifecicle of <KeepAlive>, the problem that most of the when do not achieve the expected effect can be avoided.

<KeepAlive when={() => true}>
<KeepAlive when={() => [false, true]}>

Multiple Cache

Under the same parent node, <KeepAlive> in the same location will use the same cache by default.

For example, with the following parameter routing scenario, the /item route will be rendered differently by id, but only the same cache can be kept.

<Route
  path="/item/:id"
  render={props => (
    <KeepAlive>
      <Item {...props} />
    </KeepAlive>
  )}
/>

Similar scenarios, you can use the id attribute of <KeepAlive> to implement multiple caches according to specific conditions.

<Route
  path="/item/:id"
  render={props => (
    <KeepAlive id={props.match.params.id}>
      <Item {...props} />
    </KeepAlive>
  )}
/>

Save Scroll Position (true by default)

<KeepAlive /> would try to detect scrollable nodes in its children, then, save their scroll position automaticlly before componentWillUnactivate and restore saving position after componentDidActivate

If you don't want <KeepAlive /> to do this thing, set saveScrollPosition prop to false

<KeepAlive saveScrollPosition={false} />

If your components share screen scroll container, document.body or document.documentElement, set saveScrollPosition prop to "screen" can save sharing screen container's scroll position before componentWillUnactivate

<KeepAlive saveScrollPosition="screen" />

Principle

Pass the children attribute of <KeepAlive /> to <AliveScope /> and render it with <Keeper />

After rendering <Keeper />, the content is transferred to <KeepAlive /> through DOM operation.

Since <Keeper /> will not be uninstalled, caching can be implemented.

Simplest Implementation Demo


Breaking Change

  1. <KeepAlive /> needs to pass children to <AliveScope /> , so the rendering of the real content will be slower than the normal situation

    Will have a certain impact on the function of strictly relying on the lifecycle order, such as getting the value of ref in componentDidMount, as follows

    class Test extends Component {
      componentDidMount() {
        console.log(this.outside) // will log <div /> instance
        console.log(this.inside) // will log undefined
      }
    
      render() {
        return (
          <div>
            <div
              ref={ref => {
                this.outside = ref
              }}
            >
              Outside KeepAlive
            </div>
            <KeepAlive>
              <div
                ref={ref => {
                  this.inside = ref
                }}
              >
                Inside KeepAlive
              </div>
            </KeepAlive>
          </div>
        )
      }
    }

    The above error in ClassComponent can be fixed by using the withActivation high-level component

    FunctionComponent currently has no processing method, you can use setTimeout or nextTick to delay ref getting behavior

    @withActivation
    class Test extends Component {
      componentDidMount() {
        console.log(this.outside) // will log <div /> instance
        console.log(this.inside) // will log <div /> instance
      }
    
      render() {
        return (
          <div>
            <div
              ref={ref => {
                this.outside = ref
              }}
            >
              Outside KeepAlive
            </div>
            <KeepAlive>
              <div
                ref={ref => {
                  this.inside = ref
                }}
              >
                Inside KeepAlive
              </div>
            </KeepAlive>
          </div>
        )
      }
    }
  2. Destructive impact on Context

    after [email protected] with [email protected]+, this question has been automatic fixed

    [email protected] with react@17+ you Need to make the following changes to achieve automatic repair

    import { autoFixContext } from 'react-activation'
    
    autoFixContext(
     [require('react/jsx-runtime'), 'jsx', 'jsxs', 'jsxDEV'],
     [require('react/jsx-dev-runtime'), 'jsx', 'jsxs', 'jsxDEV']
    )

    Versions below [email protected] need to be repaired manually, refer to the following

    Problem reference: StructureBuilder/react-keep-alive#36

    <Provider value={1}>
      {show && (
        <KeepAlive>
          <Consumer>
            {(
              context // Since the rendering level is broken, the context cannot be obtained here.
            ) => <Test contextValue={context} />}
          </Consumer>
        </KeepAlive>
      )}
      <button onClick={toggle}>toggle</button>
    </Provider>

    Choose a repair method

    • Create Context using createContext exported from react-activation

    • Fix the affected Context with fixContext exported from react-activation

    ...
    import { createContext } from 'react-activation'
    
    const { Provider, Consumer } = createContext()
    ...
    // or
    ...
    import { createContext } from 'react'
    import { fixContext } from 'react-activation'
    
    const Context = createContext()
    const { Provider, Consumer } = Context
    
    fixContext(Context)
    ...
  3. Affects the functionality that depends on the level of the React component, as follows


More Repositories

1

webpack-multiple-pages

自由代码分割、react/vue共存、支持高清方案、代码自动校验与格式化
JavaScript
65
star
2

js-bridge-adapter

web 端 bridge 适配器,用以对接不同 webview 中任意模式的 jsBridge 交互
JavaScript
27
star
3

formini

1.8kb form core with vanilla js
TypeScript
23
star
4

mono-micro-project

微前端 monorepo 项目样例
TypeScript
22
star
5

re-modulex

3kb gzipped make Redux easier like Vuex in React Application
JavaScript
21
star
6

santi

Isomorphic framework base on create-react-app and jsdom
JavaScript
17
star
7

auto-skeleton-plugin

基于 jsdom 的、自动生成骨架的 Webpack 插件,工作方式参考 Prerender-SPA-Plugin
JavaScript
16
star
8

tiny-scroll-listener

一个监听滚动的小工具
JavaScript
12
star
9

babel-plugin-jsx-css-modules

JSX 中无感知使用 CSS Modules
JavaScript
12
star
10

gulp-foal

Allow you to run task with param (NOT from cmd) when gulp's running.
JavaScript
10
star
11

tools

自用工具库
TypeScript
8
star
12

pro-components

自研自用的、基于 antd 的管理系统场景重型组件
TypeScript
8
star
13

mobile-components

自研自用的、基于 react 的、小体积的移动端组件库
8
star
14

react-router-cache-outlets

cache outlets for react-router v6+
TypeScript
7
star
15

umi4-keep-demo

umi4 keep-alive demo
TypeScript
6
star
16

js-dynamic-function

动态函数,类似于多态函数,但不限于按参数类型条件切换执行体
JavaScript
5
star
17

babel-plugin-tester

用于测试自研的 babel 插件
JavaScript
5
star
18

icons

自用的、基于 icones 的、可按需加载的 react 图标组件库
TypeScript
4
star
19

i18nshell

1.9kb gzipped tiny i18n shell, splitable instances, lazyloadable resources, customizable currency, time or jsx i18n support
JavaScript
4
star
20

blog

4
star
21

react-node-key

Automatic key-marked for React nodes
JavaScript
3
star
22

freeCurry.js

Create one new function with default param which's index you can set. Like R._ of Ramda.js
JavaScript
3
star
23

MConsole

One tool for checking console's output in mobile web page.
JavaScript
2
star
24

cjy0208.github.io

1
star
25

tryGet.js

Get property without breaking Error
JavaScript
1
star
26

jsdom-memory-leak-demo

demo for jsdom memory leak
JavaScript
1
star
27

CJY0208

1
star