¿Cómo actualizar React Context desde dentro de un componente secundario?

98

Tengo la configuración de idioma en el contexto como se muestra a continuación

class LanguageProvider extends Component {
  static childContextTypes = {
    langConfig: PropTypes.object,
  };

  getChildContext() {
    return { langConfig: 'en' };
  }

  render() {
    return this.props.children;
  }
}

export default LanguageProvider;

El código de mi aplicación será algo como el siguiente

<LanguageProvider>
  <App>
    <MyPage />
  </App>
</LanguageProvider>

Mi página tiene un componente para cambiar el idioma

<MyPage>
  <LanguageSwitcher/>
</MyPage>

LanguageSwitcher en esta MyPagenecesidad de actualizar el contexto para cambiar el idioma a 'jp' como se muestra a continuación

class LanguageSwitcher extends Component {
  static contextTypes = {
    langConfig: PropTypes.object,
  };

  updateLanguage() {
    //Here I need to update the langConfig to 'jp' 
  }

  render() {
    return <button onClick={this.updateLanguage}>Change Language</button>;
  }
}

export default LanguageSwitcher;

¿Cómo puedo actualizar el contexto desde dentro del componente LanguageSwitcher?

mshameer
fuente
¿Has leído esto? facebook.github.io/react/docs/context.html#updating-context Quizás esto sea algo más adecuado para el estado, no para el contexto
azium
@azium Sí ... En ese documento, el contexto se actualiza desde el componente en sí o hay un enlace de blog agregado en el documento que contiene el contexto pasado como un accesorio al proveedor de contexto.Necesito actualizarlo desde el componente secundario
mshameer
Uhh no, el documento dice que no use el contexto si necesita actualizarlo. "No lo hagas" para ser precisos. Lo reiteraré, use el estado, no el contexto
azium
2
@LondonRob ¿Qué tipo de respuesta canónica estás buscando? En mi opinión, el contenido de los documentos me parece muy bien. Si desea establecer el contexto en un niño, simplemente cree un establecedor en el componente del proveedor y páselo a un consumidor infantil. Luego, llame a ese establecedor en el consumidor secundario y configúrelo en cualquier dato que esté dentro del niño. Aún sigue con la idea de React de elevar los datos.
Andrew Li
2
@azium es solo un aviso para otros que lean este comentario todos estos años después. Actualizar el contexto de un componente secundario es ahora compatible y bastante sencillo: hyp.is/FiP3mG6fEeqJiOfWzfKpgw/reactjs.org/docs/context.html
Lustig

Respuestas:

226

Usando ganchos

Los ganchos se introdujeron en 16.8.0, por lo que el siguiente código requiere una versión mínima de 16.8.0 (desplácese hacia abajo para ver el ejemplo de componentes de clase). Demostración de CodeSandbox

1. Establecer el estado principal para el contexto dinámico

En primer lugar, para tener un contexto dinámico que se pueda pasar a los consumidores, usaré el estado del padre. Esto asegura que tengo una única fuente de verdad. Por ejemplo, mi aplicación principal se verá así:

const App = () => {
  const [language, setLanguage] = useState("en");
  const value = { language, setLanguage };

  return (
    ...
  );
};

Se languagealmacena en el estado. Pasaremos ambos languagey la función setter a setLanguagetravés del contexto más adelante.

2. Crear un contexto

A continuación, creé un contexto de lenguaje como este:

// set the defaults
const LanguageContext = React.createContext({
  language: "en",
  setLanguage: () => {}
});

Aquí estoy configurando los valores predeterminados para language('en') y una setLanguagefunción que será enviada por el proveedor de contexto a los consumidores. Estos son solo valores predeterminados y proporcionaré sus valores cuando use el componente de proveedor en el padre App.

Nota: lo LanguageContextmismo sigue siendo si

3. Crear un consumidor de contexto

Para que el selector de idioma configure el idioma, debe tener acceso a la función de configuración de idioma a través del contexto. Puede verse algo como esto:

const LanguageSwitcher = () => {
  const { language, setLanguage } = useContext(LanguageContext);
  return (
    <button onClick={() => setLanguage("jp")}>
      Switch Language (Current: {language})
    </button>
  );
};

Aquí solo estoy configurando el idioma en 'jp', pero es posible que tenga su propia lógica para configurar los idiomas para esto.

4. Envolviendo al consumidor en un proveedor

Ahora renderizaré mi componente de cambio de idioma en ay pasaré LanguageContext.Providerlos valores que deben enviarse a través del contexto a cualquier nivel más profundo. Así es como se Appve mi padre :

const App = () => {
  const [language, setLanguage] = useState("en");
  const value = { language, setLanguage };

  return (
    <LanguageContext.Provider value={value}>
      <h2>Current Language: {language}</h2>
      <p>Click button to change to jp</p>
      <div>
        {/* Can be nested */}
        <LanguageSwitcher />
      </div>
    </LanguageContext.Provider>
  );
};

Ahora, cada vez que se hace clic en el conmutador de idioma, se actualiza el contexto de forma dinámica.

Demostración de CodeSandbox

Usando componentes de clase

La última API de contexto se introdujo en React 16.3, que proporciona una excelente manera de tener un contexto dinámico. El siguiente código requiere una versión mínima de 16.3.0. Demostración de CodeSandbox

1. Establecer el estado principal para el contexto dinámico

En primer lugar, para tener un contexto dinámico que se pueda pasar a los consumidores, usaré el estado del padre. Esto asegura que tengo una única fuente de verdad. Por ejemplo, mi aplicación principal se verá así:

class App extends Component {
  setLanguage = language => {
    this.setState({ language });
  };

  state = {
    language: "en",
    setLanguage: this.setLanguage
  };

  ...
}

El languagese almacena en el estado junto con un método de establecimiento de idioma, que puede mantener fuera del árbol de estado.

2. Crear un contexto

A continuación, creé un contexto de lenguaje como este:

// set the defaults
const LanguageContext = React.createContext({
  language: "en",
  setLanguage: () => {}
});

Aquí estoy configurando los valores predeterminados para language('en') y una setLanguagefunción que será enviada por el proveedor de contexto a los consumidores. Estos son solo valores predeterminados y proporcionaré sus valores cuando use el componente de proveedor en el padre App.

3. Crear un consumidor de contexto

Para que el selector de idioma configure el idioma, debe tener acceso a la función de configuración de idioma a través del contexto. Puede verse algo como esto:

class LanguageSwitcher extends Component {
  render() {
    return (
      <LanguageContext.Consumer>
        {({ language, setLanguage }) => (
          <button onClick={() => setLanguage("jp")}>
            Switch Language (Current: {language})
          </button>
        )}
      </LanguageContext.Consumer>
    );
  }
}

Aquí solo estoy configurando el idioma en 'jp', pero es posible que tenga su propia lógica para configurar los idiomas para esto.

4. Envolviendo al consumidor en un proveedor

Ahora renderizaré mi componente de cambio de idioma en ay pasaré LanguageContext.Providerlos valores que deben enviarse a través del contexto a cualquier nivel más profundo. Así es como se Appve mi padre :

class App extends Component {
  setLanguage = language => {
    this.setState({ language });
  };

  state = {
    language: "en",
    setLanguage: this.setLanguage
  };

  render() {
    return (
      <LanguageContext.Provider value={this.state}>
        <h2>Current Language: {this.state.language}</h2>
        <p>Click button to change to jp</p>
        <div>
          {/* Can be nested */}
          <LanguageSwitcher />
        </div>
      </LanguageContext.Provider>
    );
  }
}

Ahora, cada vez que se hace clic en el conmutador de idioma, se actualiza el contexto de forma dinámica.

Demostración de CodeSandbox

Divyanshu Maithani
fuente
¿Cuál es el propósito de los valores predeterminados con los que inicializa el contexto? ¿No son esos valores predeterminados siempre anulados por el Provider?
ecoe
@ecoe correcto; sin embargo, en caso de que el proveedor apruebe no value, el consumidor utilizará los valores predeterminados.
Divyanshu Maithani
1
¿Por qué los contextos se limitan a establecer / obtener un valor simple ... Eso parece muy ineficiente. Un mejor ejemplo sería resaltar un contexto que tiene predeterminado como objeto y actualizar el objeto en consecuencia.
AlxVallejo
Es posible utilizar contextos para lo que está mencionando. El ejemplo en la respuesta fue retorno basado en la pregunta. Por lo tanto, contiene solo un valor para la brevedad.
Divyanshu Maithani
1
Typecript se queja en mi caso si setLanguage no tiene parámetros. setLanguage: (language: string) => {}funciona para mi.
alex351
48

La respuesta anterior de Maithani es una gran solución, pero es un componente de clase sin el uso de ganchos. Dado que React recomienda el uso de componentes funcionales y ganchos, lo implementaré con los ganchos useContext y useState. Así es como puede actualizar el contexto desde un componente secundario.

LanguageContextMangement.js

import React, { useState } from 'react'

export const LanguageContext = React.createContext({
  language: "en",
  setLanguage: () => {}
})

export const LanguageContextProvider = (props) => {

  const setLanguage = (language) => {
    setState({...state, language: language})
  }

  const initState = {
    language: "en",
    setLanguage: setLanguage
  } 

  const [state, setState] = useState(initState)

  return (
    <LanguageContext.Provider value={state}>
      {props.children}
    </LanguageContext.Provider>
  )
}

App.js

import React, { useContext } from 'react'
import { LanguageContextProvider, LanguageContext } from './LanguageContextManagement'

function App() {

  const state = useContext(LanguageContext)

  return (
    <LanguageContextProvider>
      <button onClick={() => state.setLanguage('pk')}>
        Current Language is: {state.language}
      </button>
    </LanguageContextProvider>
  )
}

export default App
Mateen Kiani
fuente
1
Estoy haciendo esto y mi función de conjunto dentro de mi componente hijo es siempre la que declaramos inicialmente al crear el contexto:() => {}
Alejandro Corredor
4
Para aclarar, creo que en tu ejemplo state.setLanguage('pk')no haré nada, ya que const state = useContext(LanguageContext)está fuera del LanguageContextProvider. Resolví mi problema moviendo el proveedor un nivel hacia arriba y luego usándolo useContexten un niño un nivel por debajo.
Alejandro Corredor
2
En caso si usted no desea mover su contexto nivel de un proveedor de seguimiento también se puede utilizar consumidor de contexto como esto: <LanguageContext.Consumer> {value => /* access your value here */} </LanguageContext.Consumer>.
Mateen Kiani
3
Me gusta mucho la forma en que organizas el archivo LanguageContextMangement.js. En mi opinión, esa es una forma limpia de hacer las cosas y voy a empezar a hacerlo ahora. ¡Gracias!
lustig
2
¡Gracias por el agradecimiento que realmente me anima a seguir haciendo un trabajo como este!
Mateen Kiani
2

Una solución bastante simple es establecer el estado en su contexto incluyendo un método setState en su proveedor así:

return ( 
            <Context.Provider value={{
              state: this.state,
              updateLanguage: (returnVal) => {
                this.setState({
                  language: returnVal
                })
              }
            }}> 
              {this.props.children} 
            </Context.Provider>
        )

Y en su consumidor, llame a updateLanguage así:

// button that sets language config
<Context.Consumer>
{(context) => 
  <button onClick={context.updateLanguage({language})}> 
    Set to {language} // if you have a dynamic val for language
  </button>
<Context.Consumer>
struensee
fuente