Поиск по сайту:

Как начать работу с Redux для управления состоянием JavaScript


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

Что делает Редукс?

Проще говоря, Redux — это централизованное хранилище данных. Все данные вашего приложения хранятся в одном большом объекте. Инструменты разработчика Redux облегчают визуализацию:

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

Делая это таким образом, вы никогда не получите неопределенного поведения с состоянием, изменяющим ваше приложение по желанию. Кроме того, поскольку есть запись о каждом действии и о том, что оно изменило, это позволяет отладку с перемещением во времени, когда вы можете прокручивать состояние вашего приложения назад, чтобы отлаживать то, что происходит с каждым действием (во многом похоже на историю git).

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

Если вы используете TypeScript, добиться строгой типизации Redux будет намного сложнее. Вместо этого вам следует следовать этому руководству, в котором используются typesafe-actions для обработки действий и редьюсеров в удобной для типов форме.

Структурирование вашего проекта

Во-первых, вам нужно создать структуру папок. Это зависит от вас и предпочтений стиля вашей команды, но в основном есть два основных шаблона, которые используются в большинстве проектов Redux. Первый — просто разделить каждый тип файла (действие, редуктор, промежуточное ПО, побочный эффект) в отдельную папку, например:

store/
  actions/
  reducers/
  sagas/
  middleware/
  index.js

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

store/
  features/
    todo/
    etc/
  sagas/
  middleware/
  root-reducer.js
  root-action.js
  index.js

Это очищает импорт, так как теперь вы можете импортировать как действия, так и редукторы в одном операторе, используя:

import { todosActions, todosReducer } from 'store/features/todos'

Вам решать, хотите ли вы сохранить код Redux в отдельной папке (/store в приведенных выше примерах) или интегрировать его в корневую папку src вашего приложения. Если вы уже разделяете код на компоненты и пишете множество настраиваемых действий и редьюсеров для каждого компонента, вы можете объединить /features/ и /components/ папки и хранить компоненты JSX вместе с кодом редуктора.

Если вы используете Redux с TypeScript, вы можете добавить дополнительный файл в каждую папку функций, чтобы определить свои типы.

Установка и настройка Redux

Установите Redux и React-Redux из NPM:

npm install redux react-redux

Вам также, возможно, понадобится redux-devtools:

npm install --save-dev redux-devtools

Первое, что вам нужно создать, — это ваш магазин. Сохраните это как /store/index.js

import { createStore } from 'redux'
import rootReducer from './root-reducer'

const store = createStore(rootReducer)

export default store;

Конечно, ваш магазин усложнится, когда вы добавите такие вещи, как надстройки с побочными эффектами, промежуточное ПО и другие утилиты, такие как connected-react-router, но пока это все, что требуется. Этот файл берет корневой редуктор и вызывает с его помощью createStore(), который экспортируется для использования приложением.

Далее мы создадим простую функцию списка дел. Вы, вероятно, захотите начать с определения действий, требуемых этой функцией, и аргументов, которые им передаются. Создайте папку /features/todos/ и сохраните следующее как types.js:

export const ADD = 'ADD_TODO'
export const DELETE = 'DELETE_TODO'
export const EDIT = 'EDIT_TODO'

Это определяет несколько строковых констант для имен действий. Независимо от того, какие данные вы передаете, каждое действие будет иметь свойство type, представляющее собой уникальную строку, идентифицирующую действие.

Вам не обязательно иметь такой тип файла, так как вы можете просто ввести строковое имя действия, но для совместимости лучше сделать это таким образом. Например, вы можете иметь todos.ADD и reminders.ADD в одном приложении, что избавит вас от необходимости вводить _TODO или _REMINDER каждый раз, когда вы ссылаетесь на действие для этой функции.

Затем сохраните следующее как /store/features/todos/actions.js:

import * as types from './types.js'

export const addTodo = text => ({ type: types.ADD, text })
export const deleteTodo = id => ({ type: types.DELETE, id })
export const editTodo = (id, text) => ({ type: types.EDIT, id, text })

Это определяет несколько действий, используя типы из строковых констант, размещая аргументы и создавая полезную нагрузку для каждого из них. Они не должны быть полностью статичными, так как являются функциями. Например, вы можете задать CUID во время выполнения для определенных действий.

Самый сложный фрагмент кода, в котором вы будете реализовывать большую часть своей бизнес-логики, находится в редьюсерах. Они могут принимать разные формы, но наиболее часто используемая настройка — это оператор switch, который обрабатывает каждый случай в зависимости от типа действия. Сохраните это как reducer.js:

import * as types from './types.js'

const initialState = [
  {
    text: 'Hello World',
    id: 0
  }
]

export default function todos(state = initialState, action) {
  switch (action.type) {
    case types.ADD:
      return [
        ...state,
        {
          id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
          text: action.text
        }
      ]    

    case types.DELETE:
      return state.filter(todo =>
        todo.id !== action.id
      )

    case types.EDIT:
      return state.map(todo =>
        todo.id === action.id ? { ...todo, text: action.text } : todo
      )

    default:
      return state
  }
}

Состояние передается в качестве аргумента, и в каждом случае возвращается модифицированная версия состояния. В этом примере ADD_TODO добавляет в состояние новый элемент (каждый раз с новым идентификатором), DELETE_TODO удаляет все элементы с заданным идентификатором, а EDIT_TODO сопоставляет и заменяет текст элемента с заданным идентификатором.

Начальное состояние также должно быть определено и передано функции редуктора в качестве значения по умолчанию для переменной состояния. Конечно, это не определяет всю вашу структуру состояния Redux, а только раздел state.todos .

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

Когда эта функция завершена, давайте подключим ее к Redux (и к нашему приложению). В /store/root-reducer.js импортируйте todosReducer (и любой другой редьюсер функций из папки /features/ ), затем передайте его в combineReducers( ), формируя один корневой редьюсер верхнего уровня, который передается в хранилище. Здесь вы настроите корневое состояние, убедившись, что каждая функция находится в отдельной ветке.

import { combineReducers } from 'redux';

import todosReducer from './features/todos/reducer';

const rootReducer = combineReducers({
  todos: todosReducer
})

export default rootReducer

Использование Redux в React

Конечно, все это бесполезно, если не связано с React. Для этого вам придется обернуть все приложение в компонент Provider. Это гарантирует, что необходимое состояние и хуки передаются каждому компоненту в вашем приложении.

В App.js или index.js, везде, где у вас есть корневая функция рендеринга, оберните свое приложение в и передайте его магазин (импортированный из /store/index.js) в качестве реквизита:

import React from 'react';
import ReactDOM from 'react-dom';

// Redux Setup
import { Provider } from 'react-redux';
import store, { history } from './store';

ReactDOM.render(
    <Provider store={store}>
       <App/>
    </Provider>
    , document.getElementById('root'));

Теперь вы можете свободно использовать Redux в своих компонентах. Самый простой способ — с функциональными компонентами и хуками. Например, чтобы отправить действие, вы будете использовать хук useDispatch() , который позволяет вам напрямую вызывать действия, например. dispatch(todosActions.addTodo(text)).

В следующем контейнере есть вход, связанный с локальным состоянием React, который используется для добавления новой задачи в состояние при каждом нажатии кнопки:

import React, { useState } from 'react';

import './Home.css';

import { TodoList } from 'components'
import { todosActions } from 'store/features/todos'
import { useDispatch } from 'react-redux'

function Home() {
  const dispatch = useDispatch();
  const [text, setText] = useState("");

  function handleClick() {
    dispatch(todosActions.addTodo(text));
    setText("");
  }

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setText(e.target.value);
  }

  return (
    <div className="App">
      <header className="App-header">

        <input type="text" value={text} onChange={handleChange} />

        <button onClick={handleClick}>
          Add New Todo
        </button>

        <TodoList />
      </header>
    </div>
  );
}

export default Home;

Затем, когда вы хотите использовать данные, хранящиеся в состоянии, используйте хук useSelector . Здесь используется функция, которая выбирает часть состояния для использования в приложении. В этом случае он устанавливает переменную post в текущий список задач. Затем это используется для отображения нового элемента списка задач для каждой записи в state.todos.

import React from 'react';
import { useSelector } from 'store'

import { Container, List, ListItem, Title } from './styles'

function TodoList() {
  const posts = useSelector(state => state.todos)

  return (
    <Container>
      <List>
        {posts.map(({ id, title }) => (
          <ListItem key={title}>
            <Title>{title} : {id}</Title>
          </ListItem>
        ))}
      </List>
    </Container>
  );
}

export default TodoList;

На самом деле вы можете создавать собственные функции селекторов, которые будут обрабатывать это за вас, сохраняя их в папке /features/ во многом подобно действиям и редюсерам.

После того, как вы все настроите и разберетесь, вы можете заняться настройкой Redux Devtools, настройкой промежуточного программного обеспечения, такого как Redux Logger или connected-react-router, или установкой модели побочных эффектов, такой как Redux. Саги.