Dependency inception


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


DI как основа для построения фреймворка, применительно к вебу, довольно молодой подход. Что бы тут было понятней о чем идет речь, начну c привычных для react-разработчиков вещей.


От контекстов к DI


Уверен, что многие использовали контексты при работе с react. Если не напрямую, то наверняка через connect в redux или inject в mobx-react. Суть в том, что в одном компоненте (MessageList) мы объявляем нечто в контексте, а в другом (Button) — говорим, что хотим получить это нечто из контекста.


const PropTypes = require('prop-types');

const Button = ({children}, context) =>
  <button style={{background: context.color}}>
    {children}
  </button>;

Button.contextTypes = {color: PropTypes.string};

class MessageList extends React.Component {
  getChildContext() {
    return {color: "purple"};
  }

  render() {
    return <Button>Ok</Button>;
  }
}

Т.е. один раз в родительском компоненте задается context.color, а далее он автоматически пробрасывается любым нижележащим компонентам, в которых через contextTypes объявлена зависимость от color. Таким образом Button можно кастомизировать без прокидывания свойств по иерархии. Причем на любом уровне иерархии можно через getChildContext() ... переопределить color для всех дочерних компонент.


Такой подход лучше изолирует компоненты друг от друга, упрощая их настройку и переиспользование. В примере выше, достаточно в родительском компоненте определить color и все кнопки поменяют цвет. Причем компонент Button может быть в другой библиотеке, которую при этом не надо рефакторить.


Однако, для реакта, в виду недостаточной продуманности, этот подход пока развит слабо. Напрямую его использовать разработчики не рекомендуют:


It is an experimental API and it is likely to break in future releases of React

написано в документации. Оно experimental в текущем виде уже достаточно давно и ощущение, что разработка зашла в тупик. Контексты в компонентах сцеплены с инфраструктурой (getChildContext), псевдотипизацией через PropTypes и больше похожи на service locator, который некоторые считают антипаттерном. Роль контекстов, на мой взгляд, недооценена и в реакте второстепенная: локализация и темизация, а также биндинги к библиотекам вроде redux и mobx.


В других фреймворках подобные инструменты развиты лучше. Например, в vue: provide/inject, а в angular, его angular di это уже полноценный навороченный DI с поддержкой типов typescript. По-сути, начиная с Angular второй версии, разработчики попытались переосмыслить опыт бэкенда (где DI уже давно существует) применительно к фронтенду. А что если попытаться развить аналогичную идею для реакта и его клонов, какие проблемы бы решились?


Прибивать или нет, вот в чем вопрос


В полноценном реакт/redux-приложении не всё делают через redux-экшены. Состояние какой-нибудь малозначительной галочки удобнее реализовать через setState. Получается — через redux громоздко, а через setState не универсально, но проще, т.к. он всегда под рукой. Статья You Might Not Need Redux известного автора, как бы говорит "если вам не нужно масштабирование — не используйте redux", подтверждая эту двойственность. Проблема в том, что это сейчас не нужно, а завтра может быть понадобится прикрутить логирование состояния галочки.


В другой статье того же автора, Presentational and Container Components, говорится примерно, что "Все компоненты равны (Presentational), но некоторые равнее (Container)" и при этом высечены в граните (прибиты к redux, mobx, relay, setState). Кастомизация Container компонента усложняется — не предполагается его переиспользовать, он уже прибит к реализации состояния и контексту.


Что бы как-то упростить создание Container-компонент, придумали HOC, но по-сути мало что поменялось. Просто чистый компонент стали комбинировать через connect/inject с чем-то вроде redux, mobx, relay. А полученный монолитный Container использовать в коде.


Иными словами, говорим Presentational и Container, а подразумеваем — переиспользуемый и непереиспользуемый. Первый удобно кастомизировать т.к. все точки расширения в свойствах, а второй — рефакторить, т.к свойств меньше, за счет его прибитости к стейту и некоторой логике. Это — некий компромисс по решению двух противоположных проблем, плата за который — разделение компонент на два типа и жертвование принципом открытости/закрытости.


Например, как в статье Заменяй и властвуй — подход SOLID, где предлагается делать большинство компонент максимально простыми, ухудшая их целостность. Но, сложные компоненты из простых все-равно надо будет где-то собирать и при этом остается вопрос как их кастомизировать. Т.е. проблема переносится на другой уровень.


<ModalBackdrop onClick={() => this.setState({ dialogOpen: false })} />
    <ModalDialog open={this.state.dialogOpen} >
        <ModalDialogBox>
            <ModalDialogHeaderBox>
                <ModalDialogCloseButton onClick={() => this.setState({ dialogOpen: false })} />
                <ModalDialogHeader>Dialog header</ModalDialogHeader>
            </ModalDialogHeaderBox>
            <ModalDialogContent>Some content</ModalDialogContent>
            <ModalDialogButtonPanel>
                <Button onClick={() => this.setState({ dialogOpen: false })} key="cancel">
                    {resources.Navigator_ButtonClose}
                </Button>
                <Button disabled={!this.state.directoryDialogSelectedValue}
                    onClick={this.onDirectoryDialogSelectButtonClick} key="ok">
                    {resources.Navigator_ButtonSelect}
                </Button>
            </ModalDialogButtonPanel>
        </ModalDialogBox>
    </ModalDialog>
</ModalBackdrop>

Если все же условиться, что эти оконечные компоненты мы не кастомизируем, то в реальности получается большое кол-во шаблонного кода, когда ради замены одной Button, весь компонент строится заново. Полноценный SOLID при таком подходе невозможен. Всегда будут компоненты-биндинги к стейту, которые нельзя расширить без модификации и компоненты-шаблоны без логики внутри и этим сложные в использовании.


Прототип


Развивая идею внедрения зависимостей, можно решить некоторые из этих проблем. Разберем решение на основе вот такого примера:


// @flow

// @jsx lom_h

// setup...

class HelloService {
    @mem name = ''
}

function HelloView(props: {greet: string}, service: HelloService) {
    return <div>
        {props.greet}, {service.name}
        <br/><input value={service.name} onChange={(e) => { service.name = e.target.value }} />
    </div>
}
// HelloView.deps = [HelloService]

ReactDOM.render(<HelloView greet="Hello"/>, document.getElementById('mount'))

fiddle


Здесь есть одна универсальная форма компонента в виде функции, независимо от того, работает он с состоянием или нет. Контексты используют типы. Из них автоматически генерируются описания зависимостей с помощью babel-plugin-transform-metadata. Аналогично typescript, который это делает, правда, только для классов. Хотя можно описывать аргументы и вручную: HelloView.deps = [HelloService]


Lifecycle


А как же быть с жизненным циклом компонента? А так ли нужна низкоуровневая работа с ним в коде? Посредством HOC как раз пытаются убрать эти lifecycle methods из основного кода, например, как в relay/graphql.


Идея в том, что актуализация данных — это не ответственность компонента. Если у вас загрузка данных происходит по факту доступа к этим данным (например, используется lazyObservable из mobx-utils), то componentDidMount в этом случае не нужен. Если надо прикрутить jquery-плагин, то есть свойство refs в элементе и т.д.


Предположим, что универсальный компонент, свободный от vendor lock-in реакта, теперь есть. Пусть, мы даже выделили его в отдельную библиотеку. Осталось решить, как расширять и настраивать то, что приходит в контекст. Ведь HelloService — это некая реализация по-умолчанию.


Поди туда — не знаю куда, принеси то — не знаю что


Что если компоненты, в силу частых изменений требований, та часть приложения, где инкапсуляция начинает мешать. Не сама по себе конечно, а в том виде, как она сегодня реализована практически во всех фреймворках: в виде шаблона, композиции функций или JSX.


Представим на секунду, что в случае любого компонента нельзя заранее сказать, что у него будет кастомизироваться. И нужен способ менять любую внутреннюю часть компонента без рефакторинга (принцип открытости/закрытости), при этом не ухудшая его читабельности, не усложняя его исходную реализацию и не вкладываясь в переиспользуемость изначально (нельзя все предвидеть).


Например, без DI можно проектировать, подразумевая кастомизацию через наследование. Т.е. дробить содержимое на мелкие методы, теряя при этом в наглядности и иерархии. О минусах этого подхода пишет автор в статье Идеальный UI фреймворк:


class MyPanel extends React.Component {
    header() { return <div class="my-panel-header">{this.props.head}</div> }
    bodier() { return <div class="my-panel-bodier">{this.props.body}</div> }
    childs() { return [ this.header() , this.bodier() ] }
    render() { return <div class="my-panel">{this.childs()}</div>
}

class MyPanelExt extends MyPanel {
    footer() { return <div class="my-panel-footer">{this.props.foot}</div> }
    childs() { return [ this.header() , this.bodier() , this.footer() ] }
}

Надо сказать, что этот автор (@vintage), придумал формат tree, который позволяет описать вышеприведенный пример с сохранением иерархии. Несмотря на то, что многие критикуют этот формат, у него есть преимущество как раз в виде переопределяемости даже самых мелких деталей без специального разбиения на части и рефакторинга. Иными словами, это бесплатная (почти, кроме постижения новой необычной концепции) буковка O в SOLID.


Полностью перенести этот принцип на JSX невозможно, однако частично реализовать его через DI можно попытаться. Смысл в том, что любой компонент в иерархии — это еще и точка расширения, слот, если рассуждать в терминах vue. И мы в родительском компоненте можем поменять его реализацию, зная его идентификатор (исходная реализация или интерфейс). Примерно так работают многие контейнеры зависимостей, позволяя ассоциировать реализации с интерфейсами.


В js/ts, в runtime, без усложнений или внедрения строковых ключей, ухудшающих безопасность кода, нельзя ссылаться на интерфейс. Поэтому следующий пример не заработает в flow или typescript (но аналогичный заработает в C# или Dart):


interface ISome {}

class MySome implements ISome {}

const map = new Map()
map.set(ISome, MySome)

Однако, можно ссылаться на абстрактный класс или функцию.


class AbstractSome {}

class MySome extends AbstractSome {}

const map = new Map()
map.set(AbstractSome, MySome)

Т.к. создание объектов и компонент происходит внутри DI-контейнера, а там внутри может быть подобный map, то любую реализацию можно переопределить. А т.к. компоненты, кроме самых примитивных — функции, то их можно подменять на функции с таким же интерфейсом, но с другой реализацией.


Например, TodoResetButtonView является частью TodoView. Требуется переопределить TodoResetButtonView на кастомную реализацию.


function TodoResetButtonView({onClick}) {
  return <button onClick={onClick}>reset</button>
}

function TodoView({todo, desc, reset}) {
    return <li>
        <input
            type="checkbox"
            checked={todo.finished}
            onClick={() => todo.finished = !todo.finished}
        />{todo.title} #{todo.id} ({desc.title})
        <TodoResetButtonView>reset</TodoResetButtonView>
    </li>
}

Предположим у нас нет возможности править TodoView (он в другой библиотеке и мы не хотим его трогать, нарушая open/close принцип и заново тестировать 11 других проектов, которые его использовали со старой кнопкой).


Поэтому создаем новую кнопку и клонируем существующий TodoView, заменяя ее в клоне. Это наследование, только наглядность не нарушается — остается иерархия и не нужно специально заранее проектировать TodoView так, что бы можно было заменить кнопку.


function ClonedTodoResetButtonView({onClick}) {
  return <button onClick={onClick}>cloned reset</button>
}

const ClonedTodoView = cloneComponent(TodoView, [
    [TodoResetButtonView, ClonedTodoResetButtonView]
], 'ClonedTodoView')

const ClonedTodoListView = cloneComponent(TodoListView, [
    [TodoView, ClonedTodoView]
], 'ClonedTodoListView')

ReactDOM.render(<ClonedTodoListView todoList={store} />, document.getElementById('mount'));

fiddle


Переопределять иногда надо не только компоненты, но и их зависимости:


class AbstractHelloService {
    name: string
}

function HelloView(props: {greet: string}, service: AbstractHelloService) {
    return <div>
        {props.greet}, {service.name}
        <br/><input value={service.name} onChange={(e) => { service.name = e.target.value }} />
    </div>
}

class AppHelloService {
    @mem name = 'Jonny'
}

function AppView() {
  return <HelloView greet="Hello"/>
}
AppView.aliases = [
    [AbstractHelloService, AppHelloService]
]

fiddle


HelloView получит экземпляр класса AppHelloService. Т.к. AppView.aliases для всех дочерних компонент переопределяет AbstractHelloService.


Есть конечно и минус подхода "все кастомизируется" через наследование. Т.к. фреймворк предоставляет больше точек расширения, то, больше ответственности по кастомизации перекладывается на того, кто использует компонент, а не проектирует его. Переопределяя части компонента "таблица", без осознания смысла, можно ненароком превратить его в "список", а это плохой признак, т.к. является искажением исходного смысла (нарушается LSP принцип).


Разделение состояния


По-умолчанию, состояние в зависимостях компонента будет выделено под каждый компонент. Однако действует общий принцип: все определяемое в компонентах выше — имеет приоритет над нижележащими зависимостями. Т.е. если зависимость впервые используется в родительском компоненте, то она будет жить вместе с ним и все нижележащие компоненты, ее запросившие, получат именно родительский экземпляр.


class HelloService {
    @mem name = 'John'
}

function HelloView(props: {greet: string}, service: HelloService) {
    return <div>
        {props.greet}, {service.name}
        <br/><input value={service.name} onChange={(e) => { service.name = e.target.value }} />
    </div>
}

class AppHelloService {
    @mem name = 'Jonny'
}

function AppView(_, service: HelloService) {
    return <div>
        <HelloView greet="Hello"/>
        <HelloView greet="Hi"/>
    </div>
}

fiddle


В такой конфигурации, оба HelloView разделяют общий экземпляр HelloService. Однако, без HelloService в AppView, на каждый дочерний компонент будет свой экземпляр.


function AppView() {
    return <div>
        <HelloView greet="Hello"/>
        <HelloView greet="Hi"/>
    </div>
}

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


Стили


Я не утверждаю, что подход css-in-js единственно правильный для использования в веб. Но и тут можно применить идею внедрения зависимостей. Проблема аналогична вышеописанной с redux/mobx и контекстами. Например, как и во многих подобных библиотеках, стили jss прибиваются к компоненту через обертку injectSheet и компонент связывается с конкретной реализацией стилей, с react-jss:


import React from 'react'
import injectSheet from 'react-jss'

const styles = {
  button: {
    background: props => props.color
  },
  label: {
    fontWeight: 'bold'
  }
}

const Button = ({classes, children}) => (
  <button className={classes.button}>
    <span className={classes.label}>
      {children}
    </span>
  </button>
)

export default injectSheet(styles)(Button)

Однако, эту прямую зависимость от jss и ему подобных можно убрать, перенеся эту ответственность на DI. В коде приложения достаточно определить функцию со стилями, как зависимость компонента и пометить ее соответственно.


// ... setup
import {action, props, mem} from 'lom_atom'
import type {NamesOf} from 'lom_atom'

class Store {
  @mem red = 140
}

function HelloTheme(store: Store) {
  return {
    wrapper: {
      background: `rgb(${store.red}, 0, 0)`
    }
  }
}
HelloTheme.theme = true

function HelloView(
  _,
  {store, theme}: {
    store: Store,
    theme: NameOf<typeof HelloTheme>
  }
) {
  return <div className={theme.wrapper}>
    color via css {store.red}: <input
      type="range"
      min="0"
      max="255"
      value={store.red}
      onInput={({target}) => { store.red = Number(target.value) }}
    />
  </div>
}

fiddle


Такой подход для стилей обладает всеми преимуществами DI, таким образом обеспечивается темизация и реактивность. В отличие от переменных в css, здесь работают типы в flow/ts. Из минусов — накладные расходы на генерацию и обновление css.


Итог


В попытке адаптировать идею внедрения зависимостей для компонентов, получилась библиотека reactive-di. Простые примеры в статье постронены на ее основе, но есть и более сложные, с загрузкой, обработкой статусов загрузки, ошибок и т.д. Есть todomvc бенчмарк для react, preact, inferno. В котором можно оценить оверхед от использования reactive-di. Правда, на 100 todos, погрешность измерений у меня была больше, чем этот оверхед.


Получился упрощенный Angular. Однако есть ряд особенностей, reactive-di


  1. Умеет интегрироваться с реактом и его клонами, оставаясь совместимым с legacy-компонентами на чистом реакте
  2. Позволяет писать на одних чистых компонентах, не оборачивая их в mobx/observe или подобное
  3. Хорошо работает с типами в flowtype не только для классов, но и для компонент-функций
  4. Ненавязчив: не требуется кучи декораторов в коде, компоненты абстрагируются от react, его можно поменять на свою реализацию, не затрагивая основной код
  5. Настраивается просто, не нужно региситрации зависимостей, provide/inject-подобных конструкций
  6. Позволяет доопределять содержимое компонента без его модификации, c сохранением иерархии его внутренностей
  7. Позволяет ненавязчиво, через интерфейсы, интегрировать css-in-js решения в компоненты

Почему до сих пор идею контекстов не развивали в этом ключе? Скорее всего непопулярность DI на фронтенде объясняется не повсеместным господством flow/ts и отсутствием стандартной поддержки интерфейсов на уровне метаданных. Попытками скопировать сложные реализации с других backend-ориентированных языков (как InversifyJS клон Ninject из C#) без глубокого переосмысления. А также пока недостаточным акцентом: например, некоторое подобие DI есть в react и vue, но там эти реализации являются неотделимой частью фреймворка и роль их второстепенная.


Хороший DI — это еще половина решения. В примерах выше часто мелькал декоратор @mem, который необходим для управления состоянием, построенном на идее ОРП. С помощью mem можно писать код в псевдосинхронном стиле, с простой, по-сравнению с mobx, обработкой ошибок и статусов загрузки. Про него я расскажу в следующей статье.

Комментарии (25)


  1. Veikedo
    25.09.2017 14:56

    Посмотрите WPF с его точками расширения/слотами/темплейтами и DataContext'ами.
    Вот там можно переопределить всё и даже довольно удобно.


    1. redyuf Автор
      25.09.2017 15:13
      +1

      WPF все-таки другая экосистема, мне была важна поддержка react, flowtype. Поэтому где-то конструкции приходилось упрощать или жертвовать гибкостью. Главное проверить, что подход работает и более-менее просто выглядит.


      1. andreyverbin
        26.09.2017 08:22

        Все же посмотрите на WPF, все ваши вопросы там уже решены с помощью DataTemplate. Получается значительно проще и чище.


        1. redyuf Автор
          26.09.2017 08:24

          А пример можно? Глянул бы.


    1. AxisPod
      26.09.2017 07:13

      WPF тут к чему? Это если наложить возможности WPF на оригинальный HTML, но никто на это не пойдет, порог вхождения станет слишком высок, и модные пацанчики тупо не осилят. А к React это никак и не прицепить, увы, инфраструктура не та совсем.


      1. Veikedo
        26.09.2017 07:54

        Я имею ввиду посмотреть саму идею расширяемости wpf; понятно, что это не перетащишь в веб просто так (хотя попытки были http://bridge.net/ http://www.cshtml5.com )
        У меня просто стойкое ощущение, что всё это было там уже довольно давно.


  1. Finesse
    26.09.2017 02:40

    Сложно читать статью из-за нестандартного синтаксиса JavaScript. Какие babel-transform нужно использовать для того, чтобы преобразовать код из примеров в ES6+JSX?


    1. redyuf Автор
      26.09.2017 08:21

      Вы какой-то конкретный пример имели в виду? Вроде вполне стандартный js с flow-типами и JSX.


      1. Finesse
        26.09.2017 13:43
        -1

        Примеры 3, 10, 11 и 14. Ничего из перечисленного не встречал даже в будущих версиях стандарта ECMAScript:


        • Конструкция @mem
        • Объявление параметров объекта в теле класса
        • Типы аргументов функций


        1. redyuf Автор
          26.09.2017 16:05
          +1

          • mem — декоратор, если имеется в виду реализация, то это отдельная библиотека, о ней я планировал в другой статье рассказать.
          • Параметры: github.com/tc39/proposal-class-fields
          • Типы не стандарт ES, но поверх наслаивается: flowtype, typescript.


          1. Finesse
            27.09.2017 06:17

            Спасибо!


  1. AlexSkvr
    26.09.2017 08:22

    Столкнулся с похожей проблемой, когда имеется несколько крупных Container-ов, бизнес хочет с десяток копий с разным поведением, но рука не поднимается дублировать столько кода и ставить крест на проекте.

    DI удобная вещь, но если используется redux подход используемый в redux-form мне кажется более логичным. Идея в том что Container, обернутый в connect, вместе со стилями css-in-js, prop-types и селекторами оборачивается еще одним HOC вида createMyAwesomeComponent. В итоге получается функция-конструктор, формирующая с помощью параметров произвольное поведение самого Container и его дочерних Presentationals.

    В такой конструктор можно на вход подать кастомные селекторы, которые прогонят через требуемую логику данные из store. Можно в виде параметров передавать мелкие кастомизированные компоненты, чтобы не перегружать сигнатуру конструктора. В store для каждого инстанса Container создается свой state, и соответственно в экшенах в разделе meta указывается имя инстанса.

    Если нужна кастомизация side effects, например тулбар с другими кнопками, то проще для тулбара создать отдельный Container и передать его в качестве параметра в основной Container.


    1. redyuf Автор
      26.09.2017 08:39

      DI мне интересно развивать, как фундамент для связывания частей приложения, без привязки к каким либо фреймворкам. Еще была важна ненавязчивость, в коде нету import some from 'reactive-di' (кроме cloneComponent), только чистые функции + метаданные.

      Redux немного другие задачи решает, навязывает определенный стиль программирования (экшены, редьюсеры и т.д.) не избавляет от HOC и этого connect в коде. Если нужны транзакции, версионирование и т.д., то решения вроде redux можно накрутить и поверх DI. Например, mobx-state-tree — это как раз пример того, как выглядел бы redux поверх mobx.


  1. vasIvas
    26.09.2017 11:38

    В TypeScript строковые литералы не считаются нетипобезопасными, так как есть строковые литеральные типы.
    Redux предназначен не для хранения состояния компонентов. Redux это слой между хранилищем данных и react. Плюс он играет роль менеджера состояния ПРИЛОЖЕНИЯ. Но нужно помнить, что определять в нем состояние приложения стоит только в том случае, если эта логика таковой является.
    Без lifecycle очень плохо. наоборот, чем больше lifecycle тем было бы круче. представьте что помимо constructor в js был бы ещё destructor, update, write, read. Жизнь стала бы сказкой.

    Я тоже пробовал внедрить DI в react и так как пишу на typescript, выбран был InversifyJS.
    Затем использовал mobx. Затем перешел на angular 4 и просто нарадоваться не могу. Все о чем можно мечтать уже сделано…


    1. redyuf Автор
      26.09.2017 15:48
      +1

      В TypeScript строковые литералы не считаются нетипобезопасными, так как есть строковые литеральные типы.
      В ts да, такая штука будет типобезопасной:
      import { InjectionToken } from '@angular/core';
      interface AppConfig {
        title: string;
      }
      export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');
      //...
      constructor(@Inject(APP_CONFIG) config: AppConfig) {
        this.title = config.title;
      }

      Однако такой подход все-равно требует расстановки декораторов, это не нативно из-за InjectionToken. Второй момент — заранее надо продумывать точки расширения, расставляя эти декораторы.

      Redux предназначен не для хранения состояния компонентов.
      Redux лучше впишется туда, где используются транзакции, timetravel, распределенная работа с данными. Если эти навороты не нужны, то есть более простые способы менять состояние, поэтому многие перешли на mobx.
      Задачу связывания redux решает не очень хорошо: redux-thunk, reselect, connect — низкоуровневые инструменты. Если основа DI, можно все связывание построить на типах, не привнося фреймворко-специфичных конструкций.
      Сравните примеры Hello world, кода сильно меньше при сравнимом уровне абстракции:
      На redux
      import { connect } from 'react-redux'
      import React, { PropTypes } from 'react'
      
      const HELLO_WORLD = 'HELLO_WORLD'
      
      const helloWorld = (state = { message: 'Hello' }, action) => {
        switch (action.type) {
          case HELLO_WORLD:
            return Object.assign({}, state, { message: 'Hello, World!' })
          default:
            return state
        }
      }
      
      const Hello = ({ onClick, message }) => {
        return (
          <div>
            <h1>{ message }</h1>
            <button onClick={onClick}>Click</button>
          </div>
        )
      }
      
      const mapStateToProps = (state, ownProps) => {
        return {
          message: state.helloWorld.message
        }
      }
      
      const mapDispatchToProps = (dispatch, ownProps) => {
        return {
          onClick: () => {
            dispatch({ type: HELLO_WORLD })
          }
        }
      }
      
      const HelloWorld = connect(
        mapStateToProps,
        mapDispatchToProps
      )(Hello)
      


    1. VolCh
      26.09.2017 21:02

      Redux вполне может использоваться без React. А состояние приложения есть суперпозиция состояний всех его компонентов. Redux в пределе стремится к тому, чтобы у компонентов своего внутреннего состояния не было и всё состояние приложения хранилось в одном-единственном хранилище под управлением Redux. Считать это хранилище частью Redux или чем-то отдельным — вопрос теоретически спорный, но на практике, по-моему, нет смысла не считать.


  1. SunDeath
    26.09.2017 16:07
    +1

    Очень тяжело читать статью.
    Я вот с react-redux регулярно работаю и имею представление о SOLID-принципах и сложностях проектирования, но почти ничего не понял.
    Какие-то отдельные часть — возможно, но сути я не уловил.
    Как автор предлагает уменьшать сложность за счёт использования DI — ускользнуло от меня.


    1. redyuf Автор
      26.09.2017 16:20
      +1

      Это первая моя статья, буду пытаться улучшать подачу в следующих. Хотя бы пример с HelloWorld понятен? Могу для сравнения еще насоздавать, только задачу сформулируйте.

      Суть такая примерно:

      • Развить идею контекста в реакте
      • Привнести SOLID на фронтенд наименьшей ценой
      • Дать возможность писать код, максимально свободный от каркаса, убрав vendor lock-in
      • Использовать типы и DI для связывания частей, вместо решений вроде connect, redux-thunk, reselect и т.д.

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


      1. SunDeath
        26.09.2017 16:45

        Спасибо за разъяснение, кажется, понял.
        Постараюсь более вдумчиво перечитать.


      1. vintage
        28.09.2017 09:25

        Думаю тут не хватает наглядного примера с картинками. Например — мини-профиль пользователя на хабре. Он появляется в 3 разных местах: при наведении на аватарку, под статьёй пользователя, на странице профиля пользователя. Каждый мини-профиль позволяет выполнить определённые действия: изменить карму, подписаться, написать сообщение. Но в зависимости от расположения имеет свои особенности: при наведении на аватар добавляется панелька со статистикой, раскладка элементов разная. Но это текущее состояние, после множества рефакторингов. А если откатиться к самому началу, когда этой кучи требований ещё не было, то как запроектировать "мини-профиль пользователя", чтобы его можно было переиспользовать в разных (пока не известных) местах не переписывая постоянно и не утопая в копипасте?


        1. redyuf Автор
          28.09.2017 11:35

          А что, по-вашему, должно быть в базовой версии, а какие возможности добавлять при ее расширении?


          1. vintage
            28.09.2017 12:14

            На мой взгляд компонент должен быть самодостаточным и вставляться одной строчкой кода. Но если надо добавить ему панельку в одном конкретном месте использования — должна быть возможность эту самую панельку добавить, не изменяя все места использования компонента, а желательно и самого компонента.


            1. redyuf Автор
              28.09.2017 14:10

              Удалить и заменить деталь внутри можно, а вот что бы добавить, надо делать композицию заново (правда, в отличие от чистого реакта, в rdi это можно проделать не для основного компонента, а для его внутренней детали). У вас, в tree, такая перекомпозиция происходит дешево в sub.

              Пока не представляю как в jsx это также дешево сделать. Можно выделять детали на первом уровне в компоненты и делая компонент, который выстроит последовательность из этих деталей.


              1. vintage
                28.09.2017 16:39

                Ну, разместить базовом компоненте плейсхолдер, который по умолчанию ничего не выводит, а через DI заменять его на что-то своё — вполне себе решение.


                1. redyuf Автор
                  28.09.2017 17:35

                  Спасибо за идею, накидал такой пример. Позже напишу статью с его разбором.