Современный фронтенд похож на старую добрую веб-разработку, но с одной загвоздкой: страница в браузере так же сложна, как и бэкенд.
Поскольку мы научились загружать все асинхронно, мы хотим сделать работу пользователя максимально продуктивной и не показывать никакого экрана загрузки. Это означает, что веб-сайт превратился в WebApp.
И самое распространенное в любом WebApp - это управление состояниями.
Весь этот пролог был призван ответить на один вопрос: "Зачем нам состояние во Frontend-приложении?".
В этом посте мы рассмотрим более эффективное управление состояниями в нашей любимой библиотеке React.
Как почти все из вас знают, хуки и наиболее часто используемый хук для управления локальным состоянием: useState
UseState отлично работает во многих сценариях и обеспечивает стабильное решение, не обращаясь к более тяжелым решениям, таким как redux, mobx и т.д.
Давайте рассмотрим небольшой пример базового хука useState:
const { useState } from 'react'; const MyComponent = () => { const [count, setCount] = useState(0); return ( <article> <h1>Count: <code>{count}</code></h1> <button onClick = {() => setCount(i => i++)}>Increase count</button> </article> ) }
Выглядит очень прямолинейно, очень невинно.
И большинство компонентов листа будут выглядеть более или менее похоже, даже без состояния.
Я бы использовал реквизиты в большинстве случаев по сравнению с состоянием. Если вы должны использовать состояние, этот компонент не должен содержать большую часть JSX, только голые части, передающие состояние другим компонентам в качестве реквизитов (конечно, есть исключения).
Но это аргумент для другого раза.
В этом посте мы представим сценарий, в котором мы не хотим внедрять крупномасштабную библиотеку управления состоянием для одного случая использования, но наше состояние становится сложным.
Рассмотрим следующую схему нашего приложения:
Учитывая вышеприведенные схемы, если мы сделаем компонент контроллера, то он будет выглядеть примерно так:
const MyApp = () => { const [shoudlOpenNewDataForm, toggleNewDataForm] = useState(false); const [confirmSave, toggleSaveConirmationModal] = useState(false); const [confirmReset, toggleResetModal] = useState(false); // ... }
Теперь предположим, что нам нужно добавить еще одно взаимодействие, в котором мы хотим разрешить редактирование данных. Для этого нам потребуется открыть ту же форму, но с некоторыми данными, что и форма "Новые данные".
И по завершении корректировки данных мы хотим подтвердить, хочет ли пользователь сохранить новое состояние.
Кроме того, мы хотим разрешить удаление данных из формы редактирования данных.
Это потребует трех дополнительных состояний для обработки этого нового добавления состояний пользовательского интерфейса:
const MyApp = () => { const [shoudlOpenNewDataForm, toggleNewDataForm] = useState(false); const [confirmSave, toggleSaveConirmationModal] = useState(false); const [confirmReset, toggleResetModal] = useState(false); const [dataBeingEdited, setData] = useState({}); const [confirmDelete, toggleDeleteConfirmationModal] = useState(false); // ... }
Все еще достаточно просто?
Давайте предположим состояние приложения, в котором мы хотим позволить пользователям добавлять/обновлять данные дочернего уровня, такие как контроль доступа, разрешения, совместное использование и т.д., создавая/редактируя новые данные.
Для этого потребуется еще несколько дополнительных useState и useRefs
Я могу продолжать добавлять еще несколько сценариев использования, чтобы усложнить состояние.
И произойдет одно: логика контроллера будет становиться все больше и больше подвержена ошибкам.
Давайте не будем доводить ситуацию до безумия и рассмотрим один из способов, с помощью которого мы можем сделать проблему управляемой.
Давайте использоватьReducer
Хук useReducer является частью основной библиотеки, позволяя нам работать с состоянием путем диспетчеризации действий.
Этот хук работает почти так же, как useState.
Давайте рассмотрим небольшой пример:
import { useReducer } from 'react'; const reducer = (state, action) => { switch (action.type) { case "open-modal": return { modal: action.payload.modal }; case "close-modal": return { modal: null }; default: return state; } }; const Modal = ({ close, name }) => ( <div> <h3>{name} Modal</h3> <button onClick = {close}>Close</button> </div> ); export const MyApp = () => { const [state, dispatch] = useReducer(reducer, { modal: null }); const openModal = (modal) => dispatch({ type: "open-modal", payload: { modal }, }); const closeModal = () => dispatch({ type: "close-modal" }); return ( <> {state.modal === "info" && <Modal close = {closeModal} name="Info" />} {state.modal === "confirmation" && ( <Modal close = {closeModal} name="Confirmation" /> )} <button onClick = {() => openModal("info")}>Open Info Modal</button> <button onClick = {() => openModal("confirmation")}> Open Confirmation Modal </button> </> ); };
В приведенном выше примере мы создали Modal для показа некоторого содержимого на основе переданных параметров.
Приведенный выше сценарий можно реализовать многими способами, но цель состоит в том, чтобы продемонстрировать крючок useReducer.
Вот несколько важных элементов useReducer:
Эта функция отвечает за изменение состояния в зависимости от типа действия. Эта функция передается в хук useReducer в качестве первого аргумента. Эта функция должна быть чистой, то есть не должна вызывать никаких побочных эффектов, чтобы ее можно было тестировать независимо.
Const [state, dispatch] = useReducer(reducer, { modal: null });
Это начальное значение передается редуктору при первом выполнении. Оно также является первым значением состояния, возвращаемого useReducer
Const [state, dispatch] = useReducer(reducer, { modal: null });
Хук useReducer возвращает массив:
Const [state, dispatch] = useReducer(reducer, { modal: null });
Это состояние должно использоваться в JSX для определения следующего визуализированного дерева компонентов. Оно генерируется путем выполнения функции reducer на предыдущем производном состоянии.
Const [state, dispatch] = useReducer(reducer, { modal: null });
Это второй элемент возвращаемого массива из useReducer. Эта функция используется для выдачи действий функции reducer.
Const [state, dispatch] = useReducer(reducer, { modal: null });
А здесь мы можем увидеть приведенный выше пример в действии:
Открыть Github GistНаряду с тестированием пользовательского интерфейса, функция reducer должна быть чистой в идеальных сценариях. Это обеспечивает тестируемость функции редуктора.
Поскольку ваша функция для изменения состояния живет в отдельной функции, а не в компоненте контроллера, теперь вы можете перенести ее в отдельный файл и разбить ее на части, чтобы добиться более модульной деривации состояния.
В предыдущем случае необходимо создать пользовательский хук для инкапсуляции всех хуков useState.
Поскольку модульность и тестируемость значительно улучшают читаемость кода, ваш старый компонент становится короче и имеет меньше умственных накладных расходов.
Теперь давайте рефакторим наш начальный пример useState. Поскольку мы еще не определили полностью модель useState, наше решение useReducer теперь будет выглядеть намного больше.
Но для сравнимого решения, масштабируемого и управляемого, более существенным будет useReducer
const removeModal = (modals, modal) => modals.filter((item) => item !== modal); export const reducer = (state, action) => { switch (action.type) { case "init-add": return { ...state, currentlyEditing: {}, modals: [...state.modals, "add-item"] }; case "edit-item": return { ...state, currentlyEditing: action.payload.item, modals: [...state.modals, "add-item"] }; case "init-delete": return { ...state, modals: [...state.modals, "confirm-delete"], currentlyEditing: action.payload.item }; case "delete-item": return { ...state, modals: removeModal(state.modals, "confirm-delete"), currentlyEditing: {}, data: state.data.filter((item) => item.id !== state.currentlyEditing.id) }; case "init-reset": return { ...state, modals: [...state.modals, "confirm-reset"] }; case "reset-editing-item": return { ...state, modals: removeModal(state.modals, "confirm-reset"), currentlyEditing: { ...(state.data.find(({ id }) => state.currentlyEditing.id) || initialState.currentlyEditing) } }; case "show-modal": return { ...state, modals: [...state.modals, action.payload.name] }; case "hide-modal": return { ...state, modals: removeModal(state.modals, action.payload.name) }; case "update-currently-editing": return { ...state, currentlyEditing: { ...state.currentlyEditing, ...action.payload } }; case "save-currently-editing": const { currentlyEditing } = state; const probableNewId = new Date().getTime(); const data = state.data .concat( currentlyEditing.id ? [] : [{ ...currentlyEditing, id: probableNewId }] ) .map((item) => item.id === currentlyEditing.id ? { ...currentlyEditing } : item ); return { ...state, data, currentlyEditing: {}, modals: removeModal(state.modals, "add-item") }; default: return state; } }; export const initialState = { data: [ { name: "A", id: 1 }, { name: "B", id: 2 } ], currentlyEditing: {}, modals: [] };
В приведенном выше примере все изменения приложения не могут быть отражены в изменениях Data; следовательно, код является высокотестируемым и надежным.
Если мы хотим проверить, открыт ли Модал для добавления нового элемента или нет, нам нужно проверить наличие элемента add-item в массиве Modals, давайте рассмотрим пример.
describe('data table reducer', () => { it('should add add-item modal when add is triggered', () => { expect( reducer({}, { type: 'init-add' }).modals ).toEqual(['add-item']); }); it('should add add-item modal when edit is triggered', () => { const action = { type: 'edit-item', payload: { item: {a: 1, b: 2} } }; const updatedState = reducer({}, action); expect(updatedState.modals).toEqual(['add-item']); expect(updatedState.currentlyEditing).toEqual(action.payload.item); }); });
Использовали ли вы useReducer?
Какие советы вы бы хотели иметь в виду при использовании useState и useReducer?
Дайте мне знать в комментариях. или в Twitter @heypankaj_ и/или @time2hack
Если вы нашли эту статью полезной, пожалуйста, поделитесь ею с другими.
Подпишитесь на блог, чтобы получать новые статьи прямо в свой почтовый ящик.
20.08.2023 18:21
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".
20.08.2023 17:46
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
19.08.2023 18:39
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.
19.08.2023 17:22
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!
18.08.2023 20:33
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий их языку и культуре.
14.08.2023 14:49
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.