React: Wie werden verschachtelte Kontexte gelöst?

Erstellt am 18. Jan. 2019  ·  38Kommentare  ·  Quelle: facebook/react

Ich habe viele Kontexte und muss auf diese Weise schreiben, so hässlich! Es behindert meine Arbeit jetzt. Ein solches Design macht es fast unbrauchbar.

<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

Hilfreichster Kommentar

@ 0xorial Sie brauchen dafür eigentlich keine Komponente, schließlich ist <>> nur ein Funktionsaufruf von React.createElement. Sie können es also zu einer Kompositionsfunktion vereinfachen, wie:

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

und benutze es als:

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/>
  );
}

Alle 38 Kommentare

Die kommende Hooks-API bietet eine andere Möglichkeit, Kontexte zu nutzen.

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

Vielen Dank. Aber was ist mit dem Anbieter?

Ich werde ehrlich sein. Wenn Sie auf diese Art der Implementierung stoßen, scheint Ihr Architekturdesign schlecht zu sein und Sie sollten wahrscheinlich nicht den React-Kontext verwenden.

Nein, ich entwerfe einen neuen Vorratsbehälter. Es muss mit dem Kontext arbeiten, um zu reagieren.
https://github.com/rabbitooops/rako

Was hat diese Bibliothek mit 5 Schichten von Anbietern / Verbrauchern zu tun?

Weil es das Injizieren mehrerer Filialen unterstützt, um zu reagieren, anstatt eine Lösung für einzelne Filialen wie Redux.

In diesem Fall liegt die Kontextbehandlung jetzt bei den Benutzern der Bibliothek und weniger bei der Bibliothek. Wie sie die Funktionen nutzen, liegt bei ihnen. Wenn sie alle Anbieter an einem Ort haben möchten (was den Zweck, mehrere Geschäfte zu haben, zunichte macht), haben sie die Wahl. Im Idealfall wird eine Lösung mit mehreren Speichern an verschiedenen Stellen in der Anwendung implementiert, sodass verschachtelte Kontexte wie diese viel seltener sind.

Meine 2 Cent mindestens.

Die Kontext-API ist daher nur für Single-Store-Lösungen wie Redux geeignet.

Überhaupt nicht. Es ist aber auch schwierig, Ihr Problem ohne ein realistischeres Beispiel zu diskutieren. Bitte erstellen Sie eine?

Stellen Sie sich vor, es gibt drei Store-Themen, Benutzer und Zähler.

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

    /* ... */
  }
}

Was wäre Ihre ideale Syntax?

Verwenden Sie context.write und unterstützen Sie das Konsumieren mehrerer Kontexte ohne Verschachtelung.

Das Konsumieren mehrerer Kontexte ohne Verschachtelung wird bereits unterstützt. (Mit Haken.)

Für Context.write ist ein RFC geöffnet. Wir wissen nicht, ob es durchkommt, weil es einige sehr komplizierte Fragen aufwirft. Aber während der RFC geöffnet ist, bin ich mir nicht sicher, was in dieser Ausgabe umsetzbar ist. Haben Sie etwas hinzuzufügen, das über das hinausgeht, was bereits in der RFC-Motivation enthalten ist?

Ich möchte eine Frage stellen. Warum reagiert die Unterstützung nicht auf den Konsum mehrerer Kontexte im Unterricht? Die Hooks-API scheint viele Probleme zu lösen und ist derzeit sehr instabil

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

Ich werde ein StoreProviders implementieren, das mehrere Kontexte automatisch verschachteln kann.

const StoreProviders = constructStoreProviders(...storeContexts)

<StoreProviders>
  <Child />
</StoreProviders>

Danke für deine Hilfe Dan! :) :)

@rabbitooops Welche Probleme mit Hooks hast du genau? Ich benutze Haken in der Produktion und sie funktionieren gut für mein Team.

Und was ist damit? Es ist jetzt sicher, Haken zu verwenden. @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 Wie wäre es mit der Verwendung eines einzelnen Geschäfts und eines Symbols als Schlüssel zur Nachahmung

data Store = Leaf Object | C Store Store

Oder auf unvollkommene Weise in 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 Anders: App.useProvider

@zhujinxuan Es tut mir leid, ich kann dich nicht bekommen.

@zhujinxuan Sie verwenden unausgesprochen , Beispiel:

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

Die Hooks-API scheint viele Probleme zu lösen und ist derzeit sehr instabil

Wir bereiten es auf eine Veröffentlichung innerhalb von ein oder zwei Wochen vor - nicht sicher, warum Sie darauf geschlossen haben. Sie werden bald fertig sein. Wenn Sie jedoch sicher sein möchten, warten Sie bitte bis zu einer stabilen Veröffentlichung.

Und was ist damit?

Das Aufrufen von Hooks in einer Schleife (wie in forEach ) ist im Allgemeinen nicht zulässig. Es ist einfach, auf diese Weise Probleme zu verursachen.

useStoreProviders

Sowohl useProvider als auch useShouldComponentUpdate sind als Hooks problematisch (weshalb React sie nicht hat). Siehe meine Antwort unter https://github.com/facebook/react/issues/14534#issuecomment -455411307.


Insgesamt habe ich Schwierigkeiten, die Absicht dieses Problems zu verstehen.

Das Konsumieren mehrerer Kontexte wird durch useContext Hook gelöst. Wir empfehlen, es nicht irgendwie mit Arrays zu "automatisieren", da dies das Schreiben von Komponenten, die zu viel Kontext abonnieren und zu oft neu rendern, zu einfach macht. Sie sollten ein klares Gefühl dafür haben, welche Kontexte eine Komponente abhört und welche useContext API Ihnen bietet. Wenn Sie müssen, können Sie useMyContexts() Hook schreiben, der explizit bestimmte Kontexte verwendet. Ich empfehle nur nicht, es dynamisch zu machen, wie Sie es getan haben, denn wenn sich die Array-Länge ändert, kann es brechen.

Das Setzen mehrerer Anbieter kann als "Boilerplate" angesehen werden, und wir haben möglicherweise eine Lösung dafür. Aber ich verstehe auch nicht, warum Sie es als großes Problem ansehen. Beispiele in diesem Thread sind nicht realistisch genug, um mir das Problem zu erklären. Ich sehe nichts Schlimmes darin, mehrere JSX-Ebenen irgendwo oben in der Anwendung zu verschachteln. Ziemlich sicher, dass Sie in den meisten Komponenten eine viel tiefere Verschachtelung von div und das tut nicht allzu weh.

Ich werde dies schließen, da ich denke, dass ich bereits auf diese Punkte geantwortet habe und die Diskussion im Kreis verläuft. Wenn etwas fehlt, lass es mich wissen.

OT: @gaearon , gibt es einen Plan, etwas wie useRender hinzuzufügen oder etwas, um mehr Kontrolle über das Rendern zu haben? z.B:

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

Das zweite Argument hat die gleiche Rolle wie useEffect hook.

useMemo ist dein Freund.

Siehe den zweiten Ausschnitt unter https://reactjs.org/docs/hooks-faq.html#how -to-memoize-berechnungen.

Am Ende hatte ich so einen Code:

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;
}

Dann in meiner Top-Level-Bereitstellungskomponente:

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

Ich sehe nichts Schlimmes darin, mehrere JSX-Ebenen irgendwo oben in der Anwendung zu verschachteln.

Ich habe ~ 15 Abhängigkeiten, die ich auf diese Weise injizierbar sein möchte, und 15 Einrückungsstufen sehen für mich nicht besonders hübsch aus :)

@ 0xorial Sie brauchen dafür eigentlich keine Komponente, schließlich ist <>> nur ein Funktionsaufruf von React.createElement. Sie können es also zu einer Kompositionsfunktion vereinfachen, wie:

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

und benutze es als:

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/>
  );
}

Ich habe in der Vergangenheit eine Bibliothek geschrieben, die diesen Fall behandelt: https://github.com/disjukr/join-react-context

react11

Dies ist definitiv etwas, das in Anwendungen ständig passiert. useContext eignet sich hervorragend, um die Kontextdaten in einer Komponente zu verbrauchen, aber nicht so gut, wenn Sie den Kontext in einer App mit mehreren Anbietern bereitstellen müssen.

Hier ist eine Schließungsalternative der @ alesmenzelsocialbakers- Lösung:

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'));

Nachteil ist, dass Sie jede bestimmte Provider-Komponente schreiben müssen.
Beispiel:

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

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

Ich habe eine Zustandsverwaltungsbibliothek erstellt , die sich besser für die Zusammensetzung von Diensten eignet. Hier ist eine Demo zur Vermeidung der Anbieterhölle . Probieren Sie es einfach aus oder lesen Sie die Quelle (100 Codezeilen)!

Es wird ein "Bereichs" -Objekt eingeführt, um den Kontextanbieter zu erfassen, sodass:

  • Services können isoliert oder zusammengestellt werden, je nachdem, ob sie sich im selben Bereich befinden.

    • Dienste können frühere Dienste im gleichen Umfang nutzen, obwohl sie sich in derselben Komponente befinden.

  • Alle von einem Bereich gesammelten Anbieter können auf einmal bereitgestellt werden, um die Hölle der Anbieter zu vermeiden.

Ich habe auch hier eine Pause eingelegt. Dies scheint gut zu funktionieren:

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

Verwendung ist:

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>
  );

Ich habe diesen Helfer auch als npm-Bibliothek react-compose-wrappers

Das Folgende zeigt, wie ich den authentifizierten Benutzer an Komponenten weitergebe, die ihn benötigen.

Ich habe beschlossen, einen Status für meine Anwendung zu erstellen. In meiner State.js-Datei habe ich meinen Ausgangszustand, Kontext, Reduzierer, Provider und Hook eingerichtet.

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;

Dann habe ich in meiner index.js-Datei meine App in den Provider eingewickelt.

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'),
);

Um den Status in einer Komponente zu verbrauchen, kann ich den Hook verwenden. Ich kann auch den Versand verwenden, um den Status zu aktualisieren. Zum Beispiel, wenn ich einen Benutzer erhalten oder festlegen möchte.

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;

Ich denke, dieser Weg gibt mir die Freiheit, den Staat so aufzubauen, wie ich ihn brauche, ohne eine Menge zusätzlicher Kontexte hinzuzufügen, und hilft mir, ein tiefes Nest von Anbietern zu vermeiden.

Die kommende Hooks-API bietet eine andere Möglichkeit, Kontexte zu nutzen.

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

Wie verwende ich das in einer Klassenkomponente?

Die kommende Hooks-API bietet eine andere Möglichkeit, Kontexte zu nutzen.
https://reactjs.org/docs/hooks-reference.html#usecontext

Wie verwende ich das in einer Klassenkomponente?

Werden Hooks nicht verwendet, um verschiedene React-Funktionen zu nutzen, ohne Klassen zu schreiben?
Nun, das heißt, alles, was verschiedene Hooks tun, ist bereits in den Klassen vorhanden. Wenn Sie über eine bequeme Syntax- und Verwendungs-API sprechen, verlagert sich die Reaktion von Klassen zu funktionalen Komponenten. Willkommen bei Funktionen und Hooks.

Ich habe ein Paket erstellt, um das Problem zu lösen, indem ich eine ähnliche API mit vue3 bereitgestellt habe

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

  • Lösen Sie das Problem, dass der Reaktionskontext einen zusätzlichen Ansichtsbaum benötigt.
  • Bewahren Sie auch die Reaktivität dessen, was injiziert wurde
  • Fraktal zwischen Anbietern
  • Verwenden Sie WeakMap, um die Abhängigkeiten zu speichern
  • Verwenden Sie das Objekt-umschlossene Symbol, um eine bessere Unterstützung für Typoskripte, Abhängigkeitsinjektion und Debug-Erfahrung zu erzielen

beachten:

  • Ich empfehle Ihnen nicht, sich stark auf die Reaktivität des Kontexts zu verlassen , da es besser ist, den Zugriff auf die Daten als auf die Daten selbst bereitzustellen. Was @gaearon darüber gesagt hat, nicht zu viele Kontexte zu abonnieren, war richtig. Er erwähnte jedoch nicht, dass es ein falsches Muster ist, das Datenabonnement zu lösen, indem man sich auf die Reaktivität des Kontexts verlässt . Das Richtige ist also, nicht mehrere Kontexte zu verwenden, sondern Ihre Abhängigkeiten in einem Kontext bereitzustellen . Halten Sie Ihre Kontexte in der Zwischenzeit so stabil wie möglich .
  • Wenn wir also eine bessere API wollen, müssen wir uns selbst darum kümmern und daran denken, die API fraktal zu machen. Dafür ist mein Paket da.

Outer.tsx

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>
    </>
  );
};

So mache ich das:

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)
}

Sie finden die Funktion pipe in vielen gängigen Bibliotheken wie rxjs. Außerdem gibt es mehrere Vorschläge auf Sprachebene für diese Pipeline-ähnliche Operation. Es ist nicht nötig, es mit einer anderen Bibliothek zu lösen.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen