Redux

Никита Солоснюк

  1. Управление состоянием в Реакт
  2. Флакс
  3. Редакс
    1. Стор
    2. Экшн
    3. Редьюсер
  4. Сайд-эффекты
  5. Реакт + Редакс
    1. Thunk/Saga
  6. Hooks

План лекции

Управление состоянием в React

  • Состояние компонента (Component State) — state
  • Относительное состояние (Relative State) — props
  • Переданное состояние (Provided State)  Context API 
state = {
    issues: issues,
    activeTab: ‘open’,
}
<Issue
    id={issue.id}
    title={issue.title}
/>

Недостатки

  • Многоуровневая передача состояния
  • Подъем состояния через функции-коллбеки
  • Смешение логики и представления

 

Here's Flux

Чё за флакс?

Flux is a pattern for managing data flow in your application. The most important concept is that data flows in one direction.

Чё за флакс? (2)

  • технология
  • фреймворк
  • библиотека
  • паттерн

 

Концепция

Action ⟶ Dispatcher ⟶ Store ⟶ View

Если необходимо изменить состояние приложения — нужно создать действие (action).

 

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


После создания, действие передается диспетчеру (dispatcher).

Action ⟶ Dispatcher ⟶ Store ⟶ View

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

 

Это делается с помощью диспетчера.


Диспетчер обрабатывает все события и отправляет данные в нужное хранилище (store).

Action ⟶ Dispatcher ⟶ Store ⟶ View

Хранилище содержит состояние приложения, и только хранилище знает и умеет изменять состояние.

 

В зависимости от типа действия, хранилище обновляет состояние (действие также содержит новые данные).


После изменения хранилище генерирует событие об изменении. После оно сообщает вашему представлению (view), что состояние изменилось и надо обновиться.

Action ⟶ Dispatcher ⟶ Store ⟶ View

Получает новое состояние и рендерит его.

 

Представление абсолютно ничего не знает про ваше приложение, всё что ему известно, это переданные данные и способ их отрисовки.


Представление также может генерировать новые действия.

Концепция

А при чём тут Редакс?

Flux

Redux

MobX

А при чём тут Редакс?

Redux - реализация паттерна Flux + улучшения

 

Основные принципы:

1. Единственный источник истины

2. Read-only

3. Изменения делаются «чистыми» функциями

 

Cartoon intro to Redux (есть версия на русском)

 

Единственный источник истины

Только одно хранилище для всего состояния приложения.

Хранилище — объект с несколькими уровнями вложенности.

 

Пример:

{
    num: 5,
}

Единственный источник истины

Пример чуть поболбше:

{
    users: {
    	list: [{
            id: 1,
            name: 'Mike',
            age: '12',
        }, {
            id: 2,
            name: 'Stasyan',
            age: '52',
        }, {
            id: 3,
            name: 'Dan',
            age: '34',
        }]
    },
    messages: {
    	list: [{
            id: 1,
            content: 'Ну ты и вымахал, Стасян!',
            authorId: 1,
            receiverId: 2,
        }, {
            id: 2,
            content: 'Стасян, скинь домашку, блинб',
            authorId: 3, 
            receiverId: 2,
        }]
    }
}

Read-only

Единственный способ изменить состояние — передать экшен — объект, описывающий, что произошло.

 

В React изменять состояние можно только с помощью setState().


В Redux — только с помощью экшенов.

Чистые функции

  • Детерминированность
  • Отсутствие сайд-эффектов
console.log(`Hello`);

const suc = a => b => a * b;

const rand = a => Math.random();

const getData = url => fetch(url);

Чистые функции

like props in React

Redux

Пример

Redux

Action

Просто объект с обязательным полем type

store.dispatch({
  type: 'INCREMENT',
  value: 42
})

Reducer

Reducer - чистая функция принимающая state и action и возвращающая новый state

function reducer(state = { num: 69 }, action) {
    switch (action.type) {
        case 'INCREMENT':
            return { num: state.num + action.num };
        default:
            return state;
}

Middleware

Мидлвейр - просто функция, которая принимает стор, следующий мидлвейр и ваш экшен.

Выполняет какое-то действие (например, выводит в консоль ваш экшен).

Передает выполнение следующему мидлвейверу (если это последний, то экшен попадает в редьюсер).

Middleware

const log = store => next => action => {
    console.log(action);
    next(action);
};

const store = createStore(reducer, applyMiddleware(log));

redux-thunk

import thunk from `redux-thunk`;

const store = createStore(reducer, applyMiddleware(thunk));


store.dispatch(dispatch => {
    fetch(`something.com`)
        .then(data => dispatch({ type: 'DATA', data }))
        .catch(err => dispatch({ type: 'ERR', err }));
});

redux-saga

Альтернативный подход к организации сайд-эффектов. Вместо того, чтобы диспатчить функции, которые обрабатываются redux-thunk-ом, вы создаёте сагу, которая собирает всю логику обработки внутрь себя. В отличие от thunk-ов, которые выполняются, когда вы их диспатчите, саги запускаются при старте приложения и как бы «работают в фоне». Саги слушают все экшены, которые диспатчит стор, и решают, что делать с ними.
И у саг в редаксе два преимущества по сравнению с thunk-ами:
— Они позволяют организовывать сложные последовательности сайд-эффектов
— И они очень легко тестируются

redux-saga

import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'
// worker Saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}
/*
  Starts fetchUser on each dispatched `USER_FETCH_REQUESTED` action.
  Allows concurrent fetches of user.
*/
function* mySaga() {
  yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}
/*
  Alternatively you may use takeLatest.
  Does not allow concurrent fetches of user. If "USER_FETCH_REQUESTED" gets
  dispatched while a fetch is already pending, that pending fetch is cancelled
  and only the latest one will be run.
*/
function* mySaga() {
  yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}

export default mySaga;

combineReducers

import { createStore, combineReducers } from 'redux';

const userReducer = (state = {}, action) => { … };

const widgetReducer = (state = {}, action) => { … };


// Создание одного большого редьюсера
const reducers = combineReducers({ userReducer, widgetReducer });

const store = createStore(reducers);

Action Creator

const getDataActionCreator = url =>
    dispatch => {
        fetch(url)
            .then(data => dispatch({ type: 'DATA', data }))
            .catch(err => dispatch({ type: 'ERR', err }));
    });

Redux hooks

  • useSelector
  • useDispatch
  • useStore

useSelector

const result: any = useSelector(selector: Function, equalityFn?: Function)
import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = () => {
  const counter = useSelector(state => state.counter)
  return <div>{counter}</div>
}

useDispatch

const dispatch = useDispatch()
import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()

  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: 'increment-counter' })}>
        Increment counter
      </button>
    </div>
  )
}

useStore

const store = useStore()
import React from 'react'
import { useStore } from 'react-redux'

export const CounterComponent = ({ value }) => {
  const store = useStore()

  // EXAMPLE ONLY! Do not do this in a real app.
  // The component will not automatically update
  //  if the store state changes
  return <div>{store.getState()}</div>
}

Перерыв!

Live coding

 

React + Redux (koa-react-starter)

Redux 2021

By Startup Summer