Reagovat Kontext - jak ho efektivně využít?

Ve svém příspěvku pro správu stavu aplikace React jsem zmínil, jak vám použití místního stavu s kontextem může pomoci spravovat stav jakékoli aplikace. Existuje několik příkladů, na které bych se nyní rád odkázal, abych vám ukázal, jak efektivně vytvářet kontextové spotřebitele, vyhnout se problémům a zároveň zlepšit čitelnost kódu a usnadnit jeho údržbu pro vaše aplikace a/nebo knihovny.

Nejprve vytvořte src/count-context.js a v něm kontext:

import * as React from 'react'

const CountContext = React.createContext()

Za prvé, nemám zde výchozí hodnotu pro kontext. Pokud bych to chtěl přidat, musel bych udělat něco takového:React.createContext({count: 0}) . Nicméně jsem to udělal schválně. Přidání výchozí hodnoty je užitečné pouze v tomto případě:

function CountDisplay() {
  const {count} = React.useContext(CountContext)
  return <div>{count}</div>
}

ReactDOM.render(<CountDisplay />, document.getElementById('⚛️'))

Protože nemáme výchozí hodnotu, dostaneme chybu na řádku, kde destrukturujeme hodnotu vrácenou z useContext() . Je to proto, že nemůžeme zničit undefined a toto je výchozí nastavení našeho kontextu.

Nikdo z nás tyto situace nemá rád, takže vaší instinktivní reakcí může být přidání výchozí hodnoty, abyste se vyhnuli chybě. K čemu by ale byl kontext, kdyby neodrážel aktuální stav věcí? Tím, že používal pouze výchozí hodnoty, toho moc nezmohl. 99 % času, kdy budete ve své aplikaci vytvářet a používat kontext, byste chtěli spotřebitelské komponenty (pomocí useContext() ) jsou poskytovány jako součást nadřazeného poskytovatele, který může poskytovat užitečnou hodnotu.

Dokumentace Reactu naznačuje, že zadání výchozí hodnoty „je užitečné při testování komponent v izolaci, protože není nutné je obalovat falešnými dodavateli“. I když je pravda, že vám to umožňuje, nesouhlasím s tím, že je to lepší, než dát komponentám potřebný kontext. Pamatujte, že pokaždé, když v testu uděláte něco, co není v aplikaci, snižujete tím důvěru, kterou vám test může dát. Existují pro to důvody, ale toto není jeden z nich.

Dobře, pokračujme. Aby byl tento kontextový modul vůbec užitečný, musíme použít poskytovatele a poskytnout komponentu, která poskytuje hodnotu. Naše komponenta bude použita následovně:

function App() {
  return (
    <CountProvider>
      <CountDisplay />
      <Counter />
    </CountProvider>
  )
}

ReactDOM.render(<App />, document.getElementById('⚛️'))

Vytvořme tedy komponentu, kterou lze použít takto:

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: *możesz* potrzebować memoizacji tej wartości
  // Dowiedz się więcej: http://kcd.im/optimize-context
  const value = {state, dispatch}
  return <CountContext.Provider value={value}>{children}</CountContext.Provider>
}

export {CountProvider}

V klidu, toto je vymyšlený příklad, který je záměrně vytvořen, aby ukázal, jaká by byla reálnější situace. To neznamená, že to bude pokaždé tak složité! Pokud to vyhovuje vašemu případu, můžete použít useState . Také některé komponenty dodavatele budou tak jednoduché a krátké, zatímco jiné budou MNOHEM spletitější, s mnoha háčky.

Vlastní spotřebitelský hák

Většina rozhraní API, která jsem viděl, vypadá nějak takto:

import * as React from 'react'
import {SomethingContext} from 'some-context-package'

function YourComponent() {
  const something = React.useContext(SomethingContext)
}

Ale myslím, že je to promarněná příležitost poskytnout lepší user experience . Podle mého názoru by to mělo být něco takového:

import * as React from 'react'
import {useSomething} from 'some-context-package'

function YourComponent() {
  const something = useSomething()
}

To má tu výhodu, že můžete dělat několik věcí, které vám ukážu v praxi:

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: *możesz* potrzebować memoizacji tej wartości
  // Dowiedz się więcej: 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}

Nejprve připojte useCount používá React.useContext získat kontextovou hodnotu z nejbližších CountProvider . Pokud taková hodnota neexistuje, vrátí chybu obsahující užitečnou zprávu, která značí, že hák nebyl zavolán na funkční komponentě vykreslené pod CountProvider . To je určitě chyba
proto může být cenné vrátit příslušnou zprávu. #FailFast

Vlastní spotřební komponenta

Pokud používáte Hooks, přeskočte tuto část. Pokud však potřebujete podporu React < 16.8.0 nebo si myslíte, že kontext musí být použit s komponentami třídy, zde je návod, jak to udělat s render-props :

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

Zde je návod, jak jej lze použít v komponentách třídy:

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

Používal jsem to předtím, než jsme měli k dispozici háčky a fungovalo to dobře. Nicméně nedoporučuji se tím trápit, pokud můžete používat Hooks. Háčky jsou mnohem lepší.

TypeScript

Slíbil jsem, že vám ukážu, jak se vyhnout výchozím chybám přeskakování pomocí TypeScriptu. Hádej co! Tím, co jsem zmínil, se problému hned vyhnete! Vlastně to není vůbec problém. Podívej se na to:

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: *możesz* potrzebować memoizacji tej wartości
  // Dowiedz się więcej: 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}

To umožňuje komukoli používat hook useCount aniž byste museli kontrolovat hodnotu, protože to děláme za něj!

Funkční příklad - CodeSandbox

A co překlepy v dispatch ?

Pokud chcete používat akční čaroděje, nevadí, ale nikdy se mi to moc nelíbilo. Vždy jsem je považoval za zbytečnou abstrakci. Tím, že používáte TypeScript a máte dobře citované akce, je s největší pravděpodobností nepotřebujete. Tímto způsobem získáte automatické doplňování syntaxe!

Doporučuji použít dispatch tímto způsobem jej udržuje stabilní po celou dobu životnosti komponenty, která jej vytvořila, takže se nemusíte obávat předání jako závislosti na useEffect .

Pokud nepíšete svůj kód JavaScript (pravděpodobně byste to měli změnit), bude vrácená chyba bezpečným řešením. Pojďme k další části, měla by vám pomoci.

A co asynchronní?

Dobrá otázka. Co když potřebujete provést asynchronní požadavek a změnit pár věcí, zatímco běží? Jistě to můžete udělat přímo v komponentě, ale ruční nastavení pro každou situaci by bylo docela otravné.

Navrhuji použít pomocnou funkci, která bere dispatch jako argumenty a další potřebné údaje a ponese odpovědnost za jejich nakládání se všemi. Zde je příklad z mého kurzu Advanced Patterns v Reactu:

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}

Pak to můžete použít takto:

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

Jsem s tímto vzorem spokojený, takže pokud byste chtěli, abych ho učil ve vaší společnosti, dejte mi vědět (nebo se přidejte do pořadníku na další workshop)!

Souhrn

Takto vypadá konečný kód:

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: *możesz* potrzebować memoizacji tej wartości
  // Dowiedz się więcej: 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}

Zde je funkční CodeSandbox

Všimněte si, že zde záměrně neexportuji CountContext . Používám pouze jeden způsob, jak nastavit a extrahovat hodnoty. Tím je zajištěno, že ostatní budou tyto hodnoty používat bezpečně.

Doufám, že vám tento článek pomohl! Pamatujte:

  1. Neměli byste používat kontext k řešení každého státního problému.

  2. Kontext nemusí být globální pro celou aplikaci, ale pouze pro její část.

  3. Můžete (a pravděpodobně byste měli) mít několik kontextů od sebe logicky oddělených.

Hodně štěstí!