Статьи

Как заменить Redux на React Hooks и контекстный API

Самый популярный способ обработки общего состояния приложения в React — использование такой среды, как Redux. Совсем недавно команда React представила несколько новых функций, в том числе React Hooks и Context API. Эти две функции эффективно устраняют множество проблем, с которыми сталкиваются разработчики крупных проектов React. Одной из самых больших проблем было «пропеллерное бурение», которое было характерно для вложенных компонентов. Решением было использование библиотеки управления состоянием, такой как Redux. К сожалению, это произошло за счет написания стандартного кода, но теперь можно заменить Redux на React Hooks и Context API.

В этой статье вы познакомитесь с новым способом обработки состояний в ваших проектах React без написания избыточного кода или установки нескольких библиотек — как в случае с Redux. Ловушки React позволяют вам использовать локальное состояние внутри компонентов функции, в то время как Context API позволяет вам делиться состоянием с другими компонентами.

Предпосылки

Чтобы следовать этому уроку, вам необходимо иметь хорошую основу в следующих темах:

Техника, которую вы изучите здесь, основана на шаблонах, которые были представлены в Redux. Это означает, что вы должны иметь четкое представление о reducers и actions прежде чем продолжить. В настоящее время я использую Visual Studio Code , который сейчас кажется самым популярным редактором кода (особенно для разработчиков JavaScript). Если вы используете Windows, я бы порекомендовал вам установить Git Bash . Используйте терминал Git Bash для выполнения всех команд, представленных в этом руководстве. Cmder также является хорошим терминалом, способным выполнять большинство команд Linux в Windows.

Вы можете получить доступ ко всему проекту, используемому в этом руководстве, из этого репозитория GitHub .

О новой методике государственного управления

Есть два типа состояний, с которыми нам нужно иметь дело в проектах React:

  • местное государство
  • глобальное состояние

Локальные состояния могут использоваться только внутри созданных компонентов. Глобальные состояния могут быть разделены между несколькими компонентами. В любом случае, это два способа объявления и обработки состояния с использованием перехватчиков React:

  • useState
  • useReducer

useState рекомендуется для обработки простых значений, таких как числа или строки. Однако, когда дело доходит до обработки сложных структур данных, вам нужно будет использовать useReducer . В отличие от useState который поставляется только с функцией useReducer хук useReducer позволяет вам указать столько функций, сколько вам нужно. Например, состояние массива объектов потребует как минимум функций для добавления, обновления и удаления элемента.

Как только вы объявите свое состояние, используя useState или useReducer , вы можете поднять его до глобального, используя React Context . Это технология, которая позволит вам обмениваться значениями между компонентами, не пропуская реквизиты. Когда вы объявляете объект контекста, он служит поставщиком для других компонентов, которые могут потреблять и подписываться на изменения контекста. Вы можете добавить к поставщику столько потребителей компонентов, сколько захотите. Общее состояние автоматически синхронизируется со всеми подписанными компонентами.

Давайте начнем создавать проект, чтобы вы могли получить практические знания о том, как все это работает.

Настройка проекта

Самый простой способ создать проект — использовать инструмент create-реагировать на приложение . Тем не менее, инструмент устанавливает множество зависимостей разработки, которые занимают много места на диске. В результате, установка сервера занимает больше времени и больше времени. Если вы не возражаете против незначительных проблем, вы можете создать новый проект React с помощью этого инструмента. Вы можете назвать это react-hooks-context-demo .

Еще один способ создания нового проекта React — клонирование начального проекта, настроенного для использования Parcel JS в качестве компоновщика. Этот метод занимает как минимум на 50% меньше дискового пространства и запускает сервер разработки быстрее, чем инструмент create-react-app . Я создал один специально для учебников React, таких как этот. Я бы порекомендовал вам сначала создать полностью пустой GitHub-репозиторий в своей учетной записи, прежде чем приступить к выполнению следующих инструкций:

 $ git clone [email protected]:brandiqa/react-parcel-starter.git react-hooks-context-demo $ cd react-hooks-context-demo $ git remote rm origin # Replace `username` and `repositoryName` with yours $ git remote add origin [email protected]:username/repositoryName.git $ git config master.remote origin $ git config master.merge refs/heads/master $ git push -u origin master # Install dependencies $ npm install 

После того, как вы выполнили все вышеперечисленные инструкции, вы можете использовать команду npm start для запуска сервера dev. Вам нужно будет запустить браузер и перейти на страницу localhost: 1234 .

01-реагировать-посылку-стартер

Если вы использовали инструмент create-react-app , он, конечно, будет выглядеть по-другому. Это нормально, так как мы изменим представление по умолчанию на следующем шаге. Если ваш проект запущен нормально, вы можете перейти к следующему разделу.

Установка библиотеки пользовательского интерфейса

Этот шаг не является необходимым для этой темы. Однако мне всегда нравится создавать чистые и красивые интерфейсы с наименьшими усилиями. Для этого урока мы будем использовать Semantic UI React . Поскольку это руководство по управлению состоянием, я не буду объяснять, как работает библиотека. Я только покажу вам, как его использовать.

 npm install semantic-ui-react semantic-ui-css 

Откройте index.js и вставьте следующий импорт:

 import 'semantic-ui-css/semantic.min.css'; import './index.css'; 

Это все, что нам нужно сделать, чтобы наш проект начал использовать Semantic UI. Давайте начнем работать с первым примером, демонстрирующим эту новую технику управления состоянием.

Пример счетчика

В этом примере мы создадим простую демонстрационную версию счетчика, состоящую из двух кнопок и кнопки отображения. Чтобы продемонстрировать глобальное состояние, этот пример будет состоять из двух презентационных компонентов. Во-первых, нам нужно определить наш контекстный объект, в котором будет жить состояние. Это похоже на store в Redux. Создание нашего контекстного кода, который будет использоваться для этой цели, потребует небольшого количества стандартного кода, который необходимо будет дублировать в каждом проекте. К счастью, кто-то уже написал специальный хук для этого, который позволит вам создать свой контекстный объект в одну строку. Просто установите пакет constate :

 npm install constate 

После установки вы сможете продолжить. Я разместил комментарии в коде, чтобы объяснить, что происходит. Создайте файл объекта context/CounterContext.js хранилища context context/CounterContext.js и вставьте этот код:

 import { useState } from "react"; import createUseContext from "constate"; // State Context Object Creator // Step 1: Create a custom hook that contains your state and actions function useCounter() { const [count, setCount] = useState(0); const increment = () => setCount(prevCount => prevCount + 1); const decrement = () => setCount(prevCount => prevCount - 1); return { count, increment, decrement }; } // Step 2: Declare your context state object to share the state with other components export const useCounterContext = createUseContext(useCounter); 

Создайте родительский компонент views/Counter.jsx и вставьте этот код:

 import React from "react"; import { Segment } from "semantic-ui-react"; import CounterDisplay from "../components/CounterDisplay"; import CounterButtons from "../components/CounterButtons"; import { useCounterContext } from "../context/CounterContext"; export default function Counter() { return ( // Step 3: Wrap the components you want to share state with using the context provider <useCounterContext.Provider> <h3>Counter</h3> <Segment textAlign="center"> <CounterDisplay /> <CounterButtons /> </Segment> </useCounterContext.Provider> ); } 

Создайте компоненты components/CounterDisplay.jsx и вставьте этот код:

 import React from "react"; import { Statistic } from "semantic-ui-react"; import { useCounterContext } from "../context/CounterContext"; export default function CounterDisplay() { // Step 4: Consume the context to access the shared state const { count } = useCounterContext(); return ( <Statistic> <Statistic.Value>{count}</Statistic.Value> <Statistic.Label>Counter</Statistic.Label> </Statistic> ); } 

Создайте компоненты components/CounterButtons.jsx и вставьте этот код:

 import React from "react"; import { Button } from "semantic-ui-react"; import { useCounterContext } from "../context/CounterContext"; export default function CounterButtons() { // Step 4: Consume the context to access the shared actions const { increment, decrement } = useCounterContext(); return ( <div> <Button.Group> <Button color="green" onClick={increment}> Add </Button> <Button color="red" onClick={decrement}> Minus </Button> </Button.Group> </div> ); } 

Замените код в App.jsx следующим:

 import React from "react"; import { Container } from "semantic-ui-react"; import Counter from "./views/Counter"; export default function App() { return ( <Container> <h1>React Hooks Context Demo</h1> <Counter /> </Container> ); } 

Страница вашего браузера должна иметь следующий вид. Нажмите на кнопки, чтобы убедиться, что все работает:

02-контр-демонстрационный ,

Надеюсь, этот пример имеет смысл — прочитайте комментарии, которые я включил. Давайте перейдем к следующему разделу, где мы настроим пример, который немного более продвинутый.

Пример контактов

В этом примере мы создадим базовую страницу CRUD для управления контактами. Он будет состоять из пары презентационных компонентов и контейнера. Также будет объект контекста для управления состоянием контактов. Поскольку наше дерево состояний будет немного сложнее, чем в предыдущем примере, нам придется использовать хук useReducer .

Создайте контекст объекта context/ContactContext.js и вставьте этот код:

 import { useReducer } from "react"; import _ from "lodash"; import createUseContext from "constate"; // Define the initial state of our app const initialState = { contacts: [ { id: "098", name: "Diana Prince", email: "[email protected]" }, { id: "099", name: "Bruce Wayne", email: "[email protected]" }, { id: "100", name: "Clark Kent", email: "[email protected]" } ], loading: false, error: null }; // Define a pure function reducer const reducer = (state, action) => { switch (action.type) { case "ADD_CONTACT": return { contacts: [...state.contacts, action.payload] }; case "DEL_CONTACT": return { contacts: state.contacts.filter(contact => contact.id != action.payload) }; case "START": return { loading: true }; case "COMPLETE": return { loading: false }; default: throw new Error(); } }; // Define your custom hook that contains your state, dispatcher and actions const useContacts = () => { const [state, dispatch] = useReducer(reducer, initialState); const { contacts, loading } = state; const addContact = (name, email) => { dispatch({ type: "ADD_CONTACT", payload: { id: _.uniqueId(10), name, email } }); }; const delContact = id => { dispatch({ type: "DEL_CONTACT", payload: id }); }; return { contacts, loading, addContact, delContact }; }; // Share your custom hook export const useContactsContext = createUseContext(useContacts); 

Создайте родительский компонент views/Contacts.jsx и вставьте этот код:

 import React from "react"; import { Segment, Header } from "semantic-ui-react"; import ContactForm from "../components/ContactForm"; import ContactTable from "../components/ContactTable"; import { useContactsContext } from "../context/ContactContext"; export default function Contacts() { return ( // Wrap the components that you want to share your custom hook state <useContactsContext.Provider> <Segment basic> <Header as="h3">Contacts</Header> <ContactForm /> <ContactTable /> </Segment> </useContactsContext.Provider> ); } 

Создайте компоненты components/ContactTable.jsx и вставьте этот код:

 import React, { useState } from "react"; import { Segment, Table, Button, Icon } from "semantic-ui-react"; import { useContactsContext } from "../context/ContactContext"; export default function ContactTable() { // Subscribe to `contacts` state and access `delContact` action const { contacts, delContact } = useContactsContext(); // Declare a local state to be used internally by this component const [selectedId, setSelectedId] = useState(); const onRemoveUser = () => { delContact(selectedId); setSelectedId(null); // Clear selection }; const rows = contacts.map(contact => ( <Table.Row key={contact.id} onClick={() => setSelectedId(contact.id)} active={contact.id === selectedId} > <Table.Cell>{contact.id}</Table.Cell> <Table.Cell>{contact.name}</Table.Cell> <Table.Cell>{contact.email}</Table.Cell> </Table.Row> )); return ( <Segment> <Table celled striped selectable> <Table.Header> <Table.Row> <Table.HeaderCell>Id</Table.HeaderCell> <Table.HeaderCell>Name</Table.HeaderCell> <Table.HeaderCell>Email</Table.HeaderCell> </Table.Row> </Table.Header> <Table.Body>{rows}</Table.Body> <Table.Footer fullWidth> <Table.Row> <Table.HeaderCell /> <Table.HeaderCell colSpan="4"> <Button floated="right" icon labelPosition="left" color="red" size="small" disabled={!selectedId} onClick={onRemoveUser} > <Icon name="trash" /> Remove User </Button> </Table.HeaderCell> </Table.Row> </Table.Footer> </Table> </Segment> ); } 

Создайте компоненты components/ContactForm.jsx и вставьте этот код:

 import React, { useState } from "react"; import { Segment, Form, Input, Button } from "semantic-ui-react"; import { useContactsContext } from "../context/ContactContext"; export default function ContactForm() { const name = useFormInput(""); const email = useFormInput(""); // Consume the context store to access the `addContact` action const { addContact } = useContactsContext(); const onSubmit = () => { addContact(name.value, email.value); // Reset Form name.onReset(); email.onReset(); }; return ( <Segment basic> <Form onSubmit={onSubmit}> <Form.Group widths="3"> <Form.Field width={6}> <Input placeholder="Enter Name" {...name} required /> </Form.Field> <Form.Field width={6}> <Input placeholder="Enter Email" {...email} type="email" required /> </Form.Field> <Form.Field width={4}> <Button fluid primary> New Contact </Button> </Form.Field> </Form.Group> </Form> </Segment> ); } function useFormInput(initialValue) { const [value, setValue] = useState(initialValue); function handleChange(e) { setValue(e.target.value); } function handleReset() { setValue(""); } return { value, onChange: handleChange, onReset: handleReset }; } 

Вставьте следующий код в App.jsx соответственно:

 import Contacts from "./views/Contacts"; //... <Container> <h1>React Hooks Context Demo</h1> {/* <Counter /> */} <Contacts /> </Container>; 

После реализации кода страница вашего браузера должна обновиться. Чтобы удалить контакт, сначала нужно выбрать строку, а затем нажать кнопку «Удалить». Чтобы создать новый контакт, просто заполните форму и нажмите кнопку «Новый контакт».

03-контакты-пример

Пройдите код, чтобы убедиться, что вы все понимаете. Прочитайте комментарии, которые я включил в код.

Резюме

Надеюсь, что оба эти примера дают отличное понимание того, как вы можете управлять состоянием общего приложения без Redux. Если бы вы переписали эти примеры без хуков и контекстного API, это привело бы к гораздо большему количеству кода. Вы должны использовать контекстный API только там, где это применимо. Реквизиты должны были быть использованы в этих примерах, если это не учебник.

Вы могли заметить во втором примере, что есть пара неиспользуемых переменных состояния, т.е. loading и error . В качестве проблемы, вы можете продвинуть это приложение дальше, чтобы использовать их. Например, вы можете реализовать ложную задержку и заставить компоненты презентации отображать состояние загрузки. Вы также можете пойти дальше и получить доступ к настоящему удаленному API. Здесь переменная состояния error может пригодиться при отображении сообщений об ошибках.

Единственный вопрос, который вы можете задать себе сейчас: нужен ли Redux для будущих проектов? Один недостаток, который я видел с этой техникой, заключается в том, что вы не можете использовать Redux DevTool Addon для отладки состояния вашего приложения. Тем не менее, это может измениться в будущем с разработкой нового инструмента. Очевидно, как разработчику, вам все равно нужно будет изучить Redux, чтобы поддерживать устаревшие проекты. Если вы начинаете новый проект, вам нужно будет спросить себя и свою команду, действительно ли использование библиотеки управления состоянием необходимо для вашего случая.