Создание собственного клона Redux с помощью библиотеки перекомпоновки.

Несколько недель назад я немного поигрался с библиотекой перекомпоновки.

Recompose - это набор помощников, которые создают компоненты высшего порядка (HOC) для React и добавляют инкапсулируемое поведение, которое вы можете применить к любому компоненту React.

Создавая что-то на codeandbox (это замечательный инструмент, вы должны его использовать!), я решил, что создам крошечный клон redux из того, что я только что узнал.

Что такое компонент высшего порядка?

В функциональном программировании мы называем «функцией высшего порядка» функцию, которая принимает функции как параметры и / или возвращает новую функцию.

// Here is our higher order function
const andThen = (fn) => (promise) => promise.then(fn)
// Here is a simple function (or first-order function)
const logger = (x) => console.log(x)
// andThen(logger) returns a new function that expects a promise !
const logPromise = andThen(logger)

Давайте воспользуемся нашей logPromiseфункцией:

const p1 = Promise.resolve(42)
logPromise(p1)
// logs '42'

Теперь вы знаете, что такое функция высшего порядка. Я уверен, что вы уже использовали его раньше. Эти функции есть везде в Javascript. (Подсказка Array.forEach 😉 😉)

Используя ту же аналогию, «компонент высшего порядка» (или HOC) - это функция, которая принимает компоненты React в качестве параметров и / или возвращает компонент React.

Теперь, когда мы знаем, что такое HOC, давайте рассмотрим простой пример Recompose.

Давай попробуем перекомпоновать

Recompose предоставляет нам вспомогательные функции, позволяющие создавать HOC.

Эти помощники могут быть объединены в и добавлять поведения к любым компонентам реакции. Они принимают базовый компонент и возвращают новый компонент с дополнительными функциями.

Мой главный вывод о перекомпоновке:

Что, если бы вы могли создавать реагирующие компоненты, не полагаясь на "это"?

Давайте посмотрим на пример из документации:

const enhance = withState('counter', 'setCounter', 0)
const Counter = enhance(({ counter, setCounter }) =>
  <div>
    Count: {counter}
    <button onClick={() => setCounter(n => n + 1)}>Increment</button>
    <button onClick={() => setCounter(n => n - 1)}>Decrement</button>
  </div>
)

Здесь withStateHOC принимает 3 параметра:

  1. ключ 'counter' - это имя поля, в котором вы будете хранить свое состояние, как и в случае с компонентами класса с this.state.counter
  2. ключ 'setCounter' - это имя функции, которая обновит состояние counter новым значением, например this.setState(oldState => ({counter: oldState + 1 }))
  3. начальное значение 'counter'

Значение состояния counter и средство обновления состояния setCounter будут переданы как реквизиты вашему компоненту, обернутому HOC enhance

Это похоже на использование setState, но в функциональном компоненте реакции! Это огромно!

Также доступны другие помощники, такие как mapProps, defaultProps, withContext, withReducer, withHandler, lifecycle (для подключения к обратным вызовам жизненного цикла React)… (и многие другие!)

Если вы хотите узнать больше о Recompose, я предлагаю вам эту отличную статью от @sharifsbeat: Почему хипстеры все заново сочиняют

Перекомпоновка Redux

Итак, как я сказал в начале этого поста, я играл с Recompose и подумал: «Эй, эта штука с перекомпоновкой крута! Я попробую создать на нем клон Redux »

Позвольте мне показать вам, что я сделал 😉

createStore

В Redux функция createStore создает хранилище redux.

Он принимает initialState и reducer и возвращает новое хранилище:

const createStore = (initialState, reducer) => {
  let state = initialState
  return {
    getState: () => state,
    subscribe: cb => {},
    dispatch: action => {
      state = reducer(state, action)
    }
  }
}

Вот базовый магазин redux.

Теперь давайте добавим эмиттер событий для обновления всех подключенных компонентов при изменении состояния.

Для этого я использовал mitt от @_developit

import mitt from 'mitt'
const createStore = (initialState, reducer) => {
  // a new event emitter instance
  const emitter = mitt()
  let state = initialState
  return {
    getState: () => state,
    subscribe: cb => {
      // subscribe to "update" event
      emitter.on('update', cb)
      return () => emitter.off('update', cb)
    },
    dispatch: action => {
      state = reducer(state, action)
      // trigger an "update" event
      emitter.emit('update', state)
    }
  }
}

Вот и все! Теперь у нас есть готовая функция createStore!

Компонент Provider

Providercomponent в Redux принимает хранилище redux в своих реквизитах и ​​передает его всем подключенным компонентам через Context API React.

Нам нужно будет использовать функцию withContext из перекомпоновки:

import React from 'react'
import { withContext } from 'recompose'
import PropTypes from 'prop-types'
// defining contextTypes is mandatory
const contextTypes = { store: PropTypes.object }
const Context = withContext(contextTypes, props => ({
  store: props.store
}))
const Provider = Context(({ children }) => <div>{children}</div>)

Мы берем хранилище redux из реквизита Provider и помещаем его под ключ 'store' объекта контекста

React-redux connect

Функция connect из 'react-redux' принимает 2 функции (mapStateToProps и mapDispatchToProps) и возвращает HOC.

Этот HOC заботится о:

  1. получение магазина из контекста
  2. передача состояния из магазина в компонент
  3. подписка на изменения в магазине

Для каждого из этих шагов мы будем использовать специальный помощник для перекомпоновки:

  1. получение магазина из контекста - ›getContext
  2. передача состояния из магазина в компонент - ›withState
  3. подписка на изменения магазина - ›lifecyle

Теперь давайте скомпонуем их, используя функцию compose из 'recompose'!

import React from 'react'
import { compose, withState, getContext, lifecycle } from 'recompose'
import PropTypes from 'prop-types'
// defining contextTypes is mandatory
const contextTypes = { store: PropTypes.object }
const enhance = compose(
  // 1. getting the store from the context
  getContext(contextTypes),
  // 2. passing down the state from the store to the component
  //    using the 'state' prop
  withState('state', 'setStoreState', {}),
  // 3. subscribing to store changes
  lifecycle({
    componentDidMount() {
      const { store, setStoreState } = this.props
      setStoreState(store.getState())
      this.unsub = store.subscribe(newState => {
        setStoreState(newState)
      })
    },
    componentWillUnmount() {
      if (this.unsub) {
        this.unsub()
        this.unsub = null
      }
    }
  })
)

Теперь мы можем создать нашу connect функцию:

const connect = (mapStateToProps, mapDispatchToProps) => Component =>
  enhance(({ store, state, ...ownProps }) => {
    const derivedState = mapStateToProps
      ? mapStateToProps(state, ownProps)
      : state
    const dispatchers = mapDispatchToProps
      ? mapDispatchToProps(store.dispatch, ownProps)
      : store.dispatch
    return <Component {...derivedState} {...dispatchers} {...ownProps} />
  })

Выполнено!

Теперь у нас есть все на месте в нашем клоне redux.

Не будем забывать, что кодовая база redux имеет гораздо больше проверок и оптимизаций, чем эта простая реализация (например, для предотвращения ненужных повторных отрисовок при вызове store.dispatch())

Используя наш «перекомпонованный» редукс

Давайте создадим редуктор, начальное состояние и хранилище:

const reducer = (state, action) => {
  const value = action.value || 1
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + value }
    case 'DECREMENT':
      return { ...state, count: state.count - value }
    default:
      return state
  }
}
const initialState = { count: 0 }
const store = createStore(initialState, reducer)

Вот наши функции mapStateToProps и mapDispatchToProps:

const mapStateToProps = (state, ownProps) => ({
  num: state.count
})
const mapDispatchToProps = (dispatch, ownProps) => ({
  increment: () => dispatch({ type: 'INCREMENT' }),
  increment4: () => dispatch({ type: 'INCREMENT', value: 4 }),
  decrement: () => dispatch({ type: 'DECREMENT' })
})

И наш компонент приложения, который мы обернем connect(mapStateToProps, mapDispatchToProps):

const App = ({ title, increment, increment4, decrement, num }) => (
  <div>
    <h2>{title}</h2>
    <p>Count : {num}</p>
    <button onClick={increment}>+1</button>
    <button onClick={decrement}>-1</button>
    <button onClick={increment4}>+4</button>
  </div>
)
const CounterApp = connect(mapStateToProps, mapDispatchToProps)(App)

Давайте загрузим все с помощью компонента Provider, который мы создали ранее.

ReactDOM.render(
  <Provider store={store}>
    <CounterApp title="Counter" />
  </Provider>,
  document.getElementById('root')
)

Как видите, мы используем его почти точно так же, как исходный redux!

(Я сказал почти, потому что я не создавал combineReducers функцию и не разбивал хранилище на дополнительные хранилища, как это делает redux с combineReducers - возможно, я обновлю эту статью, если сделаю это )

Конечный результат доступен здесь 😎:



Спасибо, что прочитали эту статью!

Понравилось? Если да, нажмите и удерживайте (😅) эту кнопку «хлопать» 👏 !

Первоначально опубликовано на моем сайте sparkyspace.com (зайдите сюда, если вы предпочитаете читать фрагменты кода с выделением синтаксиса 👍)