Как использовать Redux для управления состоянием
Возможность иметь единственный источник (единое хранилище) — вот что дает преимущество Redux над традиционным Context API.
Итак, без лишних слов, давайте рассмотрим, как мы можем наилучшим образом использовать Redux для управления состоянием в масштабе всего приложения, написав более чистый и оптимизированный код.
Перед тем, как мы начнем
Содержание статьи:
В этой статье в основном рассматриваются различные способы улучшения существующего хранилища Redux. Таким образом, вам нужно знать основы Redux, такие как настройка редукторов, хранилище Redux и отправка действий. Вы уже знаете основы Redux и хотите улучшить свои знания о хранилище Redux? Хорошо. Вот что мы расскажем в этой статье:
Краткое описание того, как использовать хранилище Redux
React JS. Основы
Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения
Получить курс сейчас!
Как использовать библиотеку Redux Toolkit, создав хранилище Redux для приложения электронной коммерции. Примечание: мы не будем создавать все приложение, а только его часть управления состоянием.
Как обрабатывать асинхронный код за пределами компонентов с помощью action creators.
Итак, приступим.
Краткий обзор Redux Store
Прежде чем посмотреть, как мы можем улучшить наш код Redux, давайте кратко рассмотрим, как мы использовали Redux до этого момента.
Вот базовый пример простого приложения Redux, которое может увеличивать и уменьшать счетчик нажатием кнопок, а состояние счетчика управляется и обрабатывается Redux:
JavaScript // redux-store.js import { createStore } from ‘redux’ const initialState = { counter: 0, } const reducer = (state = initialState, action) => { // Reducer function switch (action.type) { case ‘INCREMENT’: return { …state, counter: state.counter + 1 } case ‘DECREMENT’: return { …state, counter: state.counter – 1 } default: return state } } const store = createStore(reducer) // redux-store export default store
123456789101112131415161718192021 | // redux-store.jsimport { createStore } from ‘redux’ const initialState = { counter: 0,} const reducer = (state = initialState, action) => { // Reducer function switch (action.type) { case ‘INCREMENT’: return { …state, counter: state.counter + 1 } case ‘DECREMENT’: return { …state, counter: state.counter – 1 } default: return state }} const store = createStore(reducer) // redux-store export default store |
JavaScript // Component.jsx import { useDispatch, useSelector } from ‘react-redux’ export default function Component(props) { const dispatch = useDispatch() const counter = useSelector((state) => state.counter) const incrementHandler = () => { dispatch({ type: ‘INCREMENT’ }) } const decrementHandler = () => { dispatch({ type: ‘DECREMENT’ }) } return ( <div> <h1>Counter:{counter}</h1> <button onClick={incrementHandler}>Increment</button> <button onClick={decrementHandler}>Decrement</button> </div> ) }
12345678910111213141516171819202122 | // Component.jsximport { useDispatch, useSelector } from ‘react-redux’ export default function Component(props) { const dispatch = useDispatch() const counter = useSelector((state) => state.counter) const incrementHandler = () => { dispatch({ type: ‘INCREMENT’ }) } const decrementHandler = () => { dispatch({ type: ‘DECREMENT’ }) } return ( <div> <h1>Counter:{counter}</h1> <button onClick={incrementHandler}>Increment</button> <button onClick={decrementHandler}>Decrement</button> </div> )} |
Это очень простой пример, который можно создать, просто используя хуки состояния. Но он служит хорошим обзором того, как мы обычно используем Redux.
Вышеупомянутое хранилище Redux прекрасно подходит, если не так много разных типов логики, которые нужно обрабатывать или отправлять.
Также обратите внимание, что я использовал const initialState вместо var или let из-за свойства неизменяемости. То есть мы на самом деле не изменяем существующее состояние, а вместо этого реконструируем новое состояние вместе с обновленными значениями.
Это хорошо. Но что, если сложность приложения растет и требует от нас выполнения нескольких состояний и логической обработки? Это именно то, что мы рассмотрим в этом руководстве.
Как создать хранилище Redux с помощью Redux Toolkit
В оставшейся части статьи давайте рассмотрим, как управлять состоянием в приложении для электронной коммерции. Мы хотели бы отслеживать и транслировать несколько состояний для различных компонентов.
Как правило, всегда полезно иметь приблизительное представление о том, как мы реализуем дизайн, прежде чем писать код.
Когда мы думаем о приложении для электронной коммерции, возможные состояния, вероятно, следующие:
Аутентификация
Отслеживание корзины
Список наблюдения
Мы можем разделить логику обработки состояния и само состояние на разные модули, а затем объединить все это в одно хранилище. Для этого мы воспользуемся Redux Toolkit.
Redux Toolkit — это сторонняя библиотека, такая как Redux, созданная и поддерживаемая командой Redux. Вам может быть интересно, почему команда Redux вообще создала Redux Toolkit. Ответ находится в самом начале документации:
Пакет Redux Toolkit предназначен для стандартного способа написания логики Redux. Первоначально он был создан для решения трех общих проблем, связанных с Redux:
«Настройка хранилища Redux слишком сложна»
«Нужно добавить много пакетов, чтобы Redux мог делать что-нибудь полезное»
«Redux требует слишком много шаблонного кода»
В общем, весь смысл использования Redux Toolkit состоит в том, чтобы сделать использование Redux проще и эффективнее.
Как установить Redux Toolkit
Вы можете установить Redux Toolkit либо с помощью npm либо yarn:
JavaScript npm install @reduxjs/toolkit yard add @reduxjs/toolkit
12 | npm install @reduxjs/toolkityard add @reduxjs/toolkit |
Как использовать Redux Toolkit
Первой остановкой при создании хранилища Redux является настройка обработки состояния. Вместо использования редукторов мы будем использовать createSlice который предоставляется Redux Toolkit. createSlice принимает 3 обязательных аргумента, а именно:
name: название для Slice
initialState: начальное состояние Slice (похоже на начальное состояние редуктора)
reducers: функции, которые влияют на состояния (действия)
Короче говоря, Slice подобен модульному сборщику состояний и их действий. Начнем с создания Slice для аутентификации и связанных с ним действий:
JavaScript // auth-slice.js const authSlice = createSlice({ name: ‘Auth’, // Could name it anything initialState: { isLoggedIn: false, }, reducers: { login: (state) => { state.isLoggedIn = true }, logout: (state) => { state.isLoggedIn = false }, }, })
123456789101112131415 | // auth-slice.jsconst authSlice = createSlice({ name: ‘Auth’, // Could name it anything initialState: { isLoggedIn: false, }, reducers: { login: (state) => { state.isLoggedIn = true }, logout: (state) => { state.isLoggedIn = false }, },}) |
Как видите, приведенный выше фрагмент посвящен аутентификации пользователя. initialState- это объект, который содержит разные значения начального состояния и состояния, на которые редукторы должны действовать. Функции в редукторах похожи на обычные редукторы, которые принимают состояние и действие в качестве аргументов.
Одна заметная загвоздка в приведенном выше фрагменте — это то, как мы имеем дело с обновлением состояния state.isLoggedIn = true. Оно явно меняет состояние, нарушая свойство неизменности. Давайте разберемся, что происходит под капотом, прежде чем делать выводы.
На первый взгляд кажется, что мы мутируем существующее состояние. Но изменение существующего состояния не вызовет широковещательную передачу обновленных значений подписчикам.
Поэтому, когда мы используем изменяющийся синтаксис в редукторах, createSlice использует библиотеку под названием Immer. Эта библиотека отображает разницу в существующем и обновленном состоянии и реконструирует новый объект с обновленным значением.
Это означает, что когда мы изменяем состояние, Immer заботится о создании нового объекта и его соответствии свойству неизменяемости, что упрощает написание кода. Теперь давайте напишем Slice-ы для других состояний:
JavaScript // cart-slice.js const cartSlice = createSlice({ name: ‘Cart’, initialState: { cart: [], qty: 0, total: 0, }, reducers: { addItem: (state, action) => { state.cart.push(action.payload.cartItem) state.qty += action.payload.cartItem.qty state.total += action.payload.cartItem.price * action.payload.cartItem.qty }, removeItem: (state, action) => { const itemIndex = state.cart.findIndex( (obj) => obj.id === action.payload.id ) if (itemIndex !== -1 && state.cart[itemIndex].qty >= 1) { state.cart[itemIndex].qty -= 1 } } } })
123456789101112131415161718192021222324 | // cart-slice.jsconst cartSlice = createSlice({ name: ‘Cart’, initialState: { cart: [], qty: 0, total: 0, }, reducers: { addItem: (state, action) => { state.cart.push(action.payload.cartItem) state.qty += action.payload.cartItem.qty state.total += action.payload.cartItem.price * action.payload.cartItem.qty }, removeItem: (state, action) => { const itemIndex = state.cart.findIndex( (obj) => obj.id === action.payload.id ) if (itemIndex !== -1 && state.cart[itemIndex].qty >= 1) { state.cart[itemIndex].qty -= 1 } } }}) |
Основная часть здесь — это логика, связанная с добавлением и удалением товаров из корзины. Мы следуем аналогичной логике для списка наблюдения, но без общего количества и общей стоимости:
React JS. Основы
Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения
Получить курс сейчас!
JavaScript // watchlist-slice.js const watchlistSlice = createSlice({ name: ‘Watchlist’, initialState: { watchlist: [], }, reducers: { addItem: (state, action) => { state.watchlist.push(action.payload.watchlistItem) }, removeItem: (state, action) => { state.watchlist.filter((item) => item.id !== action.payload.id) } } })
123456789101112131415 | // watchlist-slice.jsconst watchlistSlice = createSlice({ name: ‘Watchlist’, initialState: { watchlist: [], }, reducers: { addItem: (state, action) => { state.watchlist.push(action.payload.watchlistItem) }, removeItem: (state, action) => { state.watchlist.filter((item) => item.id !== action.payload.id) } }}) |
Обратите внимание, как доступ к элементу действия осуществляется через свойство payload, а не напрямую. Это связано с тем, что Redux Toolkit добавляет свойство payload для action по умолчанию. Рayload- это объект, который может дополнительно содержать другие вложенные объекты или простые пары ключ-значение. Он в основном содержит аргументы, переданные диспетчеру действий.
Теперь вы можете спросить себя, как мы на самом деле собираемся отправлять действия? До использования Redux Toolkit мы выполняли действия на основе свойства type действия. Затем, в зависимости от type мы выполняли разные операции.
Но здесь мы не используем никаких свойств type, потому что мы будем экспортировать функции редуктора как действия, которые затем можно будет вызывать в диспетчере. Функции-редукторы можно экспортировать как действия, вызвав свойство actions в созданном slice:
JavaScript export const authActions = authSlice.actions export const cartActions = cartSlice.actions export const wathclistActions = watchlistSlice.actions
12345 | export const authActions = authSlice.actions export const cartActions = cartSlice.actions export const wathclistActions = watchlistSlice.actions |
Все фрагменты здесь не связаны напрямую друг с другом, поэтому безопасно и полезно хранить их в отдельных файлах. Если вы добавляете фрагменты в разные файлы, обязательно экспортируйте их.
Как объединить slice в одно хранилище
В хранилище может быть только один редуктор, поэтому важно объединить все slice и их редукторы в один редуктор без потери его идентичности и функциональности. Для достижения этой цели мы будем использовать configureStore для createStore.
configureStore похож на createStore, но он может объединять редукторы нескольких slice в один редуктор. У него есть объект reducer, который принимает один или несколько slice, например:
JavaScript import { authSlice } from ‘./auth-slice’ import { cartSlice } from ‘./cart-slice’ import { wathclistSlice } from ‘./watchlist-slice’ const store = configureStore({ reducer: { authSliceReducer: authSlice.reducer, cartSliceReducer: cartSlice.reducer, watchlistSliceReducer: watchlist.reducer, }, }) export defualt store
12345678910111213 | import { authSlice } from ‘./auth-slice’import { cartSlice } from ‘./cart-slice’import { wathclistSlice } from ‘./watchlist-slice’ const store = configureStore({ reducer: { authSliceReducer: authSlice.reducer, cartSliceReducer: cartSlice.reducer, watchlistSliceReducer: watchlist.reducer, },}) export defualt store |
Это настраивает хранилище для его использования несколькими компонентами приложения.
Как использовать состояния Redux в компонентах
Теперь, когда Redux хранилище готово, мы можем использовать и направлять actions из компонентов, используя хуки useSelector и useDispatch. Вот простой пример:
JavaScript // Component.jsx import { useDispatch, useSelector } from ‘react-redux’ import { cartActions } from ‘…’ export default function Cart(props) { const dispatch = useDispatch() const selector = useSelector((state) => state.watchlistSliceReducer) // Since the store has multiple reducers, we need to drill into the appropriate slice state. const addToCartHandler = () => { const dummyitem = { id: Math.random(), name: `Dummy Item ${Math.random()}`, price: 20 * Math.random(), } dispatch(cartActions.addItem(cartItem.dummyitem)) } const removeFromCartHandler = (id) => { dispatch(cartActions.removeItem(id)) //Passing id as an argument to the reducer function. } return ( <div> {selector.cart.length && selector.cart.map((item) => { return ( <div> <p>Name: {item.name}</p> <p>Price: {item.price}</p> <p>Quantity: {item.qty}</p> <button onClick={removeFromCartHandler}>Remove item</button> </div> ) })} <h3>Total cart value:{selector.cart.total}</h3> <button onClick={addToCartHandler}>Add dummy item</button> </div> ) }
1234567891011121314151617181920212223242526272829303132333435363738 | // Component.jsximport { useDispatch, useSelector } from ‘react-redux’import { cartActions } from ‘…’ export default function Cart(props) { const dispatch = useDispatch() const selector = useSelector((state) => state.watchlistSliceReducer) // Since the store has multiple reducers, we need to drill into the appropriate slice state. const addToCartHandler = () => { const dummyitem = { id: Math.random(), name: `Dummy Item ${Math.random()}`, price: 20 * Math.random(), } dispatch(cartActions.addItem(cartItem.dummyitem)) } const removeFromCartHandler = (id) => { dispatch(cartActions.removeItem(id)) //Passing id as an argument to the reducer function. } return ( <div> {selector.cart.length && selector.cart.map((item) => { return ( <div> <p>Name: {item.name}</p> <p>Price: {item.price}</p> <p>Quantity: {item.qty}</p> <button onClick={removeFromCartHandler}>Remove item</button> </div> ) })} <h3>Total cart value:{selector.cart.total}</h3> <button onClick={addToCartHandler}>Add dummy item</button> </div> )} |
Как обрабатывать асинхронный код с помощью Action Creators
Еще одна важная тема, которую мы не рассмотрели — как обрабатывать побочные эффекты или асинхронный код с помощью Redux.
Рассмотрим сценарий, в котором вы хотите отправить действие, которое должно обрабатывать блок кода, вызывающий побочный эффект. Но в то же время редукторы должны быть чистыми, без побочных эффектов и синхронными.
Это означает, что добавление любого кода в редукторы, вызывающего побочные эффекты, противоречит основным принципам Redux и очень плохо.
Чтобы исправить это, у нас есть два варианта: либо использовать useEffect / componentDidMount, либо написать action creators.
Как справиться с побочными эффектами с помощью useEffect или componentDidMount
Один из способов справиться с кодом, создающим побочные эффекты, — использовать useEffect. Делая это, мы отделяем код, создающий побочный эффект, от самого отправленного действия, поэтому редукторы остаются чистыми и синхронными.
Но одним из основных недостатков использования useEffect является избыточность и дублирование кода. Если есть два или более компонента, которые производят один и тот же побочный эффект, мы хотели бы, чтобы одна и та же логика выполнялась в хуке useEffect, что является плохой практикой.
Один из быстрых обходных путей — поместить логику во вспомогательную функцию и запустить эту функцию в корневом компоненте, а все остальные компоненты будут прослушивать изменения через состояние Redux.
Это было бы допустимо и не обязательно плохой практикой, но еще лучше было бы использовать преобразователь для создания действий.
Как справиться с побочными эффектами с помощью Action Creators
Преобразователь — это в основном функция, возвращающая другую функцию, которая не вызывается немедленно. Фактически, мы все это время бессознательно использовали создателей действий, когда отправляли действия. Это потому, что Redux Toolkit абстрагирует все это от нас. Что действительно происходит под капотом, так это то, что эта функция возвращает объект action, который соответствует функции редуктора. Например, когда мы делаем это:
JavaScript function dispatchActions(args) { return { type: ‘UNIQUE’, payload: { …args } } }
123 | function dispatchActions(args) { return { type: ‘UNIQUE’, payload: { …args } }} |
Метод dispatchActions(…) возвращает объект action со свойствами type и payload. Грубо говоря, функция dispatchActions() будет примерно такой:
JavaScript export const actionCreatorThunk = async(args) => { return (dispatch) => { // async code here dispatch(actions.actionDispatcher(args)) // more async code // more dispatch functions } }
12345678 | export const actionCreatorThunk = async(args) => { return (dispatch) => { // async code here dispatch(actions.actionDispatcher(args)) // more async code // more dispatch functions }} |
type: ‘UNIQUE’ является заполнителем, но внутренне уникальный идентификатор назначается различным диспетчерам actions, которые затем подключаются к своим соответствующим функциям редуктора.
Таким образом, dispatch(actions.dispatchActions(args)) фактически означает, dispatch({ type: ‘UNIQUE_ID’, args: args }). Это также должно прояснить, почему свойство payload прикреплено к диспетчеру действий.
Таким образом, преобразователи похожи на action creator, определяемого пользователем, который возвращает функцию вместо объекта действия. Преобразователи для action creator — это автономные функции, а не функция-редуктор, поэтому мы можем писать там асинхронный код.
Преобразователь действия — это функция, которая принимает аргументы, переданные пользователем, и возвращает функцию, которая дополнительно принимает аргумент dispatch, переданный Redux Toolkit за нас. И именно Redux Toolkit позже вызывает эту возвращаемую функцию.
Стандартный код преобразователя для action creator будет выглядеть примерно так:
JavaScript export const actionCreatorThunk = async(args) => { return (dispatch) => { // async code here dispatch(actions.actionDispatcher(args)) // more async code // more dispatch functions } }
12345678 | export const actionCreatorThunk = async(args) => { return (dispatch) => { // async code here dispatch(actions.actionDispatcher(args)) // more async code // more dispatch functions }} |
Возвращенная функция также может быть async функцией, потому что ясно, что она обрабатывает другой async код. Action creators могут иметь несколько dispatch функций, отправляющих несколько dispatch действий. Преобразователи action creator могут быть отправлены следующим образом:
JavaScript import {actionCreatorThunk} from ‘…’ import {useDispatch} from ‘react-redux’ export default function Component(args){ const dispatch = useDispatch() dispatch(actionCreatorThunk(dataToBePassed)) … … … }
12345678910 | import {actionCreatorThunk} from ‘…’import {useDispatch} from ‘react-redux’export default function Component(args){ const dispatch = useDispatch() dispatch(actionCreatorThunk(dataToBePassed)) … … … } |
Преимущество Redux Toolkit в том, что он не только принимает объекты actions, возвращаемые функциями редуктора, но также принимает функции, возвращаемые action creators.
Когда определяется, что вместо объекта action возвращается функция, Redux Toolkit автоматически вызывает возвращаемую функцию и передает функцию диспетчеризации в качестве аргумента.
Мы можем использовать action creators в местах, где выполняются сетевые вызовы, либо для POST, либо для запроса данных из БД, а затем устанавливать состояние Redux из отправленных / полученных данных. Это обеспечивает правильную координацию между серверной и интерфейсной системами.
Заключение
Если вы дочитали до сих пор, спасибо. Я очень ценю, что вы нашли время прочитать до конца. Вкратце, синхронный код и код без побочных эффектов должен идти в редукторах, в то время как асинхронный код должен использоваться в action creators или обработчиках побочных эффектов, таких как useEffect.
Это все. Надеюсь, статья помогла Вам узнать что-то новое о написании лучшего кода Redux для управления состоянием всего приложения. Спасибо!
Автор: Prajwal Kulkarni
Источник: webformyself.com