React: 如何反应解决嵌套上下文?

创建于 2019-01-18  ·  38评论  ·  资料来源: facebook/react

我有很多上下文,我必须用这种方式写,太丑了! 现在阻碍了我的工作。 这种设计使其几乎无法使用。

<context1.Provider value={value1}>
  <context2.Provider value={value2}>
    <context3.Provider value={value3}>
      <context4.Provider value={value4}>
        <context5.Provider value={value5}>

        </context5.Provider>
      </context4.Provider>
    </context3.Provider>
  </context2.Provider>
</context1.Provider>
<context1.Consumer>
  {value1 => <context2.Consumer>
    {value2 => <context3.Consumer>
      {value3 => <context4.Consumer>
        {value4 => <context5.Consumer>
          {value5 => (
            null
          )}
        </context5.Consumer>}
      </context4.Consumer>}
    </context3.Consumer>}
  </context2.Consumer>}
</context1.Consumer>
Question

最有用的评论

@ 0xorial实际上并不需要一个组件,毕竟<>>只是一个函数调用React.createElement。 因此,您可以将其简化为诸如以下的撰写函数:

const compose = (contexts, children) =>
  contexts.reduce((acc, [Context, value]) => {
    return <Context.Provider value={value}>{acc}</Context.Provider>;
  }, children);

并将其用作:

import Context1 from './context1';
import Context2 from './context2';
import Context3 from './context3';
...
import Context15 from './context15';

const MyComponent = (props) => {
  // const value1..15 = ... get the values from somewhere ;

  return compose(
    [
      [Context1, value1],
      [Context2, value2],
      [Context3, value3],
      ...
      [Context15, value15],
    ],
    <SomeSubComponent/>
  );
}

所有38条评论

即将推出的Hooks API提供了一种使用上下文的不同方式。

https://reactjs.org/docs/hooks-reference.html#usecontext

谢谢。 但是提供者呢?

老实说如果您遇到这种实现,那么您的架构设计似乎很差,您可能不应该使用React上下文。

不,我正在设计一个新的存储容器。 它需要根据上下文进行反应。
https://github.com/rabbitooops/rako

该库与供应商/消费者的5层深度有什么关系?

因为它支持注入多个存储以做出反应,而不是像Redux这样的单个存储解决方案。

在那种情况下,上下文处理现在是在库的用户上,而在库的用户上则更少。 他们如何利用这些功能取决于他们,如果他们希望所有提供商都集中在一个地方(这违反了开设多个商店的目的),那么这就是他们的选择。 理想情况下,将在应用程序中的不同位置实现多商店解决方案,因此像这样的嵌套上下文很少见。

至少是我的2美分。

因此,上下文API仅对像redux这样的单一存储解决方案友好。

一点也不。 但是,如果没有更实际的例子,也很难讨论您的问题。 请创建一个?

想象一下,有三个商店主题,用户和柜台。

function theme(getState) {
  return {
    color: 'white',
    setColor(color) {
      this.setState({color})
    }
  }
}

function user(getState) {
  return {
    name: '',
    setName(name) {
      this.setState({name})
    }
  }
}

function counter(getState) {
  return {
    value: 0,
    increment() {
      const {value} = getState()
      this.setState({value: value + 1})
    }
  }
}

const [themeStore, userStore, counterStore] = createStores(theme, user, counter)
const [themeContext, userContext, counterContext] = createContexts(themeStore, userStore, counterStore)


class App extends React.Component {
  render() {
    return (
      <themeContext.StoreProvider>
        <userContext.StoreProvider>
          <counterContext.StoreProvider>

            <Child />

          </counterContext.StoreProvider>
        </userContext.StoreProvider>
      </themeContext.StoreProvider>
    )
  }
}

class Child extends React.Component {
  static contextType = [themeContext, userContext]
  render() {
    const [theme, user] = this.context

    /* ... */
  }
}

您理想的语法是什么?

使用context.write,支持在不嵌套的情况下使用多个上下文。

已经支持在不嵌套的情况下使用多个上下文。 (带挂钩)

Context.write为此打开了一个RFC。 我们不知道它能否通过,因为它提出了一些非常复杂的问题。 但是,尽管RFC是开放的,但我不确定在此问题中什么是可行的。 除了RFC动机中已有的内容之外,您还有其他要添加的内容吗?

我想问一个问题。 为什么不对在类中消耗多个上下文的支持做出反应? 似乎hooks API有很多问题需要解决,并且目前非常不稳定。

static contextType = [themeContext, userContext]
const [theme, user] = this.context

我将实现一个StoreProviders ,它可以自动嵌套多个上下文。

const StoreProviders = constructStoreProviders(...storeContexts)

<StoreProviders>
  <Child />
</StoreProviders>

感谢Dan的帮助! :)

@rabbitooops钩子到底有哪些问题? 我在生产中使用钩子,它们对我的团队非常有效。

那呢? 立即使用钩子是安全的。 @gaearon

// A library
function useStoreProviders(Component, ...contexts) {
  contexts.forEach(context => Component.useProvider(context, someValue))
}

// User code
function App(props) {
  const [theme] = useState('white')

  // Safe to use `component hook`.
  App.useProvider(themeContext, theme)
  App.useShouldComponentUpdate(() => {})

  // Meanwhile, library can also use `component hook`.
  useStoreProviders(App, storeContext1, storeContext2, storeContext3)

  // Normal hook can't use `component hook`.
  customHook()

  /* ... */
}

<App />

decorateBeforeRender(App)

@rabbitooops如何使用单个商店和Symbol作为键来模仿多层商店?

data Store = Leaf Object | C Store Store

或以不完美的方式在javascript中:

const LEFT = Symbol('LEFT')
const RIGHT = Symbol('RIGHT')
function createLeafStore = return new Store({});
function createStore(leftChild :: Store, rightChild :: Store) {
  return new Store({[LEFT]: leftChild, [Right]: rightChild})
}

@TrySound不同: App.useProvider

@zhujinxuan对不起,我无法

@zhujinxuan您可以使用Unstated ,例如:

<Subscribe to={[AppContainer, CounterContainer, ...]}>
  {(app, counter, ...) => (
    <Child />
  )}
</Subscribe>

似乎hooks API有很多问题需要解决,并且目前非常不稳定

我们正在为一两周内的发布做准备-不知道您为什么这么推断。 他们将很快准备就绪,尽管如果您希望安全起见,请等到稳定发行。

那呢?

通常不允许在循环中调用Hook(就像在forEach所做的那样)。 这样很容易引起问题。

useStoreProviders

useProvideruseShouldComponentUpdate都像Hooks一样有问题(这就是为什么React没有它们)。 在https://github.com/facebook/react/issues/14534#issuecomment -455411307中查看我的回复。


总体而言,我正在努力了解此问题的意图。

消耗多个上下文由useContext Hook解决。 我们建议您以某种方式使用数组“自动化”它,因为这使得编写订阅过多上下文并过于频繁地重新渲染的组件太容易了。 您应该清楚地了解组件侦听的上下文,这是useContext API给您的。 如果必须,您可以编写显式使用特定上下文的useMyContexts() Hook。 我只是不建议像您一样使它动态化,因为如果数组长度改变,它可能会损坏。

放置多个提供程序可以被视为“样板”,我们最终可能会为此找到解决方案。 但我也不明白您为什么认为这是一个大问题。 该线程中的示例不够实际,无法向我解释该问题。 在应用程序顶部某个地方嵌套几层JSX,我认为没有任何不好的地方。 可以肯定的是,您在大多数组件中都有更深的div嵌套,这不会对您造成太大的伤害。

由于我认为我已经回答了这些问题,所以我将结束讨论,并且讨论会绕圈进行。 如果有什么遗漏,请告诉我。

OT: @gaearon ,是否有计划添加useRender东西或具有更多渲染控制权的东西? 例如:

useRender(() => <div />, [...props])

第二个arg具有useEffect挂钩的作用。

useMemo是您的朋友。

请参阅https://reactjs.org/docs/hooks-faq.html#how -to-memoize-calculations中的第二个片段。

我最终得到了这样的代码:

function provider<T>(theProvider: React.Provider<T>, value: T) {
   return {
      provider: theProvider,
      value
   };
}

function MultiProvider(props: {providers: Array<{provider: any; value: any}>; children: React.ReactElement}) {
   let previous = props.children;
   for (let i = props.providers.length - 1; i >= 0; i--) {
      previous = React.createElement(props.providers[i].provider, {value: props.providers[i].value}, previous);
   }
   return previous;
}

然后在我的顶级提供组件中:

public render() {
      return (
         <MultiProvider
            providers={[
               provider(Context1.Provider, this.context1),
               provider(Context2.Provider, this.context2),
               provider(Context3.Provider, this.context3),
               provider(Context4.Provider, this.context4),
               provider(Context5.Provider, this.context5),
            ]}
         ><AppComponents />
      </MultiProvider>
}

@gaearon

在应用程序顶部某个地方嵌套几层JSX,我认为没有任何不好的地方。

我有〜15个依赖关系,希望以这种方式进行注入,而缩进15个级别对我来说并不好看:)

@ 0xorial实际上并不需要一个组件,毕竟<>>只是一个函数调用React.createElement。 因此,您可以将其简化为诸如以下的撰写函数:

const compose = (contexts, children) =>
  contexts.reduce((acc, [Context, value]) => {
    return <Context.Provider value={value}>{acc}</Context.Provider>;
  }, children);

并将其用作:

import Context1 from './context1';
import Context2 from './context2';
import Context3 from './context3';
...
import Context15 from './context15';

const MyComponent = (props) => {
  // const value1..15 = ... get the values from somewhere ;

  return compose(
    [
      [Context1, value1],
      [Context2, value2],
      [Context3, value3],
      ...
      [Context15, value15],
    ],
    <SomeSubComponent/>
  );
}

我过去写过一个处理这种情况的库: https :

react11

这绝对是应用程序中经常发生的事情。 useContext非常适合_消耗组件中的上下文数据,但是当您需要在具有多个提供程序的应用程序中提供_provide_上下文时,它并不是那么好。

这是@alesmenzelsocialbakers解决方案的封闭替代方案:

const composeProviders = (...Providers) => (Child) => (props) => (
  Providers.reduce((acc, Provider) => (
    <Provider>
      {acc}
    </Provider>
  ), <Child {...props} />)
)

const WrappedApp = composeProviders(
  ProgressProvider,
  IntentsProvider,
  EntitiesProvider,
  MessagesProvider
)(App)

ReactDOM.render(<WrappedApp />, document.getElementById('root'));

缺点是您必须编写每个特定的提供程序组件。
例:

export const ProgressProvider = ({ children }) => {
  const [progress, setProgress] = useState(0)

  return (
    <ProgressContext.Provider value={{ progress, setProgress }}>
      {children}
    </ProgressContext.Provider>
  )
}

我创建了一个状态管理库,该这是避免提供商地狱的演示。 随意尝试或阅读其源代码(100行代码)!

它引入了一个“作用域”对象来收集上下文提供者,从而:

  • 可以隔离服务或组合服务,具体取决于它们是否在同一范围内。

    • 服务可以使用相同范围内的以前的服务,尽管它们在同一组件中。

  • 可以一次提供一个作用域收集的所有提供程序,避免了提供程序的麻烦。

我也对此有所了解。 这似乎工作正常:

const composeWrappers = (
  wrappers: React.FunctionComponent[]
): React.FunctionComponent => {
  return wrappers.reduce((Acc, Current): React.FunctionComponent => {
    return props => <Current><Acc {...props} /></Current>
  });
}

用法是:

const SuperProvider = composeWrappers([
    props => <IntlProvider locale={locale} messages={messages} children={props.children} />,
    props => <ApolloProvider client={client}>{props.children}</ApolloProvider>,
    props => <FooContext.Provider value={foo}>{props.children}</FooContext.Provider>,
    props => <BarContext.Provider value={bar}>{props.children}</BarContext.Provider>,
    props => <BazContext.Provider value={baz}>{props.children}</BazContext.Provider>,
  ]);
  return (
    <SuperProvider>
      <MainComponent />
    </SuperProvider>
  );

我还将此帮助程序发布为npm库react-compose-wrappers

下面显示了如何将经过身份验证的用户传递给需要它的组件。

我决定为我的应用程序创建一个状态。 在State.js文件中,设置我的初始状态,上下文,reduceer,provider和hook。

import React, { createContext, useContext, useReducer } from 'react';

const INITIAL_STATE = {}

const Context = createContext();

const reducer = (state, action) => 
  action 
    ? ({ ...state, [action.type]: action[action.type] }) 
    : state;

export const Provider = ({ children }) => (
  <Context.Provider value={ useReducer(reducer, INITIAL_STATE) }>
    { children }
  </Context.Provider>
);

const State = () => useContext(Context);

export default State;

然后在我的index.js文件中,将我的应用程序包装在提供程序中。

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from './State';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <Provider>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root'),
);

要消耗组件中的状态,我可以使用钩子。 我还可以使用分派来更新状态。 例如,如果我想获取或设置用户。

import React, {useEffect} from 'react';
import State from './State'

const ExampleComponent = () => {
  const [{ user }, dispatch] = State(); 

  useEffect(() => {
    const getUser = async () => {
      const data = await fetch('http://example.com/user.json');  // However you get your data
      dispatch({ type: 'user', user: data });
    }
    getUser();
  }, [dispatch]);

  // Don't render anything until user is retrieved
  // The user is undefined since I passed an empty object as my initial state
  if(user === undefined) return null; 

  return(
    <p>{user.name}</p>
  );
}

export default ExampleComponent;

我认为这种方式使我可以自由地建立自己需要的状态,而无需添加大量额外的上下文,并且可以帮助我避免提供程序的过多嵌套。

即将推出的Hooks API提供了一种使用上下文的不同方式。

https://reactjs.org/docs/hooks-reference.html#usecontext

如何在类组件中使用它?

即将推出的Hooks API提供了一种使用上下文的不同方式。
https://reactjs.org/docs/hooks-reference.html#usecontext

如何在类组件中使用它?

不用编写类就可以使用钩子来利用React的各种功能吗?
好吧,也就是说,各种钩子所做的一切都已经存在于类中。 如果您在谈论便捷的语法和用法api,那么react将从类转移到功能组件,因此欢迎使用函数和钩子)

我创建了一个程序包以通过为vue3提供类似的API来解决此问题

https://github.com/TotooriaHyperion/react-multi-provide

  • 解决反应上下文需要额外的视图树的问题。
  • 也保留注射的反应性
  • 提供者之间的分形
  • 使用WeakMap存储依赖项
  • 使用对象包装的符号来获得更好的打字稿支持,依赖项注入和调试体验

注意:

  • 我不建议您过分依赖上下文的反应性,因为最好提供对数据的访问而不是数据本身。 @gaearon所说的关于不订阅太多上下文的说法是正确的。 但是他没有提到依靠上下文的反应性来解决数据订阅是错误的模式。 因此,正确的做法不是使用多个上下文,而是在一个上下文中提供依赖项。 同时,请保持上下文尽可能稳定
  • 因此,如果我们想要一个更好的API,我们需要自己处理它,并牢记使API分形。 那就是我的包裹的目的。

外部文件

import React, { useMemo } from "react";
import { Providers, useCreateContexts, useProvide } from "../..";
import { createService, ServiceA } from "./service";

export const Outer: React.FC = ({ children }) => {
  const contexts = useCreateContexts();
  const service = useMemo(createService, []);
  useProvide(contexts, ServiceA.id, service);
  return <Providers contexts={contexts}>{children}</Providers>;
};

Inner2.tsx

import React from "react";
import { useContexts, useReplaySubject } from "../..";
import { ServiceA } from "./service";

export const Inner2: React.FC = () => {
  const [
    {
      state$,
      actions: { inc, dec },
    },
  ] = useContexts([ServiceA.id]);
  const count = useReplaySubject(state$);
  return (
    <>
      <p>{count}</p>
      <div>
        <button onClick={inc}>Increment</button>
        <button onClick={dec}>Decrement</button>
      </div>
    </>
  );
};

这是我的方法:

interface Composable {
    (node: React.ReactNode): React.ReactElement
}

const composable1: Composable = (node)=>{
      return <someContext.Provider>{node}</someContext.Provider>
}

function Comp({children}:{children?:React.ReactNode}){
       return pipe(
             composabl1, composabl2, composable3
       )(children)
}

您可以在许多流行的库(例如rxjs)中找到pipe函数,对于这种类似管道的操作也有一些语言级别的建议。 无需使用另一个库来“解决”它。

此页面是否有帮助?
0 / 5 - 0 等级