В процессе создания всё более сложных и крупных приложений в React начинаешь понимать, что управление общим состоянием всего приложения невозможно только при помощи класса React.Component
, использующего конструктор constructor()
с вызовом setState()
. Нам нужен контейнер состояний, такой как Redux
, чтобы можно было запустить его в разных средах, централизовав состояние/логику приложения, и легко проводить отладку.
Итак, вот некоторые особенности Redux, с которыми вы, наверняка, уже сталкивались, но не совсем понимали, что и как они делают… и, конечно же, примеры.
Мы рассмотрим 15 малоизвестных особенностей Redux:
- Ключевые принципы Redux.
- Компромиссы при использовании Redux вместо Flux.
- Различие между
mapStateToProps
иmapDispatchToProps
. - Диспетчеризация действия в редукторе.
- Диспетчеризация действия при загрузке.
- Сброс состояния в Redux.
- Использование символа @в функции декораторе connect.
- Различие между React context и React-Redux.
- Создание AJAX-запроса в Redux.
- Лучший способ получить доступ в хранилище Redux.
- Зачем использовать константы?
- Зачем использовать
ownProps
вmapStateToProps()
иmapDispatchToProps()
? - Различие между
call()
иput()
в Redux-Saga. - Различие между Redux-Saga и Redux-Thunk.
- Установка начального состояния в Redux.
1. Ключевые принципы Redux
В основе работы Redux лежат 3 главных принципа:
1️⃣ Единственный источник истины
Это означает, что состояние всего приложения содержится в хранилище в виде дерева объектов.
Преимущества:
- Единственное дерево состояния облегчает процесс отладки и проверки приложения.
- Позволяет сохранять состояние приложения для ускорения процесса разработки.
- Облегчает создание универсальных и крупномасштабных приложений.
Пример:
// При выполнении:console.log(store.getState())
// В результате вы получите:
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
2️⃣ Состояние только для чтения
Состояние можно изменить только при отправке действия.
Так как все состояния централизованы и происходят последовательно друг за другом, то нет необходимости следить за состоянием гонки.
Пример:
// Способ отправки действий:
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
3️⃣ Изменения выполняются чистыми функциями
Вы пишите редукторы, чтобы указать на изменение состояния при помощи действий.
Так как редукторы являются чистыми функциями, вы можете контролировать порядок их вызова, отправлять дополнительные данные или даже писать переиспользуемые редукторы.
Пример:
// Способ создания чистых функций/редукторов
function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
import { combineReducers, createStore } from 'redux'
const reducer = combineReducers({ visibilityFilter, todos })
const store = createStore(reducer)
2. Преимущества Flux перед Redux
Flux — это шаблон, Redux — библиотека. При использовании Redux вам придется пойти на некоторые компромиссы:
- Придется избегать мутаций. Этогоможно добиться при помощи пакетов
redux-immutable-state-invariant
и Immutable.js. - Тщательно выбирать пакеты. УRedux есть точки расширения, такие как мидлвары и расширители хранилища, содержащие обширную экосистему пакетов.
- Отсутствует удобная интеграция с Flow.
3. Различие между mapStateToProps() и mapDispatchToProps()
> mapStateToProps()
Это утилита, которая помогает вашему компоненту обновлять состояние (которое обновляется некоторыми другими компонентами). Эта функция передается в качестве первого аргумента в connect
и впоследствии будет вызываться каждый раз при изменении состояния хранилища Redux.
Пример:
// TodoList.js
function mapStateToProps(state) {
const { todos } = state
return { todoList: todos.allIds }
}
export default connect(mapStateToProps)(TodoList)
> mapDispatchToProps()
Это утилита, которая помогает вашему компоненту запускать событие действия (отправка действия, которое может вызвать изменение состояния приложения). Компонент получает [dispatch](https://react-redux.js.org/api/connect#dispatch)
по умолчанию.
Пример:
import { addTodo, deleteTodo, toggleTodo } from './actionCreators'
const mapDispatchToProps = {
addTodo,
deleteTodo,
toggleTodo
}
export default connect(
null,
mapDispatchToProps
)(TodoApp)
4. Диспетчеризация действия в редукторе
Причиной для внесения этого пункта в мой список стал следующий популярный вопрос со Stackoverflow: “Можно ли отправить действие в самом редукторе?” Отвечая на этот вопрос, скажу лишь одно:
Отправка действия в редуктор — это анти-шаблон.
5. Диспетчеризация действия при загрузке
Вы можете отправить действие во время загрузки с помощью метода componentDidMount()
, проверяя данные в методе render()
.
Пример:
class App extends Component {
componentDidMount() {
this.props.fetchData()
}
render() {
return this.props.isLoaded
? <div>{'Loaded'}</div>
: <div>{'Not Loaded'}</div>
}
}
const mapStateToProps = (state) => ({
isLoaded: state.isLoaded
})
const mapDispatchToProps = { fetchData }
export default connect(mapStateToProps, mapDispatchToProps)(App)
6. Сброс состояния в Redux
Лучший способ — использовать исходный редуктор и передать действие редуктору, созданному при помощи combineReducers()
.
Пример:
const appReducer = combineReducers({
// Здесь размещаются редукторы высшего уровня.
})
const rootReducer = (state, action) => {
if (action.type === 'USER_LOGOUT') {
state = undefined
}
return appReducer(state, action)
}
7. Использование символа @ в функции декораторе connect
В JavaScript символ @ используетсядляобозначения декораторов, при помощи которых выдобавляете или видоизменяете классы и свойства.
8. Различие между React context и
React-Redux
> React context
React context предоставляет способ направлять данные через дерево компонентов без необходимости передачи свойств сверху вниз вручную на каждом уровне.
Используется для небольших приложений, тогда как Redux сам по себе обеспечивает более основательное и мощное управление состояниями.
> React-Redux
React-Redux — это официальная библиотека, которая предоставляет привязки React для Redux. Она позволяет компонентам React считывать данные из хранилища Redux и отправлять туда действия для обновления данных.
9. Создание AJAX-запроса в Redux
AJAX позволяет отправлять асинхронные HTTP-запросы для передачи и извлечения данных с сервера.
Чтобы выполнить вызов AJAX в Redux, можно использовать мидлвар redux-thunk
для определения асинхронных действий.
Пример:
export function fetchAccount(id) {
return dispatch => {
dispatch(setLoadingAccountState()) // Отображение спиннера загрузки
fetch(`/account/${id}`, (response) => {
dispatch(doneFetchingAccount()) // Скрытие спиннера загрузки
if (response.status === 200) {
dispatch(setAccount(response.json)) // Использование обычной функции для установки полученного состояния
} else {
dispatch(someError)
}
})
}
}
function setAccount(data) {
return { type: 'SET_Account', data: data }
}
10. Лучший способ получить доступ в хранилище Redux
Лучший способ — использовать функцию connect()
с применением шаблона функций высшего порядка. Это позволяет отобразить креаторы состояния и действия в компонент и автоматически передать их, когда обновится хранилище.
Пример:
// Вы можете передать контекст как опцию в функцию connect
export default connect(
mapState,
mapDispatch,
null,
{ context: MyContext }
)(MyComponent)
// или, как обычно, вызвать функцию connect, чтобы начать
const ConnectedComponent = connect(
mapState,
mapDispatch
)(MyComponent)
// Затем передайте настроенный контекст как свойство присоединенному компоненту
<ConnectedComponent context={MyContext} />
11. Зачем использовать константы?
Использование констант позволяет легко находить все случаи применения конкретной выполняемой функции в проекте. Кроме того, они помогут избежать большого количества ошибок ReferenceError
.
Пример:
// В constants.js
export const ADD_TODO = 'ADD_TODO'
export const DELETE_TODO = 'DELETE_TODO'
export const EDIT_TODO = 'EDIT_TODO'
// В actions.js
import { ADD_TODO } from './actionTypes';
export function addTodo(text) {
return { type: ADD_TODO, text }
}
// В reducer.js
import { ADD_TODO } from './actionTypes'
export default (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
];
default:
return state
}
}
12. Зачем использовать ownProps в mapStateToProps()
и mapDispatchToProps()
?
ownProps
является необязательным параметром, который мы добавляем в mapStateToProps()
или mapDispatchToProps()
в качестве второго аргумента. Используйте его в том случае, если вашему компоненту нужны данные из его собственных свойств для извлечения данных из хранилища.
Пример:
// Todo.js
function mapStateToProps(state, ownProps) {
const { visibilityFilter } = state
const { id } = ownProps
const todo = getTodoById(state, id)
// компонент получает дополнительно:
return { todo, visibilityFilter }
}
// Затем в вашем приложении родительский компонент отображает:
<ConnectedTodo id={123} />
// и ваш компонент получает props.id, props.todo и props.visibilityFilter
Согласно документации:
Вам необязательно включать значения из ownProps
в объект, возвращаемый из mapStateToProps
. connect
автоматически объединит разные источники свойств в конечный набор.
13. Различие между call()
и put()
в Redux-Saga
redux-saga
— это библиотека, которая призвана облегчить управление побочными эффектами приложения, а также сделать их более эффективными в выполнении, легкими в тестировании и способствующими улучшению обработки ошибок.
> call()
Проще говоря, вы используете call()
для создания описания эффекта, который дает команду мидлвару для вызова промиса. После этого мидлвар вызывает функцию и проверяет ее результат.
> put()
Что же касается функции put()
, то она создает эффект, который дает мидлвару команду отправить действие в хранилище.
function* fetchUserSaga(action) {
// Функция `call` получает оставшиеся аргументы, которые передаются в функцию `api.fetchUser`.
// Команда мидлвару вызвать промис, его разрешенное значение присваивается переменной `userData`
const userData = yield call(api.fetchUser, action.userId)
// Команда мидлвару отправить соответствующее действие.
yield put({
type: 'FETCH_USER_SUCCESS',
userData
})
}
Таким образом, call()
и put()
являются функциями креаторов эффектов.
14. Различие между Redux-Saga и Redux-Thunk
Мидлвар Redux Thunk
позволяет написать креатор действия, который возвращает функцию вместо действия. Thunk можно использовать для задержки отправки действия или для отправки только при выполнении определенного условия.
- Thunk использует промисы, тогда как Saga — генераторы.
- Thunk прост в использовании, да и промисы хорошо знакомы. Saga/Генераторы более мощные, но требуют изучения.
15. Установка начального состояния в Redux
Существуют 2 способа:
1.Использовать метод createStore
, который принимает в качестве второго аргумента необязательное значение preloadedState
.
Пример:
const rootReducer = combineReducers({
todos: todos,
visibilityFilter: visibilityFilter
})
const initialState = {
todos: [{ id: 123, name: 'example', completed: false }]
}
const store = createStore(
rootReducer,
initialState
)
2.Использовать явную проверку внутри редуктора.
Пример:
function myReducer(state = someDefaultValue, action)
Надеюсь, я понятно объяснил эти 15 особенностей Redux. Интересно, а знали ли вы о них?
Читайте также:
- Увеличиваем производительность приложения React + Redux с библиотекой Reselect
- Что такое Редьюсеры: Как использовать их без Redux
- Анализ возможностей React Native или 15 вариантов улучшить ваше приложение!
Перевод статьи Vaibhav Khulbe: Demystifying lesser-known Redux terms and features (With examples)