Translate

Поиск по этому блогу

среда, 26 декабря 2018 г.

Redux - простое руководство.

Пришло время разобраться с Redux и написать простое приложение с его использованием.




Посмотреть готовое приложение можно на хосте.

Я не буду долго рассказывать зачем он нужен и как он работает. В сети есть масса информации.

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

Для простоты доступа к данным и работы с ними, мы будем использовать Redux.



Стоит напомнить, что Redux использует Flux архитектуру (не путайте с Flux-библиотекой, которая тоже использует такую архитектуру). Flux предполагает однонаправленный поток данных - One-way binding

Для простоты понимания этого, стоит посмотреть на картинку <<Схема>>:



Справа, квадратик у нас имеет подписью View. Это и есть те компоненты, которые вы непосредственно создаете в React и которые в перспективе будут отражать дрянные на экране монитора. В браузере эти компоненты получают данные из Store.

Store - это хранилище и именно так его нужно понимать в данном контексте. По своей сути Store - это обыкновенный объект, который и определяет, как у нас будет выглядеть наши компоненты.

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

Далее, у нас идёт Dispatcher. Dispatcher это центральный узел, который обрабатывает все наши потоки данных. То есть - всё проходит через него! А конкретно, через него проходит те данные, которые определяют как раз-таки action.

Значит есть один action (слева на картинке). Этот Action у нас стоит самого начала и считаете это теми данными которые идут по умолчанию. То есть, по умолчанию поступает какие-то данные в Action говорит диспетчеру - "Покажи мне Username такой-то и Lastname такой-то!". И диспетчер заносит изменения в Store, откуда они подтягиваются View (React -components) и мы видим картинку на экране.

В тоже время, если мы хотим поменять нашу визуальную картинку со стороны пользователя, мы так же создаем новый Action. Новый Action, который опять обращается к диспетчеру и делает тоже самое. Говорит диспетчеру "Обнови username Коля на username Вася" и он его обновляет. Это определяет уже наш View.

Вот в этом собственное заключается вся суть архитектуры Flux и самого принципа One Way binding, то есть однонаправленного потока данных. Если вы заметили, то у нас поток данных идёт всегда в одну сторону!

View не может непосредственно повлиять на что-то, что есть в нашим хранилище! Это главное отличие его от так называемого Two-way binding, например, который использует ангуляр.

Действия загрузки данных.

Передача данных.

Для простоты, мы будем использовать готовое решение create-react-app. Как его установить я рассказывал в своих постах о React Переходим в папку нашего приложения и устанавливаем два дополнительных модуля:
  1. redux
  2. react-redux
Эти модули позволят нам использовать весь функционал Redux и просто совместить его с React.

npm install redux react-redux


--save-dev команду можно не писать.

После установки, мы можем открыть наш проект в редакторе.

Выбираем файл index.js в папке src - это точка входа в наше приложение.

Импортируем компонент Provider из модуля react-redux

import { Provider } from "react-redux";

Данный компонент является оберткой для нашего приложения и позволяет получать компонентам свойства из Store, поэтому следующим шагом нам нужно обернуть компонент App в Provider.


ReactDOM.render(<Provider>
                    <App />
                </Provider>, document.getElementById('root'));


Следующее, что нам нужно сделать, это импортировать функцию, которая и создает Store.

import { createStore } from 'redux';

Для создания хранилища - Store напишем:

const store = createStore(()=>{});

В качестве параметра эта функция должна принимать reducer. Сам по себе редусер - это просто функция, поэтому мы для начала, можем записать вместо него , пустую функцию, например стрелочную - ()=>{}.

Для соединения провайдера с хранилищем Store, мы добавим Provider атрибут store, со значением store.


ReactDOM.render(<Provider store={store}>
                    <App />
                </Provider>, document.getElementById('root'));


Таким образом, мы создали Store, обернули его в провайдер и сделали данные доступные для наших компонентов.

Создание Reducers.

В папке src создадим папку reducers и в ней два файла index.js и info.js.

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

Сам термин пришел к нам из JS Array.prototype.reduce().

Вся суть редусера - это взять предыдущее, значение, плюс текущее значение и в конце - вернуть новое значение.

То есть, мы берем предыдущее состояние State, то что нам нужно поменять, соединяем это и получаем что-то новое, что в последствии у нас отрендерится на экране.

Создадим первый редусер в файле info.js


// имитация базы данных
const initialState = {
    user: "unknown user"
}
//  сам редусер 
export default function userInfo (state = initialState) {
    return state
}


Функция принимает в качестве стандартного значения - initialState и возвращает новый state на основании старого.

Проще говоря редусер принимает один state, а возвращает новый state.

Файл index.js в папке reducers, нам потребуется для того, чтобы все редусеры держать в одном месте, но в разных файлах. В качестве "родительского" редусера мы будем использовать index.js. В этом нам поможет, то что уже реализовано в библиотеке Redux - combineReducers


import { combineReducers } from "redux"
import userInfo from "./info"

const rootReducer = combineReducers({
  userInfo
})

export default rootReducer;



Теперь, самое время вернуться в файл index.js в корне проекта, и заменить анонимную, пустую функцию на rootReducer. И добавим импорт редусеров в наш файл.


import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import { Provider } from "react-redux";
import { createStore } from "redux";
import rootReducer from "./reducers/index";

const store = createStore(rootReducer);

ReactDOM.render(<Provider store={store}>
                    <App />
                </Provider>, document.getElementById('root'));
serviceWorker.unregister();



Таким образом наш Store, будет знать, что когда мы к нему обращаемся, то он будет вызывать все редусеры, которые у нас есть.

Store по своей сути, это просто обычный объект, который содержит State для всего нашего приложения. То есть всё, что у нас может изменяться, обо всём этом должен знать State и он это регулирует.

Фактически, мы сейчас с помощью нашей функции в редусере ( function userInfo ), занесли в наш State данных переменной user со значением "unknown user". Теперь осталась нам подключить наш компонент к этому Store и взять с него то что принадлежит нам по праву, а именно наши свойства.

App.js


import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

import { connect } from "react-redux"

class App extends Component {
  render() {
    return (
          <h2>Welcome to React</h2>
    );
  }
}


export default connect()(App);



Чтобы подключить Redux к компоненту есть специальная функция - connect

Подключаем ее:

import { connect } from "react-redux";

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

export default connect()(App);

Если сейчас запустит и посмотреть приложение:



Здесь можно видеть, что функция connect создает дополнительный компонент над компонентом App.

Передача свойств приложению.

Для передачи свойств приложению:

App.js

import React, { Component } from 'react';

import './App.css';

import { connect } from "react-redux"

class App extends Component {
  render() {
    return (
          <h2>Welcome to React {this.props.user}</h2>
    );
  }
}

function mapStateToProps(state) {
  return {
    user: state.userInfo.user
  }
}

export default connect(mapStateToProps)(App);



Создаем дополнительную функцию mapStateToProps. Название произвольное, но оно рекомендуется, чтобы другие разработчики понимали то, что делает эта функция. Эта функция будет передавать наши state в свойства. Функция принимает наш state, а возвращать будет некую переменную ( для логичности - user), а в качестве значения возьмем те данные, которые указали на данный момент в редюсере.

Чтобы обратится к этим данным, нам нужно обратиться к нашему state, с которого мы подтягиваем данные. Дaлее к userinfo из rootReducer и уже используя userinfo мы попадаем в наш изначальный редусер и получаем доступ к значению переменной user в файле info.js.

user: state.userInfo.user

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



Таким образом мы с помощью функции передали в компонент App свойство user со значением unknown user

Мы смогли передать те значения, которые мы изначально несли в нашу "Базу данных" - const initialState.

Вывести свойства на экран нам легко - {this.props.user}

Практически, мы получили тоже самое, как если бы передали свойство user={"unknown user"} непосредственно в компонент реакт.

Псомотреть весь код приложения, можно в репо - redux-simple-abc ci -m"Simple data transfer with Redux"

Действия пользователя.

Действия - Actions

Все что мы делали ранее не относилось к действиям пользователя. Если следовать второй картинке этого поста, то мы разбирали работу Action, который слева на картинке.

Действия пользователя будут представлены в Action, который на картинке <<Схема>> вверху.

Важно запомнить, что Action определяют что именно нужно поменять, а Reducer - как именно.


Для лучшего понимания работы редусеров, следует вспомнить, как различаются компоненты в React:



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

В папке src создадим папки: containers и components.

В папке components создадим: User.js и Year.js

В файле User.js создадим простой, глупый компонент - Dumb Component:


import React from "react"
export default class User extends React.Component {
  render() {
    return (
      <h2>Welcome to React {this.props.user}</h2>
    );
  }
}



Его render() мы взяли из файла App.js.

В файле Year.js мы создадим несколько кнопок, для возможности выбора пользователем, соответствующего года.


import React from "react"

export default class Year extends React.Component {
  constructor(props) {
    super(props)
    this.onBtnClick = this.onBtnClick.bind(this)
  }

  onBtnClick(event) {
    return this.props.setYear(event.target.textContent)
  }

  render() {
    return <div>
      <button onClick={this.onBtnClick}>1975</button>
      <button onClick={this.onBtnClick}>1991</button>
      <button onClick={this.onBtnClick}>2015</button>
      <p>This year has been chosen - {this.props.year}</p>
    </div>
  }
}



Если коротко, то создали компонент, который будет выводит кнопки. На кнопки навесили событие onClick, которое вызывает функцию onBtnClick. В конструкторе прибиндили эту функцию. Эта функция возвращает функцию setYear(), которая в качестве аргумента принимает значение кнопки (текст кнопки - в данном случае год) - event.target.textContent.

Таким образом мы написали два dumb-компонента.

Теперь нам нужно поменять файл App.js

Поскольку он у нас будет выполнять функцию smart - компонента, мы переместим его в папку src/containers

Так как мы поменяли директорию файла App.js, то в файле нам нужно изменить путь подключения его в файле index.js в корне сайта.

import App from './containers/App';

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



Посмотреть весь код приложения на этом этапе, можно в репо - redux-simple-abc ci -m"divided the application into components"

Теперь нам необходимо в наш smart - компонент App.js добавить наши dump-компоненты - User.js и Year.js


    import React, { Component } from 'react';
    import './App.css';
    import User from '../components/User';
    import Year from '../components/Year';
    import { connect } from "react-redux"

    class App extends Component {
      render() {
        return (
           <div>
            <User />
            <Year />
          </div>
              
        );
      }
    }

    function mapStateToProps(state) {
      return {
        user: state.userInfo.user
      }
    }

    export default connect(mapStateToProps)(App);


Если сейчас посмотреть в браузер, то мы увидим:



Вывели на экран все статические данные, кроме необходимых переменных.

Переменная user не задействована в экшенах и мы ее можем легко отобразить прямо сейчас:

<User user={this.props.user} />



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

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

В папке src создаем папку actions а в ней файл actionYear.js


export default function setYearAction(year) {
    return {
      type: "SET_YEAR",
      payload: year
    }
  }



Экшен по своей сути это обычный объект. Для удобства их создают через специальные функции, которые называются Action Creators. Это упрощает чтение кода и очень удобно для разработки!

Название функции setYearAction - произвольное, но лучше, если оно будет понятным.

Функция обязательно возвращает объект и в нем должно быть обязательно поле - type.

Поле - type, чем то похоже на идентификатора экшена, и значение этого поля всегда строка с заглавными буквами - UPPERCASE.

Вторая переменная payload, будет принимать значение year то, которое мы передаем в эту функцию изначально.

payload - тоже вне гласное соглашение. Обозначает что-то типа "полезной нагрузки".

Более подробно о формате написания экшенов можно почитать в стандартах - Flux Standard Action

Как мы говорили ранее - экшен - ЧТО МЕНЯЕМ... А мы меняем -year, который в аргументе и меняем его экшеном - "SET_YEAR"

Теперь нам нужно понять КАК нам его изменить.

За это у нас отвечает редусер.

Переходим в редусеры src/reducers/info.js и будем его усовершенствовать.

Добавим некий изначальный стейт - например: year: 2015, а в самом редусере добавим еще один аргумент - action

Таким образом редусер будет принимать начальное состояние: state = initialState и то, что должно измениться - action, все это соединяет и возвращает нам новый стейт на основании проделанной работы.

Обычно их указывают в формате switch/case.

В нем обратимся к типу экшена и обработаем различные варианты.

Если у нас тип = "SET_YEAR", то мы возвращаем весь предыдущий стейт ( ...state) и ключ year с новым значением action.payload.

Другими совами: Мы берем наш изначальный стейт :

reducers/info.js

const initialState = {
    user: "unknown user",
    year: 2015
}



Вторым аргументом берем стейт из экшена: actions/actionYear.js
export default function setYearAction(year) {
    return {
      type: "SET_YEAR",
      payload: year
    }
  }



Соединяем их вместе и получаем что-то новое! А конкретно - изменяем поле year на то, которое указано в экшене.

Теперь нашей задачей является - доделать наш "умный компонент", который должен отправлять данные в Store.

Переходим в src/containers/App.js

Первое - импортировать функцию из actionYear.js

import setYearAction from '../actions/actionYear';

Выведем год статически:


    import React, { Component } from 'react';
import './App.css';
import User from '../components/User';
import Year from '../components/Year';
import setYearAction from '../actions/actionYear';
import { connect } from "react-redux";

class App extends Component {
  render() {
    return (
      <div>
        <User user={this.props.user} />
        <Year year={this.props.year}/>
      </div>
          
    );
  }
}

function mapStateToProps(state) {
  return {
    user: state.userInfo.user,
    year: state.userInfo.year
  }
}

export default connect(mapStateToProps)(App);


Посмотрим в браузере:



Таким образом мы вывели "стартовые значения" стейта.

Теперь нам нужно оживить наши кнопки!

Для этого нам нужно передать новые данные диспетчеру, а он их добавит в Store.

Для этого есть специальная функция - mapDispatchToProps(dispatch) .Она принимает аргумент - dispatch и возвращает некий объект.


function mapDispatchToProps(dispatch) {
  return {
    setYearFunction: year => {
      dispatch(setYearAction(year))
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App);



setYearFunction - произвольное название. Мы выбрали его, чтобы просто отличать функцию от других. В качестве ключа в нее мы передаем функцию, которую мы записали в стрелочном формате. Эта функция принимает аргумент year, который передаст наш экшен actionYear и с ним мы в функции dispatch() вызываем экшен креатор. - setYearAction(year) и в качестве аргумента ей передадим тот же year

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

Если посмотреть в браузер, то в панели разработчика мы увидим:



Поскольку мы подключили в наш компоненту новую функцию - mapDispatchToProps она передала в наш компоненте новое свойство - setYearFunction (как мы его и назвали в этой функции)

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

В файле App.js передадим компоненту Year напишем свойство, которому передадим функцию setYearFunction из ретурна mapDispatchToProps

<Year year={this.props.year} setYear={this.props.setYearFunction} />

this.props - пишем, потому что функция передается в качестве свойства.

Теперь становится ясным, что в функции onBtnClick мы как раз и возвращаем это свойство со значением текста кнопки:

Файл: components/Year.js

 onBtnClick(event) {
    return this.props.setYear(event.target.textContent)
  }



Теперь самое время посмтреть наше приложение и проверить, что все работает правильно:



Коротко:

Все начианется в нашем компоненте - Year.Здесь есть кнопки, при нажатии на которые срабатывает функция onBtnClic

Эта функция возвращает нам event.target.textContent - то есть в данном случае это текст кнопки.

Далее эти данные попадают в функцию mapDispatchToProps(dispatch) и в ней уже year будет равен тому, что мы указали в компоненте и функция dispatch(setYearAction(year)) отправляет наш экшен в хранилище -Store с помощью метода connect(mapStateToProps, mapDispatchToProps)(App)

В Store запускается редусер.Он принимает начальный стейт и экшен, который ему прислал mapDispatchToProps(dispatch), срабатывает условие - "SET_YEAR" и year меняет значение на новое - action.payload.

Далее возвращается в приложение функцией mapStateToProps(state) и мы получаем доступ к этим данным.

Посмотреть весь код приложения, можно в репо - redux-simple-abc ci -m"Finish"





                                                                                                                                                             

3 комментария:



Хотите освоить самые современные методы написания React приложений? Надоели простые проекты? Нужны курсы, книги, руководства, индивидуальные занятия по React и не только? Хотите стать разработчиком полного цикла, освоить стек MERN, или вы только начинаете свой путь в программировании, и не знаете с чего начать, то пишите через форму связи, подписывайтесь на мой канал в Телеге, вступайте в группу на Facebook.Пишите мне - kolesnikovy70 почта gmail.com