В процессе создания всё более сложных и крупных приложений в React начинаешь понимать, что управление общим состоянием всего приложения невозможно только при помощи класса React.Component
, использующего конструктор constructor()
с вызовом setState()
. Нам нужен контейнер состояний, такой как Redux
, чтобы можно было запустить его в разных средах, централизовав состояние/логику приложения, и легко проводить отладку.
Итак, вот некоторые особенности Redux, с которыми вы, наверняка, уже сталкивались, но не совсем понимали, что и как они делают… и, конечно же, примеры.
“Посмотрим, как эти части согласуются друг с другом”.Мы рассмотрим 15 малоизвестных особенностей Redux:
mapStateToProps
и mapDispatchToProps
.ownProps
в mapStateToProps()
и mapDispatchToProps()
?call()
и put()
в Redux-Saga.В основе работы 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)
Flux — это шаблон, Redux — библиотека. При использовании Redux вам придется пойти на некоторые компромиссы:
redux-immutable-state-invariant
и Immutable.js. 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)
Причиной для внесения этого пункта в мой список стал следующий популярный вопрос со Stackoverflow: “Можно ли отправить действие в самом редукторе?” Отвечая на этот вопрос, скажу лишь одно:
Отправка действия в редуктор — это анти-шаблон.
Вы можете отправить действие во время загрузки с помощью метода 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)
Лучший способ — использовать исходный редуктор и передать действие редуктору, созданному при помощи combineReducers()
.
Пример:
const appReducer = combineReducers({
// Здесь размещаются редукторы высшего уровня.
})
const rootReducer = (state, action) => {
if (action.type === 'USER_LOGOUT') {
state = undefined
}
return appReducer(state, action)
}
В JavaScript символ @ используетсядляобозначения декораторов, при помощи которых выдобавляете или видоизменяете классы и свойства.
context и
React-Redux context
React context предоставляет способ направлять данные через дерево компонентов без необходимости передачи свойств сверху вниз вручную на каждом уровне.
Используется для небольших приложений, тогда как Redux сам по себе обеспечивает более основательное и мощное управление состояниями.
> React-ReduxReact-Redux — это официальная библиотека, которая предоставляет привязки React для Redux. Она позволяет компонентам React считывать данные из хранилища 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 }
}
Лучший способ — использовать функцию connect()
с применением шаблона функций высшего порядка. Это позволяет отобразить креаторы состояния и действия в компонент и автоматически передать их, когда обновится хранилище.
Пример:
// Вы можете передать контекст как опцию в функцию connect
export default connect(
mapState,
mapDispatch,
null,
{ context: MyContext }
)(MyComponent)
// или, как обычно, вызвать функцию connect, чтобы начать
const ConnectedComponent = connect(
mapState,
mapDispatch
)(MyComponent)
// Затем передайте настроенный контекст как свойство присоединенному компоненту
<ConnectedComponent context={MyContext} />
Использование констант позволяет легко находить все случаи применения конкретной выполняемой функции в проекте. Кроме того, они помогут избежать большого количества ошибок 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
}
}
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
автоматически объединит разные источники свойств в конечный набор.
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()
являются функциями креаторов эффектов.
Мидлвар Redux Thunk
позволяет написать креатор действия, который возвращает функцию вместо действия. Thunk можно использовать для задержки отправки действия или для отправки только при выполнении определенного условия.
Существуют 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. Интересно, а знали ли вы о них?
Перевод статьи Vaibhav Khulbe: Demystifying lesser-known Redux terms and features (With examples)
Комментарии