Dostęp do kontekstu reakcji poza funkcją renderowania

93

Opracowuję nową aplikację, używając nowego interfejsu API React Context zamiast Redux, a wcześniej, Reduxgdy potrzebowałem na przykład listy użytkowników, po prostu wzywam componentDidMountswoją akcję, ale teraz, dzięki React Context, moje działania są wewnątrz mój Konsument, który znajduje się wewnątrz mojej funkcji renderującej, co oznacza, że ​​za każdym razem, gdy moja funkcja renderująca jest wywoływana, wywoła moją akcję, aby uzyskać listę moich użytkowników, a to nie jest dobre, ponieważ będę wykonywać wiele niepotrzebnych żądań.

Jak więc mogę wywołać moją akcję tylko raz, na przykład in componentDidMountzamiast wywoływania renderowania?

Aby to zilustrować, spójrz na ten kod:

Załóżmy, że zawijam wszystko Providersw jeden komponent, w ten sposób:

import React from 'react';

import UserProvider from './UserProvider';
import PostProvider from './PostProvider';

export default class Provider extends React.Component {
  render(){
    return(
      <UserProvider>
        <PostProvider>
          {this.props.children}
        </PostProvider>
      </UserProvider>
    )
  }
}

Następnie umieszczam ten komponent dostawcy opakowujący całą moją aplikację, w ten sposób:

import React from 'react';
import Provider from './providers/Provider';
import { Router } from './Router';

export default class App extends React.Component {
  render() {
    const Component = Router();
    return(
      <Provider>
        <Component />
      </Provider>
    )
  }
}

Teraz, na przykład, dla moich użytkowników, będzie to mniej więcej tak:

import React from 'react';
import UserContext from '../contexts/UserContext';

export default class Users extends React.Component {
  render(){
    return(
      <UserContext.Consumer>
        {({getUsers, users}) => {
          getUsers();
          return(
            <h1>Users</h1>
            <ul>
              {users.map(user) => (
                <li>{user.name}</li>
              )}
            </ul>
          )
        }}
      </UserContext.Consumer>
    )
  }
}

Chcę tego:

import React from 'react';
import UserContext from '../contexts/UserContext';

export default class Users extends React.Component {
  componentDidMount(){
    this.props.getUsers();
  }

  render(){
    return(
      <UserContext.Consumer>
        {({users}) => {
          getUsers();
          return(
            <h1>Users</h1>
            <ul>
              {users.map(user) => (
                <li>{user.name}</li>
              )}
            </ul>
          )
        }}
      </UserContext.Consumer>
    )
  }
}

Ale oczywiście powyższy przykład nie działa, ponieważ getUsersrekwizyty nie znajdują się w widoku moich użytkowników. Jaki jest właściwy sposób, aby to zrobić, jeśli jest to w ogóle możliwe?

Gustavo Mendonça
źródło

Odpowiedzi:

144

EDYCJA: Wraz z wprowadzeniem punktów reagowania w wersji 16.8.0 można używać kontekstu w komponentach funkcjonalnych, korzystając z useContexthaka

const Users = () => {
    const contextValue = useContext(UserContext);
    // rest logic here
}

EDYCJA: od wersji 16.6.0 wzwyż. Możesz wykorzystać kontekst w metodzie cyklu życia, używając this.contextlike

class Users extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* perform a side-effect at mount using the value of UserContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* render something based on the value of UserContext */
  }
}
Users.contextType = UserContext; // This part is important to access context values

Przed wersją 16.6.0 można było to zrobić w następujący sposób

Aby użyć Context w swojej metodzie życia, powinieneś napisać komponent w stylu

class Users extends React.Component {
  componentDidMount(){
    this.props.getUsers();
  }

  render(){
    const { users } = this.props;
    return(

            <h1>Users</h1>
            <ul>
              {users.map(user) => (
                <li>{user.name}</li>
              )}
            </ul>
    )
  }
}
export default props => ( <UserContext.Consumer>
        {({users, getUsers}) => {
           return <Users {...props} users={users} getUsers={getUsers} />
        }}
      </UserContext.Consumer>
)

Ogólnie rzecz biorąc, możesz zachować jeden kontekst w swojej aplikacji i sensowne jest umieszczenie powyższego loginu w HOC, aby móc go ponownie wykorzystać. Możesz to napisać jak

import UserContext from 'path/to/UserContext';

const withUserContext = Component => {
  return props => {
    return (
      <UserContext.Consumer>
        {({users, getUsers}) => {
          return <Component {...props} users={users} getUsers={getUsers} />;
        }}
      </UserContext.Consumer>
    );
  };
};

a potem możesz go używać jak

export default withUserContext(User);
Shubham Khatri
źródło
To jest sposób na zrobienie tego! Ale pakuję swoje komponenty innymi rzeczami, więc kod będzie coraz bardziej skomplikowany w ten sposób. Tak więc, użycie biblioteki with-context i jej dekoratorów sprawia, że ​​kod jest znacznie prostszy. Ale dziękuję za odpowiedź!
Gustavo Mendonça
Dla mnie to nie działa, używam github z kontekstem
PassionateDeveloper
1
Shubham Khatri ma rację w swojej odpowiedzi, jest tylko mała literówka, która uniemożliwia to uruchomienie. Nawiasy klamrowe uniemożliwiają niejawny zwrot. Po prostu zastąp je nawiasami.
VDubs
1
Czy możesz wyjaśnić, jak używać w this.contextwielu kontekstach w tym samym komponencie? Załóżmy, że mam komponent, który musi mieć dostęp do UserContext i PostContext, jak sobie z tym poradzić? To, co widzę, to stworzenie kontekstu łączącego oba, ale to jedyne lub lepsze rozwiązanie na to?
Gustavo Mendonça
1
@ GustavoMendonça Możesz subskrybować tylko jeden kontekst przy użyciu implementacji API this.context. Jeśli chcesz przeczytać więcej niż jeden, musisz postępować zgodnie ze wzorem rekwizytów renderowania
Shubham Khatri,
3

Ok, znalazłem sposób na to z pewnymi ograniczeniami. Dzięki with-contextbibliotece udało mi się wstawić wszystkie moje dane konsumenckie do moich elementów składowych.

Ale wstawienie więcej niż jednego konsumenta do tego samego komponentu jest skomplikowane, musisz stworzyć mieszanych konsumentów za pomocą tej biblioteki, co sprawia, że ​​kod jest nieelegancki i nieproduktywny.

Link do tej biblioteki: https://github.com/SunHuawei/with-context

EDYCJA: Właściwie nie musisz używać wielokontekstowego interfejsu API, który with-contextzapewnia, w rzeczywistości, możesz użyć prostego interfejsu API i utworzyć dekorator dla każdego kontekstu, a jeśli chcesz użyć więcej niż jednego konsumenta w swoim komponencie, po prostu zadeklaruj ponad swoją klasę tylu dekoratorów, ile chcesz!

Gustavo Mendonça
źródło
1

Z mojej strony wystarczyło dodać .bind(this)do wydarzenia. Tak wygląda mój komponent.

// Stores File
class RootStore {
   //...States, etc
}
const myRootContext = React.createContext(new RootStore())
export default myRootContext;


// In Component
class MyComp extends Component {
  static contextType = myRootContext;

  doSomething() {
   console.log()
  }

  render() {
    return <button onClick={this.doSomething.bind(this)}></button>
  }
}
Ballpin
źródło
1
ten kod wygląda na znacznie prostszy, ale w rzeczywistości nie uzyskuje dostępu do wartości ani w ogóle nie współdziała z wystąpieniem RootStore. czy możesz pokazać przykład podzielony na dwa pliki i reaktywne aktualizacje interfejsu użytkownika?
dcsan
-8

Musisz przekazać kontekst w wyższym komponencie nadrzędnym, aby uzyskać dostęp jako rekwizyty w potomku.

kprocks
źródło