JavaRush /Курсы /Модуль 3: React /Обработка вложенных состояний и вложенных компонентов

Обработка вложенных состояний и вложенных компонентов

Модуль 3: React
2 уровень , 7 лекция
Открыта

Обработка вложенных состояний

Почему вложенные состояния — это вызов?

Когда приложение становится сложнее, наше состояние начинает напоминать многослойный пирог (или слоёный баг, если что-то пошло не так). Например, если мы разрабатываем панель управления пользователями, наше состояние может выглядеть так:

interface UserState {
  id: number;
  name: string;
  preferences: {
    theme: string;
    notifications: boolean;
  };
  projects: Array<{
    id: number;
    title: string;
    completed: boolean;
  }>;
}

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

Подходы к работе с вложенными состояниями

1. Обновление с помощью глубокого копирования

Если нам нужно изменить один вложенный уровень, важным правилом является не мутировать состояние напрямую. Мы создаём копии каждого уровня, который затрагивает:

function userReducer(state: UserState, action: Action): UserState {
  switch (action.type) {
    case 'UPDATE_THEME':
      return {
        ...state,
        preferences: {
          ...state.preferences,
          theme: action.payload,
        },
      };
    case 'TOGGLE_PROJECT_COMPLETED':
      return {
        ...state,
        projects: state.projects.map(project =>
          project.id === action.payload
            ? { ...project, completed: !project.completed }
            : project
        ),
      };
    default:
      return state;
  }
}

Кажется, просто... пока наш объект не станет пятиуровневым монстром. В таких случаях можно использовать immer для работы с иммутабельными данными (https://immerjs.github.io/immer/). Но пока держим всё нативно, чтобы понять, как это работает.

2. Разделение редьюсеров

Когда структура становится слишком сложной, можно разделить её на несколько редьюсеров, каждый из которых управляет своим участком состояния. Это похоже на делегирование задач в команде.

function preferencesReducer(state: Preferences, action: Action): Preferences {
  switch (action.type) {
    case 'UPDATE_THEME':
      return { ...state, theme: action.payload };
    default:
      return state;
  }
}

function projectsReducer(state: Project[], action: Action): Project[] {
  switch (action.type) {
    case 'TOGGLE_PROJECT_COMPLETED':
      return state.map(project =>
        project.id === action.payload
          ? { ...project, completed: !project.completed }
          : project
      );
    default:
      return state;
  }
}

function userReducer(state: UserState, action: Action): UserState {
  return {
    ...state,
    preferences: preferencesReducer(state.preferences, action),
    projects: projectsReducer(state.projects, action),
  };
}

Теперь каждый редьюсер отвечает за свой кусочек состояния. Это делает логику более модульной и контролируемой.

Практический пример: Управление состоянием панели пользователя

Представим, что мы создаём панель управления пользователями. Начнём с настройки состояния:

interface Preferences {
  theme: string;
  notifications: boolean;
}

interface Project {
  id: number;
  title: string;
  completed: boolean;
}

interface UserState {
  id: number;
  name: string;
  preferences: Preferences;
  projects: Project[];
}

const initialState: UserState = {
  id: 1,
  name: 'John Doe',
  preferences: {
    theme: 'light',
    notifications: true,
  },
  projects: [
    { id: 1, title: 'React App', completed: false },
    { id: 2, title: 'TypeScript Guide', completed: true },
  ],
};

Теперь реализуем редьюсеры для управления этим состоянием:

function preferencesReducer(state: Preferences, action: Action): Preferences {
  switch (action.type) {
    case 'UPDATE_THEME':
      return { ...state, theme: action.payload };
    default:
      return state;
  }
}

function projectsReducer(state: Project[], action: Action): Project[] {
  switch (action.type) {
    case 'TOGGLE_PROJECT_COMPLETED':
      return state.map(project =>
        project.id === action.payload
          ? { ...project, completed: !project.completed }
          : project
      );
    default:
      return state;
  }
}

function userReducer(state: UserState, action: Action): UserState {
  return {
    ...state,
    preferences: preferencesReducer(state.preferences, action),
    projects: projectsReducer(state.projects, action),
  };
}

Работа с вложенными компонентами

Вложенные компоненты и доступ к состоянию

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

Решение: использование контекста

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

  1. Создаём контекст:
    import React, { createContext, useReducer, useContext } from 'react';
    
    const UserContext = createContext<[UserState, React.Dispatch<Action>] | undefined>(undefined);
    
  1. Настраиваем провайдер:

    export const UserProvider: React.FC = ({ children }) => {
      const [state, dispatch] = useReducer(userReducer, initialState);
    
      return (
        <UserContext.Provider value={[state, dispatch]}>
          {children}
        </UserContext.Provider>
      );
    };
  2. Используем в компонентах:

    const UserPreferences: React.FC = () => {
      const context = useContext(UserContext);
      if (!context) throw new Error('UserPreferences must be used within UserProvider');
      const [state, dispatch] = context;
    
      return (
        <div>
          <p>Theme: {state.preferences.theme}</p>
          <button onClick={() => dispatch({ type: 'UPDATE_THEME', payload: 'dark' })}>
            Switch to Dark Theme
          </button>
        </div>
      );
    };

Вложенные компоненты с динамическими данными

Что, если у нас есть список компонентов и каждый из них должен быть интерактивным? Например, проекты:

const ProjectsList: React.FC = () => {
  const context = useContext(UserContext);
  if (!context) throw new Error('ProjectsList must be used within UserProvider');
  const [state, dispatch] = context;

  return (
    <ul>
      {state.projects.map(project => (
        <li key={project.id}>
          <span>{project.title}</span>
          <button onClick={() => dispatch({ type: 'TOGGLE_PROJECT_COMPLETED', payload: project.id })}>
            Mark as {project.completed ? 'Incomplete' : 'Complete'}
          </button>
        </li>
      ))}
    </ul>
  );
};

Теперь любой проект может обновляться независимо, а состояние будет сохранено глобально.

Сборка всего приложения

const App: React.FC = () => {
  return (
    <UserProvider>
      <h1>Admin Panel</h1>
      <UserPreferences />
      <ProjectsList />
    </UserProvider>
  );
};

1
Задача
Модуль 3: React, 2 уровень, 7 лекция
Недоступна
Простое редактирование вложенного состояния
Простое редактирование вложенного состояния
1
Задача
Модуль 3: React, 2 уровень, 7 лекция
Недоступна
Управление списком проектов
Управление списком проектов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ