Container Pattern

Container Pattern в Haunted представляет собой подход к организации компонентов, при котором логика управления состоянием и данные отделяются от представления. Этот паттерн широко используется для повышения масштабируемости и тестируемости приложений на Web Components с использованием Haunted.


Структура Container Pattern

Container обычно выполняет три основные функции:

  1. Управление состоянием Container отвечает за хранение и обновление данных, которые используются компонентами-представлениями. В Haunted это чаще всего реализуется с помощью хука useState или useReducer.

  2. Связь с внешними источниками данных Container может получать данные из API, локального хранилища или глобального состояния. Для асинхронных операций используется useEffect для подписки и синхронизации состояния.

  3. Передача состояния и действий дочерним компонентам Container не занимается визуализацией напрямую. Он передает данные и методы управления через пропсы дочерним компонентам, которые отвечают исключительно за рендеринг.


Пример базового Container

import { html, component, useState, useEffect } from 'haunted';
import { TodoList } from './TodoList.js';

function TodoContainer() {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    fetch('/api/todos')
      .then(res => res.json())
      .then(setTodos);
  }, []);

  const addTodo = (text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    setTodos([...todos, newTodo]);
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  return html`
    <${TodoList} 
      .todos=${todos} 
      .on Add=${addTodo} 
      .onTog gle=${toggleTodo} 
    />
  `;
}

customElements.define('todo-container', component(TodoContainer));

Разбор примера:

  • useState используется для управления локальным состоянием списка задач.
  • useEffect обеспечивает загрузку данных при монтировании компонента.
  • Методы addTodo и toggleTodo передаются в дочерний компонент через пропсы.
  • Дочерний компонент (TodoList) отвечает только за визуализацию и обработку событий интерфейса.

Преимущества Container Pattern

  1. Разделение ответственности Логика данных полностью отделена от UI. Это упрощает сопровождение и тестирование.

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

  3. Легкость тестирования Container можно протестировать отдельно, подставляя фиктивные данные и проверяя корректность методов управления состоянием.

  4. Упрощение масштабирования В крупных приложениях добавление новых источников данных или изменение логики не требует вмешательства в UI-компоненты.


Расширение Container Pattern

Для более сложных сценариев Container может использовать:

  • useReducer для управления сложными состояниями с множеством действий.
  • Контекстные объекты (Context API) для передачи состояния на несколько уровней вложенности без “проброса пропсов”.
  • Асинхронные операции с обработкой ошибок через хуки useEffect и отдельные методы.

Пример с useReducer:

import { html, component, useReducer, useEffect } from 'haunted';
import { TodoList } from './TodoList.js';

const initialState = { todos: [], loading: true, error: null };

function reducer(state, action) {
  switch (action.type) {
    case 'SET_TODOS':
      return { ...state, todos: action.payload, loading: false };
    case 'ADD_TODO':
      return { ...state, todos: [...state.todos, action.payload] };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
        )
      };
    case 'SET_ERROR':
      return { ...state, error: action.payload, loading: false };
    default:
      return state;
  }
}

function TodoContainer() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    fetch('/api/todos')
      .then(res => res.json())
      .then(todos => dispatch({ type: 'SET_TODOS', payload: todos }))
      .catch(err => dispatch({ type: 'SET_ERROR', payload: err }));
  }, []);

  const addTodo = (text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    dispatch({ type: 'ADD_TODO', payload: newTodo });
  };

  const toggleTodo = (id) => dispatch({ type: 'TOGGLE_TODO', payload: id });

  return html`
    <${TodoList} 
      .todos=${state.todos} 
      .loading=${state.loading} 
      .error=${state.error} 
      .on Add=${addTodo} 
      .onTog gle=${toggleTodo} 
    />
  `;
}

customElements.define('todo-container', component(TodoContainer));

Рекомендации по применению

  • Разделять UI и бизнес-логику в отдельные компоненты всегда.
  • Использовать Container для работы с внешними API, локальным хранилищем или глобальным состоянием.
  • Переиспользовать UI-компоненты с разными контейнерами для различных источников данных.
  • Обрабатывать ошибки и состояние загрузки в контейнере, чтобы UI оставался простым и предсказуемым.

Container Pattern в Haunted позволяет строить чистые, масштабируемые и легко тестируемые приложения, где каждый компонент выполняет строго определенную роль — либо управление данными, либо визуализацию.