Контейнеры

  1. Контейнеры - это умные компоненты иными словами контроллеры.

  2. Контейнер знает всё об окружении, сам берёт данные и вызывает действия, направляет данные компонентам и обрабатывает их события.

  3. В контейнере не допускается стилизация разметки на css. Используются компоненты. Допустимы html теги без указания классов.

  4. Создаются в директориях /containers и /app.

    • /app — контейнеры цельных страниц или разделов.
    • /containers — повторно используемые фрагменты страниц или вынесенная из страниц сложная логика.
  5. Именование, функциональный стиль, мемомизация — всё как у компонента.

  6. Контейнерам через props не передаются данные, только параметры, от которых зависит логика контейнера. Например, идентификатор, по которому из стейта будут выбраны все свойства конкретного объекта или вызваны действия.

  7. Логика инициализации реализуется хуком useInit() вместо useEffect(). По умолчанию, этот хук вызывается единственный раз при первом рендере контейнера, если не указана зависимость на параметры. Позволяет вызывать асинхронные действия и учитывает их при серверном рендере. Может отслеживать переходы назад/вперед по истории браузера, когда меняются search параметры адреса. Кроме этого, добавляет ясности в код.

    import useInit from '@src/utils/hooks/use-init';
    //..
    useInit(async () => {
      // Загружаем категории только при первом рендере контейнера
      await categories.load({ limit: 1000 });
    });
    
  8. Избегать каскадов инициализации, когда вложенный контейнер рендерится после инициализации родительского контейнера, и запускает свою инициализацию. Все инициализации должны происходить в контейнере-странице в /app. Контейнеры в /containers должны сразу выбирать данные из стейта.

  9. В /app не должно быть контейнеров без привязки к маршруту навигации (роутингу или другим вариантам). Структура директорий в /app соответствует карте сайта.

  10. В /containers используются поддиректории для группировки контейнеров по общим признакам. Чтобы легче ориентироваться как в директории компонентов.

  11. Декомпозировать тяжелые контейнеры для уменьшения зависимостей рендера. Например, из контейнера страницы каталога выносится в отдельные контейнеры таблица товаров и блок с фильтрами. Каждый контейнер независимо выполняет свою часть работы. При обновлении параметра фильтра, обновится только контейнер фильтров. Подгрузятся новые товары — обновится только контейнер таблицы.

  12. Для выборки данных из redux состояния использовать хук useSelectorMap(state => ({})). Позволяет выбрать множество свойств.

Страница-контейнер "Каталог", привязана к маршруту /catalog/:categoryId

function Catalog(props) {
  
  // Только инициализация каталога. Страница не выводит товары - делегирует вложенным контейнерам
  useInit(async () => {
    // загружаем товары при первом рендере контейнера или когда меняется categoryId в параметре роута (url)
    await articles.init({ categoryId: props.match.params.categoryId });
  }, [props.match.params.categoryId]);

  useInit(async () => {
    // Загружаем категории только при первом рендере контейнера
    await categories.load({ fields: '*', limit: 1000 });
  });

  return (
    <LayoutPage header={<HeaderContainer />}>
      <LayoutContent>
        <CategoryTree />
        <ArticleList />
      </LayoutContent>
    </LayoutPage>
  );
}

Контейнер "Фильтр по категории"

function CategoryTree(){
   // Контейнер дерева категорий только берет катеогрии из стейта и передаёт в компонент  
  const select = useSelectorMap(state => ({
    roots: state.categories.roots,
    wait: state.categories.wait,
  }));
  
  const renders = {
    // Кастомный рендер пункта в дереве
    item: useCallback(item => {
      return <Link to={`/catalog/${item._id}`}>{item.title}</Link>;
    }, [select.roots])
  };

  return (
     <Tree
        items={select.roots}
        renderItem={renders.item}
     />
  );
}

Контейнер "Список товаров"

function ArticleList() {
  // Берем товары и передаём в компонент списка 
  const select = useSelectorMap(state => ({
    items: state.articles.items,
    wait: state.articles.wait,
  }));
  
  const renders = {
    card: useCallback(item => {
      return <ArticleCard data={item}/>;
    }, [select.items])
  };

  return <List items={select.items} renderItem={renders.card}/>
}