DEV Community

loading...

Cómo usar React Context de manera efectiva

Jeremias Fernandez
Updated on ・8 min read

Versíon traducida del articulo de Kent C. Dodds.

Cómo crear y exponer a los providers y consumers de React Context

En Application State Management with React, Kent C. Dodds habla de cómo el uso de una combinación de estado local y React Context puede ayudarte a administrar bien el estado en cualquier aplicación React. Muestra algunos ejemplos y menciona algunas cosas sobre esos ejemplos y cómo puedes crear Context consumers de React de manera efectiva para evitar algunos problemas y mejorar la experiencia del desarrollador y la capacidad de mantenimiento de los objetos de contexto que creas para tu aplicación y / o bibliotecas .

Nota: lee Application State Management con React y sigue el consejo de que no se debe recurrir a Context para resolver todos los problemas de intercambio de estados que se cruzan en tu escritorio. Cuando necesites recurrir a Context, es de esperar que esta publicación de blog te ayude a saber cómo hacerlo de manera efectiva. Además, recuerda que el contexto NO tiene que ser global para toda la aplicación, puede aplicarse a una parte de su árbol y puede (y probablemente debería) tener múltiples contextos separados lógicamente en tu aplicación.

// src/count-context.js

import * as React from 'react'

const CountContext = React.createContext()
Enter fullscreen mode Exit fullscreen mode

En primer lugar, no tengo un valor inicial para CountContext. Si quisiera un valor inicial, llamaría a React.createContext ({count: 0}). Pero no incluyo un valor predeterminado y eso es intencional. El valor predeterminado solo es útil en una situación como esta:

function CountDisplay() {

  const {count} = React.useContext(CountContext)

  return <div>{count}</div>

}

ReactDOM.render(<CountDisplay />, document.getElementById('⚛️'))
Enter fullscreen mode Exit fullscreen mode

Debido a que no tenemos un valor predeterminado para nuestro CountContext, obtendremos un error en la línea resaltada donde estamos desestructurando el valor de retorno de useContext. Esto se debe a que nuestro valor predeterminado no está definido y no se puede desestructurar sin definir.

A ninguno de nosotros nos gustan los errores de tiempo de ejecución, por lo que tu reacción instintiva puede ser agregar un valor predeterminado para evitar el error de "tiempo de ejecución". Sin embargo, ¿de qué serviría el contexto si no tuviera un valor real? Si solo estás usando el valor predeterminado que se proporcionó, entonces realmente no puede hacer mucho bien. El 99% del tiempo que vas a crear y usar contexto en tu aplicación, desea que tus Context Consumers (aquellos que usan useContext) se representen dentro de un provider que pueda proporcionar un valor útil.

Nota: hay situaciones en las que los valores predeterminados son útiles, pero la mayoría de las veces no son necesarios ni útiles.

Los documentos de React sugieren que proporcionar un valor predeterminado "puede ser útil para probar componentes de forma aislada sin wrappearlos". Si bien es cierto que te permite hacer esto, no estoy de acuerdo en que sea mejor que wrappear sus componentes con el context necesario. Recuerda que cada vez que hace algo en su prueba que no hace en su aplicación, reduce la cantidad de confianza que la prueba puede brindarte. Hay razones para hacer esto, pero esa no es una de ellas.

Nota: Si estás usando TypeScript, no proporcionar un valor predeterminado puede ser realmente molesto para las personas que usan React.useContext, pero te voy a mostrar cómo evitar ese problema por completo a continuación. ¡Sigue leyendo!

El componente Custom Provider

Ok, continuemos. Para que este módulo de contexto sea útil en absoluto, debemos usar el Provider y exponer un componente que proporcione un valor. Nuestro componente se utilizará así:

import * as React from 'react'

import {SomethingContext} from 'some-context-package'

function YourComponent() {

  const something = React.useContext(SomethingContext)

}
Enter fullscreen mode Exit fullscreen mode

Pero creo que es una oportunidad perdida para brindar una mejor experiencia de usuario. En cambio, creo que debería ser así:

import * as React from 'react'

import {useSomething} from 'some-context-package'

function YourComponent() {

  const something = useSomething()

}
Enter fullscreen mode Exit fullscreen mode

Esto tiene la ventaja de que puedes hacer algunas cosas que te mostraré en la implementación ahora:

// src/count-context.js

import * as React from 'react'

const CountContext = React.createContext()

function countReducer(state, action) {

  switch (action.type) {

    case 'increment': {

      return {count: state.count + 1}

    }

    case 'decrement': {

      return {count: state.count - 1}

    }

    default: {

      throw new Error(`Unhandled action type: ${action.type}`)

    }

  }

}

function CountProvider({children}) {

  const [state, dispatch] = React.useReducer(countReducer, {count: 0})

  // NOTE: you *might* need to memoize this value

  // Learn more in http://kcd.im/optimize-context

  const value = {state, dispatch}

  return <CountContext.Provider value={value}>{children}</CountContext.Provider>

}

function useCount() {

  const context = React.useContext(CountContext)

  if (context === undefined) {

    throw new Error('useCount must be used within a CountProvider')

  }

  return context

}

export {CountProvider, useCount}
Enter fullscreen mode Exit fullscreen mode

Primero, el custom hook useCount usa React.useContext para obtener el valor de contexto proporcionado del CountProvider más cercano. Sin embargo, si no hay ningún valor, arrojamos un mensaje de error útil que indica que no se está llamando al hook dentro de un componente de función que se representa dentro de un CountProvider. Sin duda, esto es un error, por lo que proporcionar el mensaje de error es valioso. #FailFast

El Componente Custom Consumer

Si puedes usar hooks, omite esta sección. Sin embargo, si necesitas soportar React <16.8.0, o crees que el Context debe ser consumido por los componentes de clase, así es como podría hacer algo similar con la API basada en renderizado para context consumers:

function CountConsumer({children}) {

  return (

    <CountContext.Consumer>

      {context => {

        if (context === undefined) {

          throw new Error('CountConsumer must be used within a CountProvider')

        }

        return children(context)

      }}

    </CountContext.Consumer>

  )

}
Enter fullscreen mode Exit fullscreen mode

Y así es como los componentes de clase lo usarían:

class CounterThing extends React.Component {

  render() {

    return (

      <CountConsumer>

        {({state, dispatch}) => (

          <div>

            <div>{state.count}</div>

            <button onClick={() => dispatch({type: 'decrement'})}>

              Decrement

            </button>

            <button onClick={() => dispatch({type: 'increment'})}>

              Increment

            </button>

          </div>

        )}

      </CountConsumer>

    )

  }

}
Enter fullscreen mode Exit fullscreen mode

Esto es lo que solía hacer antes de que tuviéramos hooks y funcionaba bien. Sin embargo, no recomendaría molestarse con esto si puedes usar hooks. Los hooks son mucho mejores.

Typescript

Te prometí que te mostraría cómo evitar problemas al omitir el valor predeterminado al usar TypeScript. ¡Adivina qué! ¡Al hacer lo que estoy sugiriendo, evitas el problema por defecto! En realidad, no es un problema en absoluto. Echa un vistazo:

// src/count-context.tsx

import * as React from 'react'

type Action = {type: 'increment'} | {type: 'decrement'}

type Dispatch = (action: Action) => void

type State = {count: number}

type CountProviderProps = {children: React.ReactNode}

const CountStateContext = React.createContext<

  {state: State; dispatch: Dispatch} | undefined

>(undefined)

function countReducer(state: State, action: Action) {

  switch (action.type) {

    case 'increment': {

      return {count: state.count + 1}

    }

    default: {

      throw new Error(`Unhandled action type: ${action.type}`)

    }

  }

}

function CountProvider({children}: CountProviderProps) {

  const [state, dispatch] = React.useReducer(countReducer, {count: 0})

  // NOTE: you *might* need to memoize this value

  // Learn more in http://kcd.im/optimize-context

  const value = {state, dispatch}

  return (

    <CountStateContext.Provider value={value}>

      {children}

    </CountStateContext.Provider>

  )

}

function useCount() {

  const context = React.useContext(CountStateContext)

  if (context === undefined) {

    throw new Error('useCount must be used within a CountProvider')

  }

  return context

}

export {CountProvider, useCount}
Enter fullscreen mode Exit fullscreen mode

Con eso, cualquiera puede usar useCount sin tener que hacer undefined-checks, ¡porque lo estamos haciendo por ellos!

Aquí hay un codesandbox

¿Qué pasa con los errores dispatch de tipado?

En este punto, ustedes, los reduxers, están gritando: "¡Ey, ¿dónde están los action creators?". Si quieres implementar action creators, está bien por mí, pero nunca me gustaron los action creators. Siempre sentí que eran una abstracción innecesaria. Además, si estas utilizando TypeScript y sus actions están bien escritas, entonces no deberías necesitarlas. ¡Puede obtener errores de tipado y autocompletado en línea!

Realmente me gusta pasar el dispatch de esta manera y, como beneficio adicional, el dispatch es estable durante la vida útil del componente que lo creó, por lo que no necesita preocuparse por pasarlo a las listas de dependencias useEffect (no importa si está incluido o no).

Si no estas tipando tu JavaScript (probablemente deberías considerarlo si no lo has hecho), entonces el error que arrojamos para los tipos de actions perdidas es a prueba de fallos. Además, lee la siguiente sección porque esto también puede ayudarte.

Que pasa con las async actions?

Esta es una gran pregunta. ¿Qué sucede si tienes una situación en la que necesitas realizar una solicitud asincrónica y necesitas enviar varias cosas en el transcurso de esa solicitud? Seguro que podrías hacerlo en el componente de llamada, pero conectar manualmente todo eso para cada componente que necesita hacer algo así sería bastante molesto.

Lo que sugiero es que crees una función auxiliar dentro de su módulo de contexto que acepte el dispatch junto con cualquier otro dato que necesite, y haga que ese helper sea responsable de lidiar con todo eso. Aquí hay un ejemplo de mi taller Advanced React Patterns:

// user-context.js

async function updateUser(dispatch, user, updates) {

  dispatch({type: 'start update', updates})

  try {

    const updatedUser = await userClient.updateUser(user, updates)

    dispatch({type: 'finish update', updatedUser})

  } catch (error) {

    dispatch({type: 'fail update', error})

  }

}

export {UserProvider, useUser, updateUser}
Enter fullscreen mode Exit fullscreen mode

Entonces puedes usar eso así:

// user-profile.js

import {useUser, updateUser} from './user-context'

function UserSettings() {

  const [{user, status, error}, userDispatch] = useUser()

  function handleSubmit(event) {

    event.preventDefault()

    updateUser(userDispatch, user, formState)

  }

  // more code...

}
Enter fullscreen mode Exit fullscreen mode

Estoy muy contento con este patrón y si desea que lo enseñe en tu empresa, házmelo saber (o agrégate a la lista de espera para la próxima vez que organice el taller).

Conclusión

Así que aquí está la versión final del código:

// src/count-context.js

import * as React from 'react'

const CountContext = React.createContext()

function countReducer(state, action) {

  switch (action.type) {

    case 'increment': {

      return {count: state.count + 1}

    }

    case 'decrement': {

      return {count: state.count - 1}

    }

    default: {

      throw new Error(`Unhandled action type: ${action.type}`)

    }

  }

}

function CountProvider({children}) {

  const [state, dispatch] = React.useReducer(countReducer, {count: 0})

  // NOTE: you *might* need to memoize this value

  // Learn more in http://kcd.im/optimize-context

  const value = {state, dispatch}

  return <CountContext.Provider value={value}>{children}</CountContext.Provider>

}

function useCount() {

  const context = React.useContext(CountContext)

  if (context === undefined) {

    throw new Error('useCount must be used within a CountProvider')

  }

  return context

}

export {CountProvider, useCount}
Enter fullscreen mode Exit fullscreen mode

Aquí hay un codesandbox funcionando

Ten en cuenta que NO estoy exportando CountContext. Esto es intencional. Expongo solo una forma de proporcionar el valor de contexto y solo una forma de consumirlo. Esto me permite asegurarme de que las personas estén usando el valor de contexto de la manera que debería ser y me permite proporcionar buenas utilidades para mis consumidores.

¡Espero que ésto sea útil para ti!

Recuerda:

  1. No deberías recurrir al context para resolver todos los problemas de intercambio de estados que se cruzan en su escritorio.

  2. El Context NO tiene que ser global para toda la aplicación, pero se puede aplicar a una parte de tu árbol

  3. Puede (y probablemente debería) tener varios contextos separados lógicamente en tu aplicación.

¡Buena suerte!

Discussion (0)