Управление состоянием
State management
Тощилин Сергей
План лекции
- State management: что это и зачем?
- Flux-архитектура
- Существующие решения
- Redux
- Использование Redux в React
- Идеальная библиотека?
State management: что это и зачем?
Показательный пример
Определения
- View – часть приложения, отвечающая за отображение данных на странице
- Состояние приложения (Application State) – структурированные данные о конкретном экземпляре приложения для одного конкретного пользователя
Двусторонняя связь
- Некоторые изменения View вызывают изменения State. Например, ввод текста или выбор радиокнопки
- Изменения State вызывают перерисовки во View.
Двусторонняя связь
- Некоторые изменения View вызывают изменения State. Например, ввод текста или выбор радиокнопки
- Изменения State вызывают перерисовки во View.
Какие изменения?
Двусторонняя связь
Двусторонняя связь
MVVM (Model-View-ViewModel)
В чем здесь проблема?
Вопросы к MVVM:
Вопросы к MVVM:
- Как контролировать внесение изменений в State? Откуда пришли данные в State? из View/из AJAX-запроса/из записи в storage (нужное подчеркнуть)
- Как вообще отлаживать изменение View после изменения State?
- Как убедиться в том, что после изменений State остался корректным?
Действия, которые поступают в диспетчер с целью отработать в Store
Cущность, которая упорядочивает и сихронизирует проброс Actions в Store
Состояние приложения. Изменяется строго на основе старого состояния и данных из Action
Конечная точка использования вашего Store
Вопросы к MVVM:
- Как контролировать внесение изменений в Store (откуда пришли данные в Store? из View/из AJAX-запроса/из записи в storage (нужное подчеркнуть))
actions!
Вопросы к MVVM:
- Как контролировать внесение изменений в Store (откуда пришли данные в Store? из View/из AJAX-запроса/из записи в storage (нужное подчеркнуть))
actions!
- Как вообще отлаживать изменение View после изменения Store?
dispatcher!
Вопросы к MVVM:
- Как контролировать внесение изменений в Store (откуда пришли данные в Store? из View/из AJAX-запроса/из записи в storage (нужное подчеркнуть))
actions!
- Как вообще отлаживать изменение View после изменения Store?
dispatcher!
- Как убедиться в том, что после изменений Store остался корректным?
...
Вопросы к Flux:
- Как контролировать внесение изменений в Store (откуда пришли данные в Store? из View/из AJAX-запроса/из записи в storage (нужное подчеркнуть))
actions!
- Как вообще отлаживать изменение View после изменения Store?
dispatcher!
- Как убедиться в том, что после изменений Store остался корректным?
...
Cуществующие решения
Cуществующие решения
Redux – библиотека для управления состоянием, основанная на flux.
Фичи Redux
Один большой Store
Ликбез: чистая функция
- Чистая функция не обладает побочными эффектами: не изменяет свои параметры или глобальные переменные
- Чистая функция является детерминированной: всегда возвращает одинаковые значения на одинаковых данных
reducers
export const notesReducer = (
state: ApplicationState,
action: IAction
) => {
switch (action.type) {
case 'INCREMENT_NOTE_COUNTER': {
const { noteCount } = state;
const { newNoteCount } = action;
return {
...state,
noteCount: noteCount + newNoteCount
};
}
...
reducers
Обычно выглядит, как длинный switch-case:
export const notesReducer = (
state: ApplicationState,
action: IAction
) => {
switch (action.type) {
case 'INCREMENT_NOTE_COUNTER': {
const { noteCount } = state;
const { newNoteCount } = action;
return {
...state,
noteCount: noteCount + newNoteCount
};
}
...
reducers
Является чистой функцией:
export const notesReducer = (
state: ApplicationState,
action: IAction
) => {
switch (action.type) {
case 'INCREMENT_NOTE_COUNTER': {
const { noteCount } = state;
const { newNoteCount } = action;
return {
...state,
noteCount: noteCount + newNoteCount
};
}
...
Фичи Redux
Есть reducers
reducer является чистой функцией, которая возвращает новый store на основе старого и данных из action
Вопросы к Flux:
- Как контролировать внесение изменений в Store (откуда пришли данные в Store? из View/из AJAX-запроса/из записи в storage (нужное подчеркнуть))
actions!
- Как вообще отлаживать изменение View после изменения Store?
dispatcher!
- Как убедиться в том, что после изменений Store остался корректным?
reducer!
Вопросы к Flux:
- Как контролировать внесение изменений в Store (откуда пришли данные в Store? из View/из AJAX-запроса/из записи в storage (нужное подчеркнуть))
actions!
- Как вообще отлаживать изменение View после изменения Store?
dispatcher!
- Как убедиться в том, что после изменений Store остался корректным?
reducer!
Некоторые моменты, которые полезно знать перед тем как начать разрабатываться с Redux:
- store и вся работа с ним кладется в отдельную папку
- Библиотека redux для работы со Store
- Библиотека react-redux для использования в компонентах React
- Есть Redux DevTools, который отлично помогает при разработке
Перерыв
Можно задавать вопросы:)
Пример приложения
Пример приложения
types
types
/src/common/types.ts
export enum NOTE_STATUS {
actual = 'actual',
old = 'old',
deleted = 'deleted'
}
interface IPureNote { ... }
export interface INote extends IPureNote { ... }
export enum ACTIONS { ... }
types
/src/common/types.ts
export enum NOTE_STATUS { ... }
export interface IPureNote { title: string;
description: string;
status: NOTE_STATUS;
created: Date;
}
export interface INote extends IPureNote {
id: number;
}
export enum ACTIONS { ... }
types
/src/common/types.ts
export enum NOTE_STATUS { ... }
export interface IPureNote { ... }
export interface INote extends IPureNote { ... }
// Все типы Action'ов
export enum ACTIONS {
ADD_NOTE = 'ADD_NOTE',
EDIT_NOTE = 'EDIT_NOTE',
CHANGE_STATUS = 'CHANGE_STATUS'
}
types
/src/common/types.ts
// Payloads – ПОЛЕЗНАЯ(!) информация, которая передается в action
export interface IEditNotePayload {
id: number;
newNote: IPureNote;
}
export interface IAddNotePayload { ... }
export interface IChangeStatusPayload { ... }
// Интерфейс, который описывает Action целиком
export interface IAction { ... }
types
/src/common/types.ts
// Payloads – ПОЛЕЗНАЯ(!) информация, которая передается в action
export interface IAddNotePayload { ... }
export interface IEditNotePayload { ... }
export interface IChangeStatusPayload { ... }
// Интерфейс, который описывает Action целиком
export interface IAction {
type: ACTIONS;
payload:
| IAddNotePayload
| IEditNotePayload
| IChangeStatusPayload;
}
actions
actions
/src/store/actions.ts
import {
IAddNotePayload,
IEditNotePayload,
IChangeStatusPayload,
ACTIONS,
IAction
} from '../common/types';
export const editNote = ({ id, newNote }: IEditNotePayload): IAction =>
({
type: ACTIONS.EDIT_NOTE,
payload: {
id,
newNote
}
});
Каждый action – есть функция
/src/store/actions.ts
import {
IAddNotePayload,
IEditNotePayload,
IChangeStatusPayload,
ACTIONS,
IAction
} from '../common/types';
export const editNote = ({ id, newNote }: IEditNotePayload): IAction =>
({
type: ACTIONS.EDIT_NOTE,
payload: {
id,
newNote
}
});
Принимает payload
/src/store/actions.ts
import {
IAddNotePayload,
IEditNotePayload,
IChangeStatusPayload,
ACTIONS,
IAction
} from '../common/types';
export const editNote = ({ id, newNote }: IEditNotePayload): IAction =>
({
type: ACTIONS.EDIT_NOTE,
payload: {
id,
newNote
}
});
Возвращает IAction
/src/store/actions.ts
import {
IAddNotePayload,
IEditNotePayload,
IChangeStatusPayload,
ACTIONS,
IAction
} from '../common/types';
export const editNote = ({ id, newNote }: IEditNotePayload): IAction =>
({
type: ACTIONS.EDIT_NOTE,
payload: {
id,
newNote
}
});
reducers
reducers
/src/store/reducers.ts
import { ..., IEditNotePayload, IAction } from '../common/types';
...
// состояние Store при инициализации приложения
export const initialState = { notes: initialNotes };
const notesReducer = (
state: ApplicationState = initialState,
action: IAction
) => {
...
switch (action.type) {
case ACTIONS.EDIT_NOTE:
...
}
};
В аргументах приходит тот самый action
/src/store/reducers.ts
import { ..., IEditNotePayload, IAction } from '../common/types';
...
// состояние Store при инициализации приложения
export const initialState = { notes: initialNotes };
const notesReducer = (
state: ApplicationState = initialState,
action: IAction
) => {
...
switch (action.type) {
case ACTIONS.EDIT_NOTE:
...
}
};
Действие определяется по типу action'а
/src/store/reducers.ts
import { ..., ACTIONS, IAction } from '../common/types';
...
// состояние Store при инициализации приложения
export const initialState = { notes: initialNotes };
const notesReducer = (
state: ApplicationState = initialState,
action: IAction
) => {
...
switch (action.type) {
case ACTIONS.EDIT_NOTE:
...
}
};
Один из case'ов подробнее:
/src/store/reducers.ts
import { ApplicationState } from './index';
...
case ACTIONS.EDIT_NOTE: {
const { id, newNote } = action.payload;
const noteWithId = { ...newNote, id };
const noteIndex = state.notes.findIndex(
(note: INote) => note.id === id
);
return {
...state,
notes: [...notes].splice(noteIndex, 1, noteWithId);
};
}
...
reducer возвращает новый Store:
/src/store/reducers.ts
import { ApplicationState } from './index';
...
case ACTIONS.EDIT_NOTE: {
const { id, newNote } = action.payload;
const noteWithId = { ...newNote, id };
const noteIndex = state.notes.findIndex(
(note: INote) => note.id === id
);
return {
...state,
notes: [...notes].splice(noteIndex, 1, noteWithId);
};
}
...
Инициализация Store
Инициализация Store
/src/store/index.ts
import { createStore } from 'redux';
import { INote } from '../common/types';
import reducer, { initialState } from './reducers';
export interface ApplicationState {
notes: INote[];
}
export default createStore(
reducer,
initialState as any
);
Передаем reducers
/src/store/index.ts
import { createStore } from 'redux';
import { INote } from '../common/types';
import reducer, { initialState } from './reducers';
export interface ApplicationState {
notes: INote[];
}
export default createStore(
reducer,
initialState as any
);
Значение Store по умолчанию
/src/store/index.ts
import { createStore } from 'redux';
import { INote } from '../common/types';
import reducer, { initialState } from './reducers';
export interface ApplicationState {
notes: INote[];
}
export default createStore(
reducer,
initialState as any
);
ApplicationState пишем здесь
/src/store/index.ts
import { createStore } from 'redux';
import { INote } from '../common/types';
import reducer, { initialState } from './reducers';
export interface ApplicationState {
notes: INote[];
}
export default createStore(
reducer,
initialState as any
);
Как подключить к React?
Как подключить к React?
/src/App.tsx
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { Route, Switch } from 'react-router-dom';
import store from './store';
export default class App extends Component {
public render() {
return (
<Provider store={store}>
<div className="App">
<Switch>
<Route path="/note/:id" component={Note} />
<Route path="/" component={NotesList} />
</Switch>
</div>
</Provider>
);
}
}
Ликбез: react-router
/src/App.tsx
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { Route, Switch } from 'react-router-dom';
import store from './store';
export default class App extends Component {
public render() {
return (
<Provider store={store}>
<div className="App">
<Switch>
<Route path="/note/:id" component={Note} />
<Route path="/" component={NotesList} />
</Switch>
</div>
</Provider>
);
}
}
На самом деле обычные роуты, на которые будет реагировать React
Вернемся к redux
/src/App.tsx
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { Route, Switch } from 'react-router-dom';
import store from './store';
export default class App extends Component {
public render() {
return (
<Provider store={store}>
<div className="App">
<Switch>
<Route path="/note/:id" component={Note} />
<Route path="/" component={NotesList} />
</Switch>
</div>
</Provider>
);
}
}
Работаем с redux в компонентах
Работаем с redux в компонентах
/src/components/note/note.tsx
import { connect } from 'react-redux';
import { ApplicationState } from '../../store';
import { changeStatus, editNote } from '../../store/actions';
...
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = { dispatch: (action: IAction) => void };
type Props = StateProps & DispatchProps & ...;
class Note extends Component<Props, IOwnState> {
...
}
const mapStateToProps = (state: ApplicationState, props: Props) => ({
currentNote: state.notes.find(note => note.id === id);
});
export default connect(mapStateToProps)(Note);
connect()()
/src/components/note/note.tsx
import { connect } from 'react-redux';
import { ApplicationState } from '../../store';
import { changeStatus, editNote } from '../../store/actions';
...
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = { dispatch: (action: IAction) => void };
type Props = StateProps & DispatchProps & ...;
class Note extends Component<Props, IOwnState> {
...
}
const mapStateToProps = (state: ApplicationState, props: Props) => ({
currentNote: state.notes.find(note => note.id === id);
});
export default connect(mapStateToProps)(Note);
connect(...)(Note) возвращает новый компонент, который имеет доступ в Store
mapStateToProps
/src/components/note/note.tsx
import { connect } from 'react-redux';
import { ApplicationState } from '../../store';
import { changeStatus, editNote } from '../../store/actions';
...
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = { dispatch: (action: IAction) => void };
type Props = StateProps & DispatchProps & ...;
class Note extends Component<Props, IOwnState> {
...
}
const mapStateToProps = (state: ApplicationState, props: Props) => ({
currentNote: state.notes.find(note => note.id === id);
});
export default connect(mapStateToProps)(Note);
mapStateToProps передается первым аргументом в connect(...) и подкладывает в this.props поля из Store
Как получить доступ к Store?
/src/components/note/note.tsx
...
type StateProps = ReturnType<typeof mapStateToProps>;
type Props = StateProps & DispatchProps;
class Note extends Component<Props, IOwnState> {
...
public render() {
return (
...
<div className="note-page__field">
Дата создания
<div className="note-page__created-date">
{this.props.currentNote.created}
</div>
</div>
...
)
}
...
}
Данные будут подложены в this.props
/src/components/note/note.tsx
...
type StateProps = ReturnType<typeof mapStateToProps>;
type Props = StateProps & DispatchProps;
class Note extends Component<Props, IOwnState> {
...
public render() {
return (
...
<div className="note-page__field">
Дата создания
<div className="note-page__created-date">
{this.props.currentNote.created}
</div>
</div>
...
)
}
...
}
...
Как вызвать событие?
/src/components/note/note.tsx
...
type DispatchProps = { dispatch: (action: IAction) => void };
type Props = StateProps & DispatchProps;
class Note extends Component<Props, IOwnState> {
...
public onEditButtonClick = (e: ReactMouseEvent) => {
...
// Кладем в currentNote данные об отредактированной заметке
const currentNote = ...;
this.props.dispatch({
type: ACTIONS.EDIT_NOTE,
payload: { id, newNote: currentNote }
});
};
...
}
...
Генерируем action прямо в dispatch
/src/components/note/note.tsx
...
type DispatchProps = { dispatch: (action: IAction) => void };
type Props = StateProps & DispatchProps;
class Note extends Component<Props, IOwnState> {
...
public onEditButtonClick = (e: ReactMouseEvent) => {
...
// Кладем в currentNote данные об отредактированной заметке
const currentNote = ...;
this.props.dispatch({
type: ACTIONS.EDIT_NOTE,
payload: { id, newNote: currentNote }
});
};
...
}
...
Используем actionCreators
/src/components/note/note.tsx
import { editNote } from '../../store/actions';
...
type DispatchProps = { dispatch: (action: IAction) => void };
type Props = StateProps & DispatchProps;
class Note extends Component<Props, IOwnState> {
...
public onEditButtonClick = (e: ReactMouseEvent) => {
...
// Кладем в currentNote данные об отредактированной заметке
const currentNote = ...;
this.props.dispatch(editNote({ id, newNote: currentNote }));
};
...
}
...
Используем actionCreators
/src/components/note/note.tsx
import { editNote } from '../../store/actions';
...
type DispatchProps = { dispatch: (action: IAction) => void };
type Props = StateProps & DispatchProps;
class Note extends Component<Props, IOwnState> {
...
public onEditButtonClick = (e: ReactMouseEvent) => {
...
// Кладем в currentNote данные об отредактированной заметке
const currentNote = ...;
this.props.dispatch(editNote({ id, newNote: currentNote }));
};
...
}
...
Еще раз весь цикл на примере
Dispatch'им событие из комопненты
/src/components/note/note.tsx
import { editNote } from '../../store/actions';
...
type DispatchProps = { dispatch: (action: IAction) => void };
type Props = StateProps & DispatchProps;
class Note extends Component<Props, IOwnState> {
...
public onEditButtonClick = (e: ReactMouseEvent) => {
...
// Кладем в currentNote данные об отредактированной заметке
const currentNote = ...;
this.props.dispatch(editNote({ id: [56], newNote: [ЗАМЕТКА] }))
};
...
}
...
actionCreator возвращает данные с типом action'а
/src/store/actions.ts
import {
IAddNotePayload,
IEditNotePayload,
IChangeStatusPayload,
ACTIONS,
IAction
} from '../common/types';
const editNote = ({ id: [56], newNote: [ЗАМЕТКА] }: IEditNotePayload) =>
({
type: ACTIONS.EDIT_NOTE // 'editNote',
payload: {
id: [56],
newNote: [ЗАМЕТКА]
}
});
...
Принимаем данные в reducer'е:
/src/store/reducers.ts
import { ApplicationState } from './index';
...
case ACTIONS.EDIT_NOTE: { // 'editNote'
const { id: [56], newNote: [ЗАМЕТКА] } = action.payload;
// noteWithId – это [ЗАМЕТКА №56]
const noteWithId = { ...newNote: [ЗАМЕТКА], id: [56] };
// находим индекс редактируемой заметки
const index = state.notes.findIndex(
(note: INote) => note.id === id
);
// копируем массив заметок и вставляем на место старой новую
return {
...state,
notes: [...notes].splice(index, 1, noteWithId: [ЗАМЕТКА №56]);
};
}
...
Кладем Store в компонент
/src/components/note/note.tsx
...
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = { dispatch: (action: IAction) => void };
type Props = StateProps & DispatchProps & ...;
class Note extends Component<Props, IOwnState> {
...
}
const mapStateToProps = (state: ApplicationState, props: Props) => ({
currentNote: state.notes.find(note => note.id === id: [56]);
// теперь this.props.currentNote – это [ЗАМЕТКА №56]
});
export default connect(mapStateToProps)(Note);
Получаем Store из this.props
/src/components/note/note.tsx
...
type StateProps = ReturnType<typeof mapStateToProps>;
type Props = StateProps & DispatchProps; // подкладываем StateProps в this.props
class Note extends Component<Props, IOwnState> {
...
public render() {
return (
...
<div className="note-page__field">
Дата создания
<div className="note-page__created-date">
{this.props.currentNote.created} // это [ЗАМЕТКА №56]
</div>
</div>
...
)
}
...
}
Получаем Store из this.props
/src/components/note/note.tsx
...
type StateProps = ReturnType<typeof mapStateToProps>;
type Props = StateProps & DispatchProps; // подкладываем StateProps в this.props
class Note extends Component<Props, IOwnState> {
...
public render() {
return (
...
<div className="note-page__field">
Дата создания
<div className="note-page__created-date">
{this.props.currentNote.created} // это [ЗАМЕТКА №56]
</div>
</div>
...
)
}
...
}
Перерыв
Можно задавать вопросы:)
Redux DevTools
Запуск
Redux DevTools
Отображается вся история action'ов для экземпляра приложения
Redux DevTools
Для каждого action'а (или даже нескольких) можно посмотреть параметры action'а, состояние store после выполнения и diff стора
Redux DevTools
Redux DevTools
Redux DevTools
То же демо, только с Redux DevTools
Идеальная библиотека?
Да, но:
- В Store теперь хочется складывать вообще все
- В redux'е store не знает о том, какие его части важны для каждой конкретной view
- Нет асинхронности:(
Проблема 1. Что делать, если Store стал одной большой помойкой
Куда класть данные и когда использовать Application State?
Все данные делятся на 3 типа:
- Данные, которые могут поменяться из нескольких мест
- Данные, которые нужны в нескольких несвязанных частях приложения (компонентах)
- Начальные данные для приложения
Данные, которые могут поменяться из нескольких мест
Данные, которые нужны в нескольких несвязанных частях приложения (компонентах)
Данные, которые нужны в нескольких несвязанных частях приложения (компонентах)
Начальные данные для приложения
Речь о данных, которые пришли с сервера: переводы, какие-то настройки, и т.д.
- Хорошо бы, чтобы эти данные хранились в одном месте...
- И чтобы достать их можно было из любого компонента...
- React Context API!
Context provides a way to pass data through the component tree without having to pass props down manually at every level
Проблема 2. Как избежать rerender'а во всех местах?
Хорошо бы, чтобы перерисовывались только те View, данные для которых реально поменялись...
Проблема 3. Асинхронность?
Библиотеки для работы с асинхронностью в Redux: