Простой Redux с React Hooks + Context

JavaScript React.js
Простой Redux с React Hooks + Context

Автор: Чен Цзюньшэн

React HooksОфициально выпущен в версии React@16.8. Я недавно начал использовать его в одном или двух внутренних проектах компании.

не понимаюHooksодноклассникиДокументация. Эта статья невернаHooksСделайте подробное введение и опишите только одно использованиеHooksидеи.

Обычно мы пишемReactЕсли это не особенно большое приложение, логика взаимодействия данных внешнего и внутреннего интерфейса несложна, поэтому мы можем напрямую писать компоненты в соответствии с обычным процессом для удовлетворения простых бизнес-сценариев. С углублением бизнес-сценариев наши компоненты постепенно становятся все больше и больше, а обмен данными между компонентами (то есть управление состоянием, но я предпочитаю называть это обменом данными) становится все более и более сложным. Итак, мы представилиReduxподдерживать нашу все более сложную передачу данных.

идеи

Придерживаясь этой идеи, когда я разрабатывал приложение, я не вводил его в начале.Redux, потому что сначала я думал, что это небольшой проект. С развитием проекта не все так просто.

Но это не так уж сложно. В это время я положил глаз наContextтело.ContextПредназначенный для контекста, он обеспечиваетProviderс однимConsumer, то есть модель производитель/потребитель, которая обеспечиваетProvider, следующие дочерние элементы проходятConsumerпотреблятьProviderданные и методы.

Благодаря этой концепции мы разделяем один и тот же верхний уровень с компонентами на разных уровнях.Provider, и компонент внутри используетConsumerпотреблять общие данные.

Когда мы сможем обмениваться данными, останется только один вопрос: как изменитьProviderА данные в нем? ответ:useReducer.

Что ж, есть идея, давайте ее реализовывать.

Пример

Предположим, у нас есть родительский элемент на определенном уровне, которому необходимо совместно использовать состояние, мы называем его Родительским, и есть два дочерних элемента между разными уровнями ниже родительского. Здесь для простого примера предполагается, что у обоих детей общая логика.

import React from "react"

function Parent() {
  const colors = ['red', 'blue']
  return (
    <>
      <Child1 color={colors[0]} />
      <Child2 color={colors[1]} />
    </>
  )
}

function Child1(props) {
  return (
    <div style={{ background: props.color }}>I am {props.color}</div>
  )
}

function Child2(props) {
  return (
    <div style={{ background: props.color }}>I am {props.color}</div>
  )
}

Теперь мы построили такую ​​подчиненную структуру.В настоящее время, передавая свойства подкомпонентам, можно добиться разделения состояния родительских компонентов. Но если уровень углубляется здесь, уровень, на котором мы проходим атрибуты, также будет углубляться. Это явно не то, чего мы хотим.

Теперь давайте представимContext.

сначала черезcreateContextметод для инициализации того, что нам нужноContext.

import React, { createContext } from "react"

const Context = createContext({
  colors: ['red', 'blue']
})

Затем мы вводим только сейчас в Parent and ChildContextи использоватьuseContextПолучить общие данные:

import React, { useContext, createContext } from "react"

const Context = createContext({
  colors: []
})

function Parent() {
  const initState = {
    colors: ["red", "blue"]
  }

  return (
    <Context.Provider value={{ colors: initState.colors }}>
      <>
        {/* 假装这些地方有着不同的层级 */}
        <Child1 />
        <Child2 />
      </>
    </Context.Provider>
  )
}

function Child1(props) {
  const { colors } = useContext(Context);

  return (
    <div style={{ background: colors[0] }}>
      I am {colors[0]}
    </div>
  )
}

// 省略 Child2 代码,同 Child1 一致

Теперь просто получите данные и визуализируйте их, а затем сделайте еще один шаг и измените цвет, щелкнув элемент. Здесь нам нужно использоватьuseReducerдля имитации изменений триггера.

Сначала нам нуженreducerдля обработки инициированного изменения.


function reducer(state, action) {
  const { colors } = action
  if (action.type === "CHANGE_COLOR") {
    return { colors: colors }
  } else {
    throw new Error()
  }
}

Здесь я упростилactionконечно, вы также можете расширить его.

Теперь мы даемProviderплюс предоставить способ изменитьdispatch.

import React, { useContext, createContext } from "react"

const Context = createContext({
  colors: []
})

function Parent() {
  const initState = {
    colors: ["red", "blue"]
  }

  const [state, dispatch] = useReducer(reducer, initState)

  return (
    <Context.Provider value={{ colors: state.colors, dispatch: dispatch }}>
      <>
        {/* 假装这些地方有着不同的层级 */}
        <Child1 />
        <Child2 />
      </>
    </Context.Provider>
  )
}

Затем дочерний компонент инициирует изменение:


function Child1(props) {
  const { colors, dispatch } = useContext(Context)

  return (
    <div
      style={{ background: colors[0] }}
      onClick={() =>
        dispatch({
          type: "CHANGE_COLOR",
          colors: ["yellow", "blue"]
        })
      }
    >
      I am {colors[0]}
    </div>
  )
}

// 省略 Child2 代码,同 Child1 一致

На этом небольшой обмен состояниями завершен. Это то, от чего мы избавляемсяReduxПрототип идеи разделения состояния был реализован позже. Полный код и примеры см.tiny redux.

Передовой

В практических приложениях наши бизнес-сценарии будут более сложными, например, наши данные собираются динамически.

В этом случае можно поставитьProviderВыньте его и инициализируйте, когда вернутся родительские данные.Context.


function Provider (props) {
  const { colors } = props
  const initState = {
    colors,
  }
  const [state, dispatch] = useReducer(reducer, initState)

  return (
    <Context.Provider value={{ colors: state.colors, dispatch: dispatch }}>
      {props.children}
    </Context.Provider>
  )
}

Затем мы делаем асинхронные операции в Parent и передаем динамические данные вProvider :


import React, { useState, useEffect } from "react"

function Parent (props) {
  const [data, setData] = useState()
  const [url, setUrl] = useState('https://example.com')

  useEffect(() => {
    fetch(url).then(res => setData(data))
  }, [url])

  if (!data) return <div>Loading ...</div>

  return (
    <Provider colors={data}>
      <>
        {/* 假装这些地方有着不同的层级 */}
        <Child1 />
        <Child2 />
      </>
    </Provider>
  )
}

глубоко

Мы можем пойти еще дальше и сделать наш механизм управления состоянием более упорядоченным.

Во-первых, определим, что нам нужно на уровне компонентовContext. Допустим, мы здесь на верхнем уровне (то есть глобальном управлении состоянием).

import React from 'react'

// 创建我们需要的 Context
export const AppContext = React.createContext(null)

Тогда мы будемuseReducerВозвращаемое значение передается непосредственно вAppContext.Provider.

import React, { useReducer } from 'react'

// 全局 Provider
export function AppProvider ({reducer, initValue, children}) {
  return (
    <AppContext.Provider value={useReducer(reducer, initValue)}>
      {children}
    </AppContext.Provider>
  )
}

Наконец, добавьте пользовательскийhooksполучитьAppContextсостояние и методы. Напиши один раз, беги везде :)

import React, { useReducer, useContext } from 'react'

export const useAppState = () => useContext(AppContext)

Наконец нашstate.jsПолный код выглядит следующим образом:

import React, { useContext, useReducer } from 'react'

export const AppContext = React.createContext(null)

export function AppProvider ({reducer, initValue, children}) {
  return (
    <AppContext.Provider value={useReducer(reducer, initValue)}>
      {children}
    </AppContext.Provider>
  )
}

export const useAppState = () => useContext(AppContext)

Компоненты используют:

import { AppProvider, useAppState } from "./state"

function App() {
  const initState = {
    colors: ["red", "blue"]
  }

  function reducer(state, action) {
    const { colors } = action;
    if (action.type === "CHANGE_COLOR") {
      return { colors: colors };
    } else {
      throw new Error();
    }
  }

  return (
    <AppProvider initValue={initState} reducer={reducer}>
      <div>
        {/* 假装这些地方有着不同的层级 */}
        <Child1 />
        <Child2 />
      </div>
    </AppProvider>
  )
}

function Child1(props) {
  const [state, dispatch] = useAppState()

  return (
    <div
      style={{ background: state.colors[0] }}
      onClick={() =>
        dispatch({
          type: "CHANGE_COLOR",
          colors: ["yellow", "blue"]
        })
      }
    >
      I am {state.colors[0]}
    </div>
  )
}

Полный код и примеры см.tiny redux.

Эпилог

Вы даже можете поместить такой небольшой механизм управления состоянием в компонент, вместо того, чтобы помещать его в что-то вродеReduxк глобальной среде. Это делает приложения, которые мы пишем, более гибкими, а не вслепуюstoreпотерянное состояние. Конечно, вы также можете написатьAppProviderуправлять глобальным состоянием,React Hooks + Contextдал нам такое удобство.

Крючки вкусные!

Небольшие программы также можно разрабатывать с помощью хуков, подробнее«Рефакторинг ваших апплетов с помощью React Hooks»