StructureBuilder / react-keep-alive

A component that maintains component state and avoids repeated re-rendering.
MIT License
984 stars 106 forks source link

未能从内嵌自定义的 Context 中使用 Consumer 读取数据 #36

Closed CJY0208 closed 5 years ago

CJY0208 commented 5 years ago

Hello

由于 react-keep-alive 原理大致为将 KeepAlive children 提取出来渲染到 <KeepAliveProvider /> 节点下,而非 <KeepAlive />之下

这将导致 KeepAlive 中的组件无法被 React 认为是在其所处的上下文之中

样例大致如下:

https://codesandbox.io/s/basic-currently-rwo9y

import React, { createContext, useState } from "react";
import ReactDOM from "react-dom";
import { Provider as KeepAliveProvider, KeepAlive } from "react-keep-alive";

const { Provider, Consumer } = createContext();

function Test({ contextValue = null }) {
  return (
    <div>
      <p>contextValue: {contextValue}</p>
    </div>
  );
}

function App() {
  const [show, setShow] = useState(true);
  const toggle = () => setShow(show => !show);
  return (
    <KeepAliveProvider>
      <div>
        <Provider value={1}>
          {show && (
            <KeepAlive name="Test">
                <Consumer>
                  {context => <Test contextValue={context} />}
                </Consumer>
            </KeepAlive>
          )}
          <button onClick={toggle}>toggle</button>
        </Provider>
      </div>
    </KeepAliveProvider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

样例中的 <Test /> 无法从 <Consumer /> 中获得 contextValue 属性

从实现原理上来说目前似乎无法避免,是不得已而为之

想问问在这方面有没有考虑其他可能的实现方式呢?

目前会对直觉上的 Context 造成破坏,有不小的危害性,如果目前无法修复的话,个人认为有必要在 README 中给出警示

CJY0208 commented 5 years ago

是否可以尝试增加一个组件 <KeepAliveStation />,类似于 ”驿站“ 的概念

每个 KeepAlive 可选择不同的缓存节点,Provider 作为默认驿站备用,也可以用在驿站被销毁时的 fallback 缓存节点

如上述例子中

...
import { Provider as KeepAliveProvider, KeepAlive, KeepAliveStation } from "react-keep-alive";
...

function App() {
  ...
  return (
    <KeepAliveProvider>
      ...
        <Provider value={1}>
          <KeepAliveStation name="station-1" />
          ...
            <KeepAlive name="Test" station="station-1">
                <Consumer>
                  {context => <Test contextValue={context} />}
                </Consumer>
            </KeepAlive>
          ...
        </Provider>
      ...
    </KeepAliveProvider>
  );
}
...

不过也许无法实现 fallback 功能,驿站销毁的话,上边挂载的缓存节点也许会强迫走 unmount 周期

这个方案也会造成 react-dev-tools 面板混乱程度的增加...

ShenChang618 commented 5 years ago

@CJY0208 抱歉现在才回复,这个问题确实很严重,并且我认为现在的实现方式也有一些问题,React.createPortal 可能并非最佳方案,因此我想看看有什么其他方式实现

CJY0208 commented 5 years ago

我自己也在尝试一个简易的 keep-alive 实现,从 React 渲染流程来看,这个问题很难绕过...目前来说似乎是实现 keep-alive 的唯一途径

在实现过程中我做了一个可行的尝试:

增加 fixContext 函数来接收要修复的上下文,在 KeepAlive 中先使用待修复上下文的 Consumer 获取到可能存在的 Provider 数据,传递到对应的 KeepAliveProvider 中,再重新使用 Provider 重建上下文,就可以修复这个问题

// 书写时
...
<KeepAliveProvider>
  ...
  <Provider value={1}>
    ...
    <KeepAlive name="Test">
      <Consumer>
        {context => <Test contextValue={context} />}
      </Consumer>
    </KeepAlive>
    ...
  </Provider>
  ...
</KeepAliveProvider>
...

// 实际渲染时
...
<KeepAliveProvider>
  ...
  <Provider value={1}>
    ...
    <Consumer>
      {context => (
        <KeepAlive name="Test" contextValue={context}>
          {/* render to Keeper named "Test" */}
        </KeepAlive>
      )}
    </Consumer>
    ...
  </Provider>
  ...
  <Keeper name="Test">
    {contextValue => (
      <Provider value={contextValue}>
        <Consumer>
          {context => <Test contextValue={context} />}
        </Consumer>
      </Provider>
    )}
  </Keeper>
</KeepAliveProvider>
...

大概是这样子,预先声明可能需要修复的 Context 组,KeepAlive 提前做 HOC 封装,套上一层 Consumer 尝试获取可能存在的上下文,再将 KeepAlive 获取到的上下文传入其对应的 Keeper 中,重建上下文关系

后续可以尝试包装 createContext 函数,让用户使用从 react-keep-alive 导出的 createContext,创建出可自动修复的 context,这样比较贴近无感知的使用体验

但如果需要修复的上下文过多,dev-tools 中的层级结构会比较难看

ShenChang618 commented 5 years ago

@CJY0208 嗯嗯,现在所讨论的这个问题,实际上最简单的方式是可以直接放在 <KeepAliveProvider> 之外,这样实际上并不会出现这个问题。

<Provider value={1}>
  <KeepAliveProvider>
    ...
    <KeepAlive name="Test">
      <Consumer>
        {context => <Test contextValue={context} />}
      </Consumer>
    </KeepAlive>
    ...
  </KeepAliveProvider>
</Provider>

你说的两种方式都很有价值,但是我认为这样会有一些复杂,我也希望暴露出来的 API 能够越少越好,这样易用性会好一些。

因此我想在不改变 API 的情况下,重构下实现。

ShenChang618 commented 5 years ago

@CJY0208 可以到这个 ISSUE #36 下讨论

CJY0208 commented 5 years ago

目前感觉是依赖于 React 层级结构的行为,可能都产生了破坏性,例如下述有两个影响

1、事件冒泡失效 2、KeepAlive 内部依赖于外部数据的 children 更新失效

https://codesandbox.io/s/basic-currently-5gfz9

function App() {
  const [show, setShow] = useState(true);
  const toggle = () => setShow(show => !show);
  return (
    <KeepAliveProvider>
      <div onClick={() => {
        console.log('捕获到冒泡事件')
      }}>
        <KeepAlive name="Test">{show && <div>random</div>}</KeepAlive>
        {show && <div>random</div>}        
        <button onClick={toggle}>toggle</button>
      </div>
    </KeepAliveProvider>
  )
}

猜测 Error Boundaries 也受了影响,不过还没测

ShenChang618 commented 5 years ago

@CJY0208 👍,这个确实是预先没有考虑到得问题

zhangmingdi commented 8 months ago

我自己也在尝试一个简易的 keep-alive 实现,从 React 渲染流程来看,这个问题很难绕过...目前来说似乎是实现 keep-alive 的唯一途径

在实现过程中我做了一个可行的尝试:

增加 fixContext 函数来接收要修复的上下文,在 KeepAlive 中先使用待修复上下文的 Consumer 获取到可能存在的 Provider 数据,传递到对应的 KeepAliveProvider 中,再重新使用 Provider 重建上下文,就可以修复这个问题

// 书写时
...
<KeepAliveProvider>
  ...
  <Provider value={1}>
    ...
    <KeepAlive name="Test">
      <Consumer>
        {context => <Test contextValue={context} />}
      </Consumer>
    </KeepAlive>
    ...
  </Provider>
  ...
</KeepAliveProvider>
...

// 实际渲染时
...
<KeepAliveProvider>
  ...
  <Provider value={1}>
    ...
    <Consumer>
      {context => (
        <KeepAlive name="Test" contextValue={context}>
          {/* render to Keeper named "Test" */}
        </KeepAlive>
      )}
    </Consumer>
    ...
  </Provider>
  ...
  <Keeper name="Test">
    {contextValue => (
      <Provider value={contextValue}>
        <Consumer>
          {context => <Test contextValue={context} />}
        </Consumer>
      </Provider>
    )}
  </Keeper>
</KeepAliveProvider>
...

大概是这样子,预先声明可能需要修复的 Context 组,KeepAlive 提前做 HOC 封装,套上一层 Consumer 尝试获取可能存在的上下文,再将 KeepAlive 获取到的上下文传入其对应的 Keeper 中,重建上下文关系

后续可以尝试包装 createContext 函数,让用户使用从 react-keep-alive 导出的 createContext,创建出可自动修复的 context,这样比较贴近无感知的使用体验

但如果需要修复的上下文过多,dev-tools 中的层级结构会比较难看

想学习一下是如何你是如何使用桥接,稳读了一下源码,不太懂。有两个问题,希望大佬能解决一下困惑

zhangmingdi commented 8 months ago

我自己也在尝试一个简易的 keep-alive 实现,从 React 渲染流程来看,这个问题很难绕过...目前来说似乎是实现 keep-alive 的唯一途径 在实现过程中我做了一个可行的尝试: 增加 fixContext 函数来接收要修复的上下文,在 KeepAlive 中先使用待修复上下文的 Consumer 获取到可能存在的 Provider 数据,传递到对应的 KeepAliveProvider 中,再重新使用 Provider 重建上下文,就可以修复这个问题

// 书写时
...
<KeepAliveProvider>
  ...
  <Provider value={1}>
    ...
    <KeepAlive name="Test">
      <Consumer>
        {context => <Test contextValue={context} />}
      </Consumer>
    </KeepAlive>
    ...
  </Provider>
  ...
</KeepAliveProvider>
...

// 实际渲染时
...
<KeepAliveProvider>
  ...
  <Provider value={1}>
    ...
    <Consumer>
      {context => (
        <KeepAlive name="Test" contextValue={context}>
          {/* render to Keeper named "Test" */}
        </KeepAlive>
      )}
    </Consumer>
    ...
  </Provider>
  ...
  <Keeper name="Test">
    {contextValue => (
      <Provider value={contextValue}>
        <Consumer>
          {context => <Test contextValue={context} />}
        </Consumer>
      </Provider>
    )}
  </Keeper>
</KeepAliveProvider>
...

大概是这样子,预先声明可能需要修复的 Context 组,KeepAlive 提前做 HOC 封装,套上一层 Consumer 尝试获取可能存在的上下文,再将 KeepAlive 获取到的上下文传入其对应的 Keeper 中,重建上下文关系 后续可以尝试包装 createContext 函数,让用户使用从 react-keep-alive 导出的 createContext,创建出可自动修复的 context,这样比较贴近无感知的使用体验 但如果需要修复的上下文过多,dev-tools 中的层级结构会比较难看

想学习一下是如何你是如何使用桥接,稳读了一下源码,不太懂。有两个问题,希望大佬能解决一下困惑 1.如何让Keeper获取不被包裹的Provider,Provider有多个的时候,怎么知道哪个Provider是自己想要的