Паттерн Flux и его реализации

Flux — это архитектурный паттерн для построения клиентских приложений с предсказуемым управлением состоянием. Он отличается однонаправленным потоком данных, что упрощает отладку и поддержку крупных приложений. Основные компоненты Flux:

  • Actions — события, описывающие, что произошло в приложении.
  • Dispatcher — центральный регистратор, который получает Actions и направляет их в Stores.
  • Stores — хранилища состояния, реагирующие на Actions и уведомляющие представления о изменениях.
  • Views (Components) — визуальные компоненты, которые подписываются на Stores и обновляются при изменении состояния.

Главная идея — данные всегда текут в одном направлении: Action → Dispatcher → Store → View.


Actions и их структура

Actions представляют собой объекты с типом и полезной нагрузкой (type и payload). В Preact можно создавать Actions через простые функции:

export const addItem = (item) => ({
    type: 'ADD_ITEM',
    payload: item
});

Использование функций для создания Actions упрощает их тестирование и поддержку. Actions не изменяют состояние напрямую, они только описывают, что должно произойти.


Dispatcher в Preact

Dispatcher является центральной шиной, на которую подписаны все Stores. Его задача — получать Actions и уведомлять Stores. В Preact часто используется легковесная реализация:

class Dispatcher {
    constructor() {
        this.callbacks = [];
    }

    register(callback) {
        this.callbacks.push(callback);
    }

    dispatch(action) {
        this.callbacks.forEach(cb => cb(action));
    }
}

export const dispatcher = new Dispatcher();

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


Stores и управление состоянием

Stores содержат состояние приложения и логику его изменения. Они слушают Actions через Dispatcher и уведомляют компоненты о необходимости обновления.

Пример Store в Preact:

import { dispatcher } from './dispatcher';

class TodoStore {
    constructor() {
        this.todos = [];
        this.listeners = [];

        dispatcher.register(this.handleAction.bind(this));
    }

    handleAction(action) {
        switch(action.type) {
            case 'ADD_ITEM':
                this.todos.push(action.payload);
                this.emitChange();
                break;
            case 'REMOVE_ITEM':
                this.todos = this.todos.filter(todo => todo.id !== action.payload.id);
                this.emitChange();
                break;
        }
    }

    emitChange() {
        this.listeners.forEach(listener => listener());
    }

    subscribe(listener) {
        this.listeners.push(listener);
    }

    getTodos() {
        return this.todos;
    }
}

export const todoStore = new TodoStore();

В этом примере каждый Store полностью контролирует своё состояние, а компоненты лишь подписываются на изменения.


Связь Stores и компонентов Preact

В Preact компоненты получают данные из Stores через подписку и обновляются при изменении состояния. Пример компонента TodoList:

import { h } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import { todoStore } from './todoStore';

export const TodoList = () => {
    const [todos, setTodos] = useState(todoStore.getTodos());

    useEffect(() => {
        const update = () => setTodos(todoStore.getTodos());
        todoStore.subscribe(update);
    }, []);

    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>{todo.text}</li>
            ))}
        </ul>
    );
};

Ключевое преимущество — компонент не знает о логике изменения данных, он просто реагирует на обновления Store.


Реализация Action Creators

Для упрощения работы с Actions часто используют Action Creators — функции, которые создают и диспатчат Actions:

import { dispatcher } from './dispatcher';
import { addItem } from './actions';

export const addItemAction = (item) => {
    dispatcher.dispatch(addItem(item));
};

Это позволяет разделять создание и отправку Actions, повышая читаемость кода.


Альтернативные реализации Flux в Preact

Существуют облегчённые библиотеки для Flux-подхода:

  • Redux — популярная реализация Flux с неизменяемым состоянием и строгим разделением reducer’ов.
  • Unistore — минималистичная библиотека, идеально подходящая для Preact.
  • Flux Utils от Facebook — набор утилит для упрощения работы с Stores и Dispatcher.

Использование этих библиотек позволяет сократить шаблонный код и внедрить дополнительные возможности, такие как middleware, асинхронные действия и DevTools.


Однонаправленный поток данных

Основное преимущество Flux — предсказуемость. Поскольку данные всегда движутся по одному пути, легко:

  • Отслеживать источник изменений.
  • Локализовать баги.
  • Писать модульные тесты для Actions и Stores.

Даже при увеличении размера приложения структура остаётся ясной: Action инициирует изменение → Store обрабатывает → View обновляется.


Интеграция с хуками Preact

Использование хуков позволяет сделать Flux-компоненты более декларативными и компактными. Пример с использованием кастомного хука:

import { useState, useEffect } from 'preact/hooks';
import { todoStore } from './todoStore';

export const useTodos = () => {
    const [todos, setTodos] = useState(todoStore.getTodos());

    useEffect(() => {
        const update = () => setTodos(todoStore.getTodos());
        todoStore.subscribe(update);
    }, []);

    return todos;
};

Теперь компоненты могут получать состояние просто через:

const todos = useTodos();

Это делает интеграцию Flux с Preact максимально удобной и идиоматичной.


Хотите, я могу дополнительно сделать схему полного потока данных Flux в Preact с Dispatcher, Stores и компонентами, чтобы визуально закрепить структуру?