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

134 lines
4.0 KiB
Markdown

# 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
```typescript
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
```typescript
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
```typescript
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>
);
};
```