4.0 KiB
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>
);
};