React: Comment réagiront-ils pour résoudre les contextes imbriqués?

Créé le 18 janv. 2019  ·  38Commentaires  ·  Source: facebook/react

J'ai beaucoup de contextes et je dois écrire de cette façon, tellement moche! Cela entrave mon travail maintenant. Une telle conception le rend presque inutilisable.

<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

Commentaire le plus utile

@ 0xorial vous n'avez pas vraiment besoin d'un composant pour cela, après tout <>> est juste un appel de fonction React.createElement. Vous pouvez donc le simplifier en une fonction de composition comme:

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

et utilisez-le comme:

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

Tous les 38 commentaires

La prochaine API Hooks propose une manière différente de consommer les contextes.

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

Je vous remercie. Mais qu'en est-il du fournisseur?

Je serai honnête. Si vous rencontrez ce type d'implémentation, la conception de votre architecture semble médiocre et vous ne devriez probablement pas utiliser le contexte React.

Non, je conçois un nouveau conteneur de magasin. Il doit travailler avec le contexte pour réagir.
https://github.com/rabbitooops/rako

Qu'est-ce que cette bibliothèque a à voir avec 5 couches de fournisseurs / consommateurs?

Parce qu'il prend en charge l'injection de plusieurs magasins pour réagir au lieu d'une solution de magasin unique comme Redux.

Dans ce cas, la gestion du contexte est désormais sur les utilisateurs de la bibliothèque, et moins sur la bibliothèque. La façon dont ils utilisent les fonctionnalités leur appartient, et s'ils veulent tous les fournisseurs au même endroit (ce qui va à l'encontre de l'objectif d'avoir plusieurs magasins), c'est leur choix. Idéalement, une solution à plusieurs magasins serait implémentée à différentes divisions de l'application, de sorte que les contextes imbriqués comme celui-ci sont beaucoup plus rares.

Mes 2 cents au moins.

Ainsi, l'API de contexte n'est conviviale que pour une solution de magasin unique telle que redux.

Pas du tout. Mais il est également difficile de discuter de votre problème sans un exemple plus réaliste. Veuillez en créer un?

Imaginez qu'il y ait trois thèmes de magasins, utilisateur et compteur.

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

    /* ... */
  }
}

Quelle serait votre syntaxe idéale?

Utilisez context.write, prenez en charge la consommation de plusieurs contextes sans imbrication.

La consommation de plusieurs contextes sans imbrication est déjà prise en charge. (Avec des crochets.)

Context.write a une RFC ouverte pour cela. Nous ne savons pas si cela passera parce que cela soulève des questions très complexes. Mais tant que la RFC est ouverte, je ne suis pas sûr de ce qui peut être actionné dans ce problème. Avez-vous quelque chose à ajouter au-delà de ce qui est déjà dans la motivation RFC?

Je veux poser une question. Pourquoi React ne prend-il pas en charge la consommation de plusieurs contextes en classe? L'API Semble hooks a beaucoup de problèmes à résoudre et est très instable pour le moment.

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

Je vais implémenter un StoreProviders qui peut imbriquer automatiquement plusieurs contextes.

const StoreProviders = constructStoreProviders(...storeContexts)

<StoreProviders>
  <Child />
</StoreProviders>

Merci pour votre aide Dan! :)

@rabbitooops Quels problèmes exactement avec les hooks avez-vous? J'utilise des crochets en production et ils fonctionnent bien pour mon équipe.

Et qu'en est-il de ça? Il est sûr d'utiliser le crochet maintenant. @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 Que diriez-vous d'utiliser un seul magasin et un symbole comme clés pour imiter plusieurs couches de magasin?

data Store = Leaf Object | C Store Store

Ou de manière imparfaite en 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 Différent: App.useProvider

@zhujinxuan Je suis désolé, je ne peux pas vous avoir.

@zhujinxuan Vous pouvez utiliser Unstated , exemple:

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

L'API Semble hooks a beaucoup de problèmes à résoudre et est très instable pour le moment

Nous le préparons pour une sortie dans une semaine ou deux - je ne sais pas pourquoi vous avez déduit cela. Ils seront bientôt prêts, mais si vous voulez être en sécurité, veuillez attendre une version stable.

Et qu'en est-il de ça?

L'appel de Hooks dans une boucle (comme vous le faites dans forEach ) n'est généralement pas autorisé. Il est facile de causer des problèmes de cette façon.

useStoreProviders

Les deux useProvider et useShouldComponentUpdate sont problématiques en tant que Hooks (c'est pourquoi React ne les a pas). Voir ma réponse sur https://github.com/facebook/react/issues/14534#issuecomment -455411307.


Dans l'ensemble, j'ai du mal à comprendre l'intention de ce problème.

La consommation de plusieurs contextes est résolue par useContext Hook. Nous ne recommandons useContext API. Si vous le devez, vous pouvez écrire useMyContexts() Hook qui utilise explicitement des contextes spécifiques. Je ne recommande tout simplement pas de le rendre dynamique comme vous l'avez fait, car si la longueur du tableau change, il peut se casser.

Mettre plusieurs fournisseurs peut être considéré comme "passe-partout" et nous pourrions éventuellement avoir une solution pour cela. Mais je ne comprends pas non plus pourquoi vous voyez cela comme un gros problème. Les exemples de ce fil ne sont pas suffisamment réalistes pour m'expliquer le problème. Je ne vois rien de mal à imbriquer plusieurs couches de JSX quelque part en haut de l'application. À peu près sûr, vous avez une imbrication beaucoup plus profonde de div dans la plupart des composants et cela ne fait pas trop mal.

Je vais clore cela car je pense avoir déjà répondu à ces points et la discussion tourne en rond. S'il manque quelque chose, faites-le moi savoir.

OT: @gaearon , il y a un plan pour ajouter quelque chose comme useRender ou quelque chose pour avoir plus de contrôle sur le rendu? par exemple:

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

Le second argument a le même rôle que useEffect hook.

useMemo est votre ami.

Voir le deuxième extrait de code dans https://reactjs.org/docs/hooks-faq.html#how -to-memoize-calculs.

J'ai fini avec un code comme ça:

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

Ensuite, dans mon composant de fourniture de haut niveau:

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

Je ne vois rien de mal à imbriquer plusieurs couches de JSX quelque part en haut de l'application.

J'ai ~ 15 dépendances que je veux être injectable de cette manière, et avoir 15 niveaux d'indentation ne me semble pas joli :)

@ 0xorial vous n'avez pas vraiment besoin d'un composant pour cela, après tout <>> est juste un appel de fonction React.createElement. Vous pouvez donc le simplifier en une fonction de composition comme:

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

et utilisez-le comme:

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

J'ai écrit une bibliothèque dans le passé qui gère ce cas: https://github.com/disjukr/join-react-context

react11

c'est certainement quelque chose qui se produit tout le temps dans les applications. useContext est idéal pour _consommer_ les données contextuelles dans un composant, mais ce n'est pas si génial lorsque vous devez _fournir_ le contexte dans une application avec plusieurs fournisseurs.

Voici une alternative de fermeture de la solution @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'));

L'inconvénient est que vous devez écrire chaque composant fournisseur spécifique.
Exemple:

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

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

J'ai créé une bibliothèque de gestion d'état qui est meilleure pour la composition des services. Voici une démonstration pour éviter l'enfer des fournisseurs . N'hésitez pas à l'essayer ou à lire sa source (100 lignes de code)!

Il introduit un objet "scope" pour collecter le fournisseur de contexte, de sorte que:

  • Les services peuvent être isolés ou composés, selon qu'ils sont dans le même périmètre.

    • Les services peuvent consommer d'anciens services dans le même périmètre, bien qu'ils soient dans le même composant.

  • Tous les fournisseurs collectés par un scope peuvent être fournis chez onece, évitant l'enfer des fournisseurs.

J'ai aussi pris une fissure à cela. Cela semble fonctionner correctement:

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

L'utilisation est:

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

J'ai également publié cet assistant en tant que bibliothèque npm react-compose-wrappers

Ce qui suit montre comment je transmets l'utilisateur authentifié aux composants qui en ont besoin.

J'ai décidé de créer un état pour mon application. Dans mon fichier State.js, j'ai configuré mon état initial, mon contexte, mon réducteur, mon fournisseur et mon 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;

Ensuite, dans mon fichier index.js, j'ai enveloppé mon application dans le fournisseur.

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

Pour consommer l'état dans un composant, je peux utiliser le hook. Je peux également utiliser dispatch pour mettre à jour l'état. Par exemple, si je veux obtenir ou définir un utilisateur.

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;

Je pense que cette façon me donne la liberté de construire l'état dont j'en ai besoin sans ajouter une tonne de contextes supplémentaires et m'aide à éviter un nid profond de fournisseurs.

La prochaine API Hooks propose une manière différente de consommer les contextes.

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

Comment utiliser ceci dans un composant de classe?

La prochaine API Hooks propose une manière différente de consommer les contextes.
https://reactjs.org/docs/hooks-reference.html#usecontext

Comment utiliser ceci dans un composant de classe?

Les hooks ne sont-ils pas utilisés pour profiter de diverses fonctionnalités de React sans écrire de classes?
Eh bien, tout ce que font les différents hooks existe déjà dans les classes. Si vous parlez de syntaxe pratique et d'utilisation de l'API, alors la réaction passe des classes aux composants fonctionnels, alors bienvenue dans les fonctions et les hooks)

J'ai créé un package pour résoudre le problème en fournissant une API similaire avec vue3

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

  • résoudre le problème que le contexte de réaction nécessite un arbre de vue supplémentaire.
  • préserve également la réactivité de ce qui a été injecté
  • fractale entre prestataires
  • utiliser WeakMap pour stocker les dépendances
  • utiliser le symbole encapsulé par objet pour obtenir une meilleure prise en charge du typographie, l'injection de dépendances et l'expérience de débogage

remarquer:

  • Je ne vous recommande pas de vous fier fortement à la réactivité du contexte , car il vaut mieux fournir l'accès aux données plutôt que les données elles-mêmes. Ce que @gaearon a dit à propos de ne pas souscrire à trop de contextes était juste. Mais il n'a pas mentionné que c'était un mauvais modèle pour résoudre l'abonnement aux données en s'appuyant sur la réactivité du contexte . Donc, la bonne chose à faire est de ne pas utiliser plusieurs contextes mais de fournir vos dépendances dans un seul contexte . Et en attendant, gardez vos contextes aussi stables que possible .
  • Et donc, si nous voulons une meilleure API, nous devons la gérer nous-mêmes et garder à l'esprit que l'API est fractale. C'est à cela que sert mon colis.

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

voici comment je fais:

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

Vous pouvez trouver la fonction pipe dans de nombreuses bibliothèques populaires telles que rxjs, il existe également plusieurs propositions au niveau du langage pour cette opération de type pipeline. Il n'est pas nécessaire de le «résoudre» en utilisant une autre bibliothèque.

Cette page vous a été utile?
0 / 5 - 0 notes