react-spa-template/docs/architecture.md
2025-04-19 19:24:27 +02:00

4.0 KiB

Architecture: Flux-style with Effector

1. Terminology & Core Concepts

Term Definition
SPA (Single-Page Application) Entire UI is boot-strapped once in the browser; further navigation is handled client-side.
Client-Side Rendering (CSR) The browser runs JavaScript (React) that turns JSON/state into DOM; no HTML is streamed from the server after first load.
Effector A small TypeScript-first library that implements Flux-style one-way data-flow with four primitives (event, store, effect, domain).
Flux-style architecture Events mutate stores ➜ stores notify subscribed views; data always moves in one direction.
Effector-based Flux architecture Using Effector as the single source of truth for state inside a React SPA.

2. Effector Primitives → Flux Mapping

createEvent()   ≈ Action
(create)Store() ≈ Store
createEffect()  ≈ Async side-effect handler
Domains         ≈ Namespace / module boundary
View            ≈ React component using `useStore` / `useUnit`

Flow diagram:

event  ─┐
        │ triggers
store ──┤ update → React re-render
        │
effect ←┘ (async; dispatches success/fail events)

3. Project Layout

src/
  app/
    root.tsx          # <App/>, routing
    routes.tsx        # Route definitions
  domains/
    <feature>/
      model.ts        # Effector events, stores, effects     (pure, testable)
      view-model.ts   # Thin hooks that adapt model to React
      ui/             # Dumb presentational components
  shared/
    ui/               # Design-system atoms
    lib/              # Generic helpers
  tests/              # Test files

4. Coding Guidelines

Guideline Why it helps
TypeScript strict mode Types give static guarantees for safe edits.
Pure functions in model.ts Deterministic behavior is easier to reason about.
Named exports only Eliminates default-export rename hazards.
Small files, single responsibility Enables chunked, parallel code transforms.
Unit tests co-located with domain logic Provide regression oracles for automated changes.
Runtime schema checks (e.g., Zod) Fail fast on unexpected data.

5. Implementation Example

model.ts

import { createStore, createEvent, createEffect } from 'effector';
import { ProductsViewsService } from '@lib/api/merchant/services/ProductsViewsService';
import type { productCompositeDto } from '@lib/api/merchant/models/productCompositeDto';

// Define events
export const setProductsFilter = createEvent<Partial<productQueryRequestDto>>();

// Define effects
export const fetchProductsFx = createEffect(async (filter: productQueryRequestDto) => {
  return await ProductsViewsService.postApiProductCompositeQueryByCanonical({
    requestBody: filter
  });
});

// Create stores
export const $products = createStore<productCompositeDto[]>([])
  .on(fetchProductsFx.doneData, (_, payload) => Array.isArray(payload) ? payload : [payload]);

view-model.ts

import { useStore } from 'effector-react';
import { useCallback } from 'react';
import {
  $products,
  $productsLoading,
  setProductsFilter,
  fetchProductsFx
} from './model';

export const useProducts = () => {
  const products = useStore($products);
  const isLoading = useStore($productsLoading);
  
  const updateFilter = useCallback((newFilter) => {
    setProductsFilter(newFilter);
  }, []);

  return {
    products,
    isLoading,
    updateFilter
  };
};

UI Component

import React from 'react';
import { useProducts } from '../view-model';
import { ProductCard } from './ProductCard';

export const ProductsList: React.FC = () => {
  const { products, isLoading } = useProducts();
  
  if (isLoading) {
    return <div>Loading...</div>;
  }
  
  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
};