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

Редукс

Redux — популярная библиотека управления состоянием, существующая с 2015 года. Она основана на архитектуре Flux и обеспечивает однонаправленный поток данных. В Redux все состояние приложения хранится в одном хранилище, которым управляет набор редюсеров. В хранилище отправляются действия, которые запускают редукторы для обновления состояния.

Одним из ключевых преимуществ Redux является то, что он предоставляет предсказуемое решение для управления состоянием. Поскольку состояние хранится в одном месте и обновляется с помощью чистых функций, легко понять, как будет меняться состояние в ответ на различные действия. Кроме того, Redux имеет большую экосистему промежуточного программного обеспечения и других инструментов, упрощающих интеграцию с другими библиотеками и фреймворками, такими как redux-thunk.

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

Например, давайте взглянем на настройку списка продуктов с помощью Redux.

Во-первых, мы определим действия, которые могут быть отправлены:

// actions.ts
import { Action } from 'redux';

export enum ActionTypes {
  ADD_ITEM = 'ADD_ITEM',
  REMOVE_ITEM = 'REMOVE_ITEM',
}

interface AddItemAction extends Action {
  type: ActionTypes.ADD_ITEM;
  payload: {
    name: string;
    quantity: number;
  };
}

interface RemoveItemAction extends Action {
  type: ActionTypes.REMOVE_ITEM;
  payload: {
    id: number;
  };
}

export type GroceriesAction = AddItemAction | RemoveItemAction;

export const addItem = (name: string, quantity: number): AddItemAction => ({
  type: ActionTypes.ADD_ITEM,
  payload: { name, quantity },
});

export const removeItem = (id: number): RemoveItemAction => ({
  type: ActionTypes.REMOVE_ITEM,
  payload: { id },
});

Далее мы определим форму нашего состояния:

// state.ts
export interface GroceriesState {
  items: {
    id: number;
    name: string;
    quantity: number;
  }[];
}

Затем мы создадим наш редюсер, который будет обрабатывать действия и соответствующим образом обновлять состояние:

// reducer.ts
import { GroceriesAction, ActionTypes } from './actions';
import { GroceriesState } from './state';

const initialState: GroceriesState = {
  items: [],
};

const groceriesReducer = (
  state = initialState,
  action: GroceriesAction,
): GroceriesState => {
  switch (action.type) {
    case ActionTypes.ADD_ITEM: {
      const newItem = {
        id: state.items.length,
        name: action.payload.name,
        quantity: action.payload.quantity,
      };
      return {
        ...state,
        items: [...state.items, newItem],
      };
    }
    case ActionTypes.REMOVE_ITEM: {
      const filteredItems = state.items.filter(
        (item) => item.id !== action.payload.id,
      );
      return {
        ...state,
        items: filteredItems,
      };
    }
    default:
      return state;
  }
};

export default groceriesReducer;

Наконец, мы создадим наш магазин и обернем наше приложение провайдером:

// store.ts
import { createStore } from 'redux';
import groceriesReducer from './reducer';

const store = createStore(groceriesReducer);

export default store;
// App.tsx
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';

const App: React.FC = () => {
  return (
    <Provider store={store}>
      {/* Your app code here */}
    </Provider>
  );
};

export default App;

Теперь мы можем использовать действия addItem и removeItem для управления нашим списком продуктов в нашем приложении:

// GroceriesList.tsx
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { addItem, removeItem } from './actions';
import { GroceriesState } from './state';

const GroceriesList: React.FC = () => {
  const [name, setName] = useState('');
  const [quantity, setQuantity] = useState(0);
  const dispatch = useDispatch();
  const items = useSelector((state: GroceriesState) => state.items);

  const handleAddItem = () => {
    dispatch(addItem(name, quantity));
    setName('');
    setQuantity(0);
  };

  const handleRemoveItem = (id: number) => {
    dispatch(removeItem(id));
  };

  return (
    <>
      <h1>Groceries List</h1>
      <div>
        <label>
          Name:
          <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
        </label>
        <label>
          Quantity:
          <input type="number" value={quantity} onChange={(e) => setQuantity(parseInt(e.target.value, 10))} />
        </label>
        <button onClick={handleAddItem}>Add Item</button>
      </div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name} ({item.quantity})
            <button onClick={() => handleRemoveItem(item.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </>
  );
};

export default GroceriesList;

Вот и все! С Redux вы можете увидеть, как дополнительный шаблон и разделение задач могут быть полезны для более крупных приложений. Мой опыт работы с Redux (например, в сочетании с thunk) может затруднить анализ больших приложений, что повлияет на ремонтопригодность; особенно для младших разработчиков.

Отдача

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

Одним из ключевых преимуществ Recoil является то, что он менее многословен, чем Redux, и требует меньше шаблонного кода. Поскольку состояние хранится в атомах и селекторах, им легче управлять и обновлять. Кроме того, Recoil предоставляет ряд полезных хуков для работы с состоянием, таких как useRecoilState и useRecoilValue.

Тем не менее, Recoil по-прежнему является относительно новой библиотекой и не имеет такой большой экосистемы, как Redux. Кроме того, поскольку Recoil спроектирован так, чтобы быть более гибким, может потребоваться более тщательное рассмотрение при разработке решения для управления состоянием для вашего приложения.

Давайте посмотрим на управление списком продуктов в Recoil:

// state.ts
import { atom } from 'recoil';

export interface Item {
  id: number;
  name: string;
  quantity: number;
}

export const groceriesState = atom<Item[]>({
  key: 'groceriesState',
  default: [],
});

Затем мы создадим наши компоненты и будем использовать хуки useRecoilState и useRecoilValue для управления состоянием и доступа к нему:

// GroceriesList.tsx
import React, { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { addItem, groceriesState, removeItem } from './state';

const GroceriesList: React.FC = () => {
  const [name, setName] = useState('');
  const [quantity, setQuantity] = useState(0);
  const [groceries, setGroceries] = useRecoilState(groceriesState);

  const handleAddItem = () => {
    const newItem = {
      id: groceries.length,
      name,
      quantity,
    };
    setGroceries([...groceries, newItem]);
    setName('');
    setQuantity(0);
  };

  const handleRemoveItem = (id: number) => {
    setGroceries(groceries.filter((item) => item.id !== id));
  };

  return (
    <>
      <h1>Groceries List</h1>
      <div>
        <label>
          Name:
          <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
        </label>
        <label>
          Quantity:
          <input type="number" value={quantity} onChange={(e) => setQuantity(parseInt(e.target.value, 10))} />
        </label>
        <button onClick={handleAddItem}>Add Item</button>
      </div>
      <ul>
        {groceries.map((item) => (
          <li key={item.id}>
            {item.name} ({item.quantity})
            <button onClick={() => handleRemoveItem(item.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </>
  );
};

export default GroceriesList;

С Recoil мы можем легко управлять списком продуктов, используя атомы и селекторы, и получать доступ к состоянию с помощью хуков useRecoilState и useRecoilValue. Это обеспечивает более гибкое и менее подробное решение для управления состоянием в нашем приложении.

Контекстный API реакции

React Context API — это встроенное решение для управления состоянием в React. Это позволяет вам создать контекст, к которому могут получить доступ дочерние компоненты в дереве. Это может быть полезным решением для передачи состояния вниз, которое необходимо многим компонентам, но не требует сложности Redux или Recoil.

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

Наконец, давайте взглянем на использование React Context API для управления нашим списком продуктов:

Во-первых, мы создадим наш контекст и определим наше состояние и действия:

// context.ts
import React, { createContext, useContext, useState } from 'react';

export interface Item {
  id: number;
  name: string;
  quantity: number;
}

interface GroceriesContextValue {
  items: Item[];
  addItem: (name: string, quantity: number) => void;
  removeItem: (id: number) => void;
}

export const GroceriesContext = createContext<GroceriesContextValue>({
  items: [],
  addItem: () => {},
  removeItem: () => {},
});

export const useGroceriesContext = (): GroceriesContextValue => useContext(GroceriesContext);

export const GroceriesProvider: React.FC = ({ children }) => {
  const [items, setItems] = useState<Item[]>([]);

  const addItem = (name: string, quantity: number) => {
    const newItem = {
      id: items.length,
      name,
      quantity,
    };
    setItems([...items, newItem]);
  };

  const removeItem = (id: number) => {
    setItems(items.filter((item) => item.id !== id));
  };

  return (
    <GroceriesContext.Provider value={{ items, addItem, removeItem }}>
      {children}
    </GroceriesContext.Provider>
  );
};

Затем мы создадим наши компоненты и обернем их провайдером:

// GroceriesList.tsx
import React, { useState } from 'react';
import { useGroceriesContext } from './context';

const GroceriesList: React.FC = () => {
  const [name, setName] = useState('');
  const [quantity, setQuantity] = useState(0);
  const { items, addItem, removeItem } = useGroceriesContext();

  const handleAddItem = () => {
    addItem(name, quantity);
    setName('');
    setQuantity(0);
  };

  return (
    <>
      <h1>Groceries List</h1>
      <div>
        <label>
          Name:
          <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
        </label>
        <label>
          Quantity:
          <input type="number" value={quantity} onChange={(e) => setQuantity(parseInt(e.target.value, 10))} />
        </label>
        <button onClick={handleAddItem}>Add Item</button>
      </div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name} ({item.quantity})
            <button onClick={() => removeItem(item.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </>
  );
};

export default GroceriesList;

И, наконец, обернем наше приложение провайдером:

// App.tsx
import React from 'react';
import GroceriesList from './GroceriesList';
import { GroceriesProvider } from './context';

const App: React.FC = () => {
  return (
    <GroceriesProvider>
      <GroceriesList />
    </GroceriesProvider>
  );
};

export default App;

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

Заключение

Когда дело доходит до управления состоянием в React, не существует универсального решения. Выбор между Redux, Recoil и React Context API будет зависеть от конкретных потребностей вашего приложения. Если вы создаете очень сложное приложение с большим количеством состояний, Redux может быть лучшим выбором. Если вы ищете более гибкое и менее многословное решение, Recoil может подойти лучше. И если вы создаете более простое приложение, которому просто нужно передать какое-то состояние, вам может подойти React Context API. Независимо от того, какое решение вы выберете, наличие хорошего решения для управления состоянием поможет обеспечить масштабируемость, удобство сопровождения и понятность вашего приложения.