Redux
вопросы после собеседования:
- что значит задиспатчить событие
- redux-thunk что делает и что будет если его отключить
- bindActionCreator - в typescript ломает типизацию - как можно обойтись без него
Redux.js это библиотека, иструмент усправления как состоянием памяти, так и состоянием интерфейса в JS приложениях. Хорошо подходит для одностраничных приложений. Еще говорят что это библиотека реализующая слой Store в приложениях написанных с использованием архитектуры Flux. Предлагает думать о приложении как о начальном состоянии модифицированном последовательностью действий.
Redux имеет три принципа: 1. Единственный источник истины - т.к. Redux имеет только одно хранилище для всего состояния приложения. 2. Состояние доступно только для чтения - единственный способ изменить состояние - передать экшен - объект, описывающий что произошло. Само хранилище имеет очень маленький API:
- store.dispatch(action) - позволяет изменять состояние store
- store.subscribe(listener) - регистрирует слущателей
- store.getState() - позволяет получить текущее состояние store
Redux реализует состояние store не просто как дерево - а как не изменяемое дерево (immutable tree). При изменении состояния всегде получаем новый объект. Это не позволяет вернуться к старому состоянию, но зато пишется болле простой код (и легче тестировать).
3. Изменения делаются чистыми функциями - изменение состояния происходит через reducer - функция, которая обрабатывает экшены и могут вносить изменения в состояние. Reducer это чистая функция - тоясть она должна удовлетворять след.условиям:
- они не должны делать внешнии вызовы по сети или к базе данныех
- они возвращают значения которые зависят только он переданых данных
- их аргументы неизменяемые, тоесть функция не вносит в них изменений
- вызов функции с теми же аргументами, возвращает тот же результат
Но на самом деле центральная функция Redux'а createStore сама по себе накладывает только два ограничения на то, как вы должны писать свой код: action'ы должны быть простыми объектами, и должны содержать определенный type. Все остальные ограничения накладывают Redux DevTools, React-Redux, React и Reselect, которые действительно полагаются на правильное использование иммутабельности, сериализуемости action'ов/состояния и чистых функций-reducer'ов.
Возможности для разработчиков, такие как отладка перемещением во времени, являются одними из ключевых вариантов использования Redux. Следовательно, такие ограничения как иммутабельность и сериализуемость существуют в основном для того, чтобы подобные возможности были доступны при разработке, а также для упрощения отслеживания потока данных и логики изменения. (Раздел «Введение» в документации Redux)
Action
Flux Standart Action (FSA) - стандарт, описывающий структуру action (литерального объекта). (https://github.com/acdlite/flux-standard-action) Basic FSA
{
type: 'GET',
payload: {
text: 'Do something'
}
}
Promise FSA
{
type: 'GET_REQUEST',
payload: request | new Error(),
error: true // boolean
}
Вы можете не придерживаться FSA, но поле type
- обязано быть, т.к. в redux-хранилище - присутствует проверка на type (причем не равный undefined).
Принято что action используются не на прямую а через функцию actionCreator - функция которая просто возвращает action.
function getComment(message){
return {
type: 'GET_COMMIT',
payload: message
}
};
Литеральный объкт принято возвращать из actionCreator-a т.к. его легко сериализовать (json), если например экшен прилетел с сервера (Сметанин)
Reducer
За изменение состояния store под действием action отвечает функция reducer:
function reducer(state = initialState, action){
switch(action.type){
case 'GET_COMMENT':
return {
...state,
photo: action.photo
}
default:
return state
}
}
initialState
это состояние store по умолчанию
reducer должен удовлетворять следующим условиям:
- должен вернуть новое состояние
- если не смог обработать событие - вернуть старое состояние!!!
- функция должна быть "чистой"
Функция которая не мутирует(изменяет) входные данные и не зависит от внешнего состояния (например, базы данных, DOM, глобальных перемен) и обеспечивает один и тот же результат и одних и тех же входных данных называется чистой функцией.
Почему редьюсер должен быть чистой функцией и обязательно возвращать старое состояние если не смог обработать action:
Вот пример кода из исходников Redux:
Как видно redux сравнивает старое состояние с новым просто как объект (тот же объект или нет, не глубокое сравнение (очень затратное)) поэтому если мы мутируем старое состояние - тогда redux не будет реагировать на изменения, а если будем возвращать новое состояние даже при не удаче - redux будет постоянно перерисовывать DOM.
Обычно на каждое поле в store пишут отдельный reducer, а потом объединяют с помощью функции combineReducer():
import user from './UserReducer';
import page from './PageReducer';
export default combineReducer({
user,
page
})
Внимание: Имена функция конкретных редьюсеров должны совпадать с названиями полей store, которые они изменяют.
Важно помнить что при dispatch любого action-а будут вызваны все редьюсеры.
Примечание: reducer-ы вызываются дважды в приложении, один раз при создании (например когда создается store - в метод createStore - передается редьюсер) и второй раз когда вызывается экшен. Еще стоит помнить что когда вы задаете состояние по умолчанию в редьюсере может произойти ситуация - пользователь при обновлении станицы, обновляет состояния хранилища до начального.
Store
Создание store:
const store = createStore(
reducer, //или combineReducer
initialState, // состояние по умолчанию
applyMiddleware(middleware) // усилители (смотри ниже)
)
В Store лучше всего хранить нормализованные данные. Так же не стоит его захламлять данными которые можно вычислить на лету (например в selector-е). Не стоит хранить временные данные, которые нужны только для внут. функционирования компонента - store-хранилище должно быть отражением БД.
Middleware
middleware (усилители) - это функция, которая фозвращает функцию. Т.е. берет входные данные, добавляет что-то и передает дальше.
С помощью усилителей реализуют, например, логирование, обработку ошибок, кэширование и т.д.
Пример логирования
export default ping = store => next => action => {
console.log('ping');
return next(action);
}
благодяря методу applyMiddleware становятся доступными:
- store - состояние нашего приложения
- next - функция-обертка, которая позволяет продолжить выполнения цепочки
- action - действие
Например для реализации асинхронных запросов нужно использовать redux-thunk.js - это синтаксический сахар - усилитель, который позволяет dispatch-ть не просто объект а функцию, а также получить доступ к dispatch внутри события. Мы получаем доступ к dispatch и getState внутри actionCreator
то есть благодаря thunk мы можем dispatch-ить функцию, которая первым параметром получает dispatch а вторым getState
function comment(){
return dispatch => {
dispatch({type: 'GET_COMMENT_REQUEST'});
return fetch(url, method: 'GET').
then(req => req.json()).
then(
result => dispatch({type: 'GET_COMMENT_SUCSESS', payload: result}),
error => dispatch({type: 'GET_COMMENT_FAILURE', payload: error})
);
}
}
когда мы dispatch-им экшен с помошью thunk - это значит из actionCreator мы возвращаем функцию которая первым аргументом получает dispatch. Форма записи без использования thunk
class Action {
constructor(private dispatch){this.dispatch = dispatch}
getUsers = (username: string) => {
const disp = this.dispatch;
disp({type: 'GET_USERS_REQUEST'});
return fetch(url).then(res => res.json())
.then(
(resolve) => disp({type: 'GET_USERS_SUCCESS, payload: resolve}),
(error) => disp({type: 'GET_USERS_ERROR', payload: error})
);
}
}
...
const mapDispatchToProps = (dispatch) => {
return {
actions: new Action(dispatch);
}
}
Сам redux-thunk:
function createMiddleware(args){
return ({dispatch, getState}) => next => action => {
if(typeof action === 'function'){
return action(dispatch, setState, args);
}
return next(action);
}
}
разное(свалка - нужно разобрать)
реализация кода reducer-а где нужно изменять состояния только одного объекта из массива
Source Code
function createStore(reducer) {
var state;
var listeners = []
function getState() {
return state
}
function subscribe(listener) {
listeners.push(listener)
return unsubscribe() {
var index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
function dispatch(action) {
state = reducer(state, action)
listeners.forEach(listener => listener())
}
dispatch({})
return { dispatch, subscribe, getState }
}
увеличение производительности и расширяемости
не использовать this.state внутри this.setState(), т.к. мы не можем быть уверены что при использовании this.state - мы получим последние данные и их ни кто не меняет.
this.setState((prevState, currentProps) => { return {...prevState, age: currentProps.age} });
redux-хранилище должно быть нормализованным (как в БД SQL), ни каких вложенных структур - только связь через id. Это позволяет избавится от вложенных reducer-ов, и толстых actionCreator-ов.
- выносить data proccessing из компонента, т.к. это сильно тормозит пере-рендеринг компонента. Лучший вариант использование selector-ов (reselect)
const userSelect = (state) => state.user;
const mapStateToProps = (state) => {
return {
user: userSelect(state),
books: booksSelect(state, props)
}
}
- использовать memorize (lodash или встроено в reselect)
- todo
отличное видео, нужно скачать и сохранить на google disk