├── .husky
├── .gitignore
├── pre-commit
└── commit-msg
├── .gitignore
├── packages
├── reca
│ ├── .npmignore
│ ├── .eslintrc.cjs
│ ├── tests
│ │ ├── fixtures
│ │ │ ├── services
│ │ │ │ ├── TestDIService.ts
│ │ │ │ └── SpaceXService.ts
│ │ │ ├── models
│ │ │ │ └── SpaceXCompanyInfo.ts
│ │ │ ├── components
│ │ │ │ ├── TestDIComponent.tsx
│ │ │ │ ├── TestStoreComponent.tsx
│ │ │ │ ├── TestAutoStoreComponent.tsx
│ │ │ │ ├── SpaceXComponent.tsx
│ │ │ │ ├── TestDIWithPropsComponent.tsx
│ │ │ │ ├── ToDoComponent.tsx
│ │ │ │ └── PerformanceComponent.tsx
│ │ │ └── stores
│ │ │ │ ├── DIStore.ts
│ │ │ │ ├── ToDoAutoStore.ts
│ │ │ │ ├── DIWithPropsStore.ts
│ │ │ │ ├── ToDoStore.ts
│ │ │ │ ├── SpaceXStore.ts
│ │ │ │ ├── LiveCycleStore.ts
│ │ │ │ └── LiveCycleAutoStore.ts
│ │ ├── performance.spec.tsx
│ │ ├── di.spec.tsx
│ │ ├── readme.spec.tsx
│ │ └── livecycle.spec.tsx
│ ├── README.md
│ ├── ts-loader.js
│ ├── src
│ │ ├── decorators
│ │ │ ├── NotRedraw.ts
│ │ │ └── NotAuto.ts
│ │ ├── index.ts
│ │ ├── config.ts
│ │ ├── hooks
│ │ │ ├── UseStore.ts
│ │ │ ├── UseServerStore.ts
│ │ │ └── UseClientStore.ts
│ │ └── stores
│ │ │ ├── Store.ts
│ │ │ └── AutoStore.ts
│ ├── eslint.config.js
│ ├── tsconfig.json
│ ├── tsconfig.build.json
│ ├── LICENSE
│ ├── package.json
│ └── CHANGELOG.md
└── reca-docs
│ ├── app
│ ├── favicon.ico
│ ├── layout.tsx
│ ├── globals.css
│ ├── page.module.css
│ └── page.tsx
│ ├── src
│ └── pages
│ │ └── index
│ │ └── IndexPage.tsx
│ ├── next.config.ts
│ ├── .gitignore
│ ├── eslint.config.js
│ ├── public
│ ├── vercel.svg
│ └── next.svg
│ ├── package.json
│ ├── tsconfig.json
│ └── README.md
├── .commitlintrc.json
├── eslint.config.js
├── tsconfig.json
├── .editorconfig
├── .github
├── workflows
│ ├── test-pr.yml
│ ├── codeql.yml
│ ├── dependabot-auto-merge.yml
│ └── npm-publish.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── PULL_REQUEST_TEMPLATE.md
├── package.json
├── LICENSE
├── SECURITY.md
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── CHANGELOG.md
└── README.md
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx lint-staged --quiet
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | npx commitlint --edit $1
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | coverage
4 | .vscode
--------------------------------------------------------------------------------
/packages/reca/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !dist/**/*
3 | !package.json
4 | !README.md
5 | !LICENSE
--------------------------------------------------------------------------------
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@commitlint/config-conventional"
4 | ]
5 | }
--------------------------------------------------------------------------------
/packages/reca-docs/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LabEG/reca/HEAD/packages/reca-docs/app/favicon.ico
--------------------------------------------------------------------------------
/packages/reca-docs/src/pages/index/IndexPage.tsx:
--------------------------------------------------------------------------------
1 |
2 | export const IndexPage = () => (
3 |
): void {
19 | this.currentTodo = event.currentTarget.value;
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/packages/reca-docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reca-docs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "dev": "next dev --turbopack",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint --fix"
11 | },
12 | "dependencies": {
13 | "react": "^19",
14 | "react-dom": "^19",
15 | "next": "^16.0.10",
16 | "reca": "^2.0.0"
17 | },
18 | "devDependencies": {
19 | "typescript": "^5",
20 | "@types/node": "^25",
21 | "@types/react": "^19",
22 | "@types/react-dom": "^19",
23 | "rimraf": "^6.1.2",
24 | "eslint": "^9",
25 | "eslint-config-next": "16.0.10"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/reca/tests/fixtures/stores/DIWithPropsStore.ts:
--------------------------------------------------------------------------------
1 | import {reflection} from "first-di";
2 | import {Store} from "../../../src/index.js";
3 | import type {ITestDIWithPropsComponent} from "../components/TestDIWithPropsComponent";
4 | import {TestDIService} from "../services/TestDIService";
5 |
6 | @reflection
7 | export class DIWithPropsStore extends Store
{
8 |
9 | public test: number = 0;
10 |
11 | public constructor (
12 | // eslint-disable-next-line @typescript-eslint/parameter-properties
13 | public diService: TestDIService,
14 | props: P
15 | ) {
16 | super();
17 |
18 | this.test = props.test;
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/packages/reca/tests/fixtures/stores/ToDoStore.ts:
--------------------------------------------------------------------------------
1 | import {Store} from "../../../src/index.js";
2 | import type {FormEvent} from "react";
3 |
4 | export class ToDoStore extends Store {
5 |
6 | public currentTodo: string = "";
7 |
8 | public todos: string[] = [];
9 |
10 | public handleAddTodo (): void {
11 | this.todos.push(this.currentTodo);
12 | this.redraw();
13 | }
14 |
15 | public handleDeleteTodo (index: number): void {
16 | this.todos.splice(index, 1);
17 | this.redraw();
18 | }
19 |
20 | public handleCurrentEdit (event: FormEvent): void {
21 | this.currentTodo = event.currentTarget.value;
22 | this.redraw();
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/packages/reca-docs/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type {Metadata} from "next";
2 | import {Inter} from "next/font/google";
3 | import "./globals.css";
4 | import {JSX} from "react";
5 |
6 | // eslint-disable-next-line new-cap
7 | const inter = Inter({subsets: ["latin"]});
8 |
9 | export const metadata: Metadata = {
10 | title: "Create Next App",
11 | description: "Generated by create next app"
12 | };
13 |
14 | interface IRootLayout {
15 | readonly children: React.ReactNode;
16 | }
17 |
18 | const RootLayout = ({children}: IRootLayout): JSX.Element => (
19 |
20 |
21 | {children}
22 |
23 |
24 | );
25 |
26 | export default RootLayout;
27 |
--------------------------------------------------------------------------------
/packages/reca/tests/performance.spec.tsx:
--------------------------------------------------------------------------------
1 |
2 | import {render} from "@testing-library/react";
3 | import {PerformanceComponent} from "./fixtures/components/PerformanceComponent";
4 | import {expect} from "chai";
5 | import {describe, it} from "node:test";
6 |
7 | describe("Store component must work fast", () => {
8 | it("count store performance", () => {
9 | const comp = render( );
10 |
11 | // Think about relative performance timer
12 |
13 | expect(Number(comp.container.querySelector(".store > .result-time")?.textContent)).to.be.lessThan(80); // 8
14 |
15 | expect(Number(comp.container.querySelector(".auto-store > .result-time")?.textContent)).to.be.lessThan(160); // 16
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/packages/reca/tests/fixtures/components/TestDIWithPropsComponent.tsx:
--------------------------------------------------------------------------------
1 |
2 | import {useStore} from "../../../src/index.js";
3 | import {DIWithPropsStore} from "../stores/DIWithPropsStore.js";
4 |
5 | export interface ITestDIWithPropsComponent {
6 | test: number;
7 | }
8 |
9 | export const TestDIWithPropsComponent = (props: P): JSX.Element => {
10 | const store = useStore(DIWithPropsStore, props);
11 |
12 | const {test} = props;
13 |
14 | return (
15 |
16 | Seed:
17 | {" "}
18 |
19 | {store.diService.seed}
20 |
21 |
22 |
23 | Props:
24 | {" "}
25 |
26 | {test}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/packages/reca/tests/fixtures/services/SpaceXService.ts:
--------------------------------------------------------------------------------
1 | import {reflection} from "first-di";
2 | import {SpaceXCompanyInfo} from "../models/SpaceXCompanyInfo";
3 |
4 | @reflection
5 | export class SpaceXService {
6 |
7 | // eslint-disable-next-line @typescript-eslint/class-methods-use-this
8 | public async getCompanyInfo (): Promise {
9 | const response = await fetch("https://api.spacexdata.com/v3/info");
10 | const json: unknown = await response.json();
11 |
12 | // ... and manies manies lines of logics
13 |
14 | if (typeof json === "object" && json !== null) {
15 | return new SpaceXCompanyInfo().applyData(json);
16 | }
17 | throw new Error("SpaceXService.getCompanyInfo: response object is not json");
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/packages/reca-docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "experimentalDecorators": true,
15 | "emitDecoratorMetadata": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "plugins": [
19 | {
20 | "name": "next"
21 | }
22 | ],
23 | "paths": {
24 | "@/*": ["./*"]
25 | }
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/packages/reca/src/config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 |
3 | import {resolve} from "first-di";
4 | import type {ClassConstructor} from "first-di/dist/typings/class-constructor";
5 |
6 | export class DiConfig {
7 |
8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
9 | public resolver: (constructor: ClassConstructor, options?: any) => T = resolve;
10 |
11 | }
12 |
13 | export class Config {
14 |
15 | public di: DiConfig = new DiConfig();
16 |
17 | // From https://gist.github.com/rhysburnie/498bfd98f24b7daf5fd5930c7f3c1b7b
18 |
19 | // eslint-disable-next-line @typescript-eslint/prefer-optional-chain, @typescript-eslint/no-unnecessary-condition
20 | public readonly isBrowser = !(typeof process !== "undefined" && process.versions && process.versions.node);
21 |
22 | }
23 |
24 | export const config = new Config();
25 |
--------------------------------------------------------------------------------
/packages/reca/tests/fixtures/stores/SpaceXStore.ts:
--------------------------------------------------------------------------------
1 | import {reflection} from "first-di";
2 | import {AutoStore} from "../../../src/index.js";
3 | import {SpaceXCompanyInfo} from "../models/SpaceXCompanyInfo.js";
4 | import {SpaceXService} from "../services/SpaceXService.js";
5 |
6 | @reflection
7 | export class SpaceXStore extends AutoStore {
8 |
9 | public companyInfo: SpaceXCompanyInfo = new SpaceXCompanyInfo();
10 |
11 | public constructor (private readonly spaceXService: SpaceXService) {
12 | super();
13 | }
14 |
15 | public activate (): void {
16 | this.fetchCompanyInfo();
17 | }
18 |
19 | private async fetchCompanyInfo (): Promise {
20 | try {
21 | this.companyInfo = await this.spaceXService.getCompanyInfo();
22 | } catch (error) {
23 | // Process exceptions, ex: this.logger.error(error.message);
24 | }
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/test-pr.yml:
--------------------------------------------------------------------------------
1 | name: Test Pull Request
2 |
3 | on:
4 | pull_request:
5 | branches: [ main ]
6 | types: [ opened, synchronize, reopened ]
7 |
8 | permissions:
9 | contents: read
10 | pull-requests: read
11 |
12 | jobs:
13 | test:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout repository
18 | uses: actions/checkout@v6
19 | with:
20 | fetch-depth: 0
21 | ref: ${{ github.event.pull_request.head.sha }}
22 |
23 | - name: Setup Node.js
24 | uses: actions/setup-node@v6
25 | with:
26 | node-version: 'latest'
27 | cache: 'npm'
28 |
29 | - name: Install dependencies
30 | run: npm ci
31 |
32 | - name: Run tests
33 | run: npm test
34 |
35 | - name: Build packages
36 | run: npm run build
37 |
38 | - name: Check for security vulnerabilities
39 | run: npm audit --production
40 | continue-on-error: true
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reca-workspace",
3 | "version": "2.0.18",
4 | "type": "module",
5 | "private": "true",
6 | "scripts": {
7 | "build": "npm run build -w packages/reca",
8 | "lint": "npm run lint -w packages/reca",
9 | "release": "npm run release -w packages/reca && standard-version",
10 | "test": "npm run lint -w packages/reca",
11 | "upgrade": "ncu -u -ws --root && rimraf -g -v **/node_modules **/package-lock.json && npm i",
12 | "prepare": "husky"
13 | },
14 | "lint-staged": {
15 | "./src/**/*.(ts|tsx|js|jsx)": [
16 | "eslint --fix"
17 | ]
18 | },
19 | "workspaces": [
20 | "packages/*"
21 | ],
22 | "devDependencies": {
23 | "@commitlint/cli": "^20.2.0",
24 | "@commitlint/config-conventional": "^20.2.0",
25 | "@labeg/code-style": "^6.10.13",
26 | "husky": "^9.1.7",
27 | "lint-staged": "^16.2.7",
28 | "rimraf": "^6.1.2",
29 | "standard-version": "^9.5.0",
30 | "npm-check-updates": "^19.2.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/reca/tests/fixtures/stores/LiveCycleStore.ts:
--------------------------------------------------------------------------------
1 | import {reflection, Store} from "../../../src/index.js";
2 |
3 | export interface ILiveCycleStoreProps {
4 | onLivecycleChange: (val: string) => void;
5 | }
6 |
7 | @reflection
8 | export class LiveCycleStore extends Store
{
9 |
10 | public state: string = "init";
11 |
12 | public constructor (props: P) {
13 | super();
14 | this.state += " constructor";
15 | props.onLivecycleChange(this.state);
16 | }
17 |
18 | public activate (props: P): void {
19 | this.state += " activate";
20 | props.onLivecycleChange(this.state);
21 | }
22 |
23 | public update (props: P): void {
24 | this.state += " update";
25 | props.onLivecycleChange(this.state);
26 | }
27 |
28 | public afterUpdate (props: P): void {
29 | this.state += " afterUpdate";
30 | props.onLivecycleChange(this.state);
31 | }
32 |
33 | public dispose (props: P): void {
34 | this.state += " dispose";
35 | props.onLivecycleChange(this.state);
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL Advanced"
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 | schedule:
9 | - cron: '0 0 * * 1' # Каждый понедельник в 00:00
10 |
11 | permissions:
12 | actions: read
13 | contents: read
14 | security-events: write
15 |
16 | jobs:
17 | analyze:
18 | name: Analyze Code
19 | runs-on: ubuntu-latest
20 | timeout-minutes: 360
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | language: [ 'javascript-typescript' ]
26 |
27 | steps:
28 | - name: Checkout repository
29 | uses: actions/checkout@v6
30 |
31 | - name: Initialize CodeQL
32 | uses: github/codeql-action/init@v4
33 | with:
34 | languages: ${{ matrix.language }}
35 | queries: security-extended,security-and-quality
36 |
37 | - name: Autobuild
38 | uses: github/codeql-action/autobuild@v4
39 |
40 | - name: Perform CodeQL Analysis
41 | uses: github/codeql-action/analyze@v4
42 | with:
43 | category: "/language:${{matrix.language}}"
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Eugene Labutin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/reca/tests/fixtures/stores/LiveCycleAutoStore.ts:
--------------------------------------------------------------------------------
1 | import {AutoStore, reflection} from "../../../src/index.js";
2 |
3 | export interface ILiveCycleStoreProps {
4 | onLivecycleChange: (val: string) => void;
5 | }
6 |
7 | @reflection
8 | export class LiveCycleAutoStore
extends AutoStore
{
9 |
10 | public state: string = "init";
11 |
12 | public constructor (props: P) {
13 | super();
14 | this.state += " constructor";
15 | props.onLivecycleChange(this.state);
16 | }
17 |
18 | public activate (props: P): void {
19 | this.state += " activate";
20 | props.onLivecycleChange(this.state);
21 | }
22 |
23 | public update (props: P): void {
24 | this.state += " update";
25 | props.onLivecycleChange(this.state);
26 | }
27 |
28 | public afterUpdate (props: P): void {
29 | this.state += " afterUpdate";
30 | props.onLivecycleChange(this.state);
31 | }
32 |
33 | public dispose (props: P): void {
34 | this.state += " dispose";
35 | props.onLivecycleChange(this.state);
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/packages/reca/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Eugene Labutin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/reca/src/hooks/UseStore.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/rules-of-hooks */
2 | import type {Store} from "../stores/Store.js";
3 | import {useClientStore} from "./UseClientStore.js";
4 | import {useServerStore} from "./UseServerStore.js";
5 |
6 | /**
7 | * Todo: add DI here
8 | *
9 | * @param store
10 | * @param props
11 | * @returns
12 | */
13 | export const useStore =
>(
14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
15 | store: new (...params: any[]) => T,
16 | props?: P
17 | ): T => {
18 | if (typeof window !== "undefined") {
19 | return useClientStore
(store, props);
20 | }
21 | return useServerStore
(store, props);
22 | };
23 |
24 | /**
25 | * Todo: add DI here
26 | *
27 | * @param store
28 | * @param props
29 | * @returns
30 | */
31 | export const useStoreAsync = async
>(
32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
33 | store: new (...params: any[]) => T,
34 | props?: P
35 | ): Promise => {
36 | await Promise.resolve();
37 |
38 | if (typeof window !== "undefined") {
39 | return useClientStore(store, props);
40 | }
41 | return useServerStore
(store, props);
42 | };
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Create a report to help us improve
4 | title: '[BUG] '
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Description
11 |
12 | A clear and concise description of the bug.
13 |
14 | ## Steps to Reproduce
15 |
16 | 1. Install `reca` version X.X.X
17 | 2. Create a component with '...'
18 | 3. Run the application
19 | 4. See error
20 |
21 | ## Expected Behavior
22 |
23 | What you expected to happen.
24 |
25 | ## Actual Behavior
26 |
27 | What actually happened.
28 |
29 | ## Code Sample
30 |
31 | ```typescript
32 | // Minimal code example that demonstrates the issue
33 | ```
34 |
35 | ## Component/Store Code
36 |
37 | ```typescript
38 | // Your component or store code
39 | ```
40 |
41 | ## Environment
42 |
43 | - **reca version**: [e.g., 2.0.4]
44 | - **React version**: [e.g., 18.3.1]
45 | - **Node.js version**: [e.g., 20.10.0]
46 | - **npm/pnpm/yarn version**: [e.g., npm 10.2.3]
47 | - **Operating System**: [e.g., Windows 11, macOS 14, Ubuntu 22.04]
48 | - **TypeScript version**: [e.g., 5.3.3]
49 | - **Build tool**: [e.g., Vite, Next.js, CRA]
50 |
51 | ## Additional Context
52 |
53 | Add any other context, screenshots, or error messages here.
54 |
55 | ## Possible Solution
56 |
57 | If you have ideas on how to fix this, please share them.
58 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot-auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot Auto-merge
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened]
6 |
7 | permissions:
8 | contents: write
9 | pull-requests: write
10 |
11 | jobs:
12 | enable-auto-merge:
13 | runs-on: ubuntu-latest
14 | # Срабатывает ТОЛЬКО для Dependabot PR
15 | if: github.actor == 'dependabot[bot]'
16 |
17 | steps:
18 | - name: Dependabot metadata
19 | id: metadata
20 | uses: dependabot/fetch-metadata@v2
21 | with:
22 | github-token: "${{ secrets.GITHUB_TOKEN }}"
23 |
24 | - name: Wait for test completion
25 | uses: lewagon/wait-on-check-action@v1.4.1
26 | with:
27 | ref: ${{ github.event.pull_request.head.sha }}
28 | check-name: 'test'
29 | repo-token: ${{ secrets.GITHUB_TOKEN }}
30 | wait-interval: 10
31 |
32 | - name: Approve PR
33 | run: gh pr review --approve "$PR_URL"
34 | env:
35 | PR_URL: ${{ github.event.pull_request.html_url }}
36 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 |
38 | - name: Merge PR
39 | run: gh pr merge --squash "$PR_URL"
40 | env:
41 | PR_URL: ${{ github.event.pull_request.html_url }}
42 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 |
--------------------------------------------------------------------------------
/packages/reca/tests/di.spec.tsx:
--------------------------------------------------------------------------------
1 | import {TesDIComponent} from "./fixtures/components/TestDIComponent.js";
2 | import {TestDIWithPropsComponent} from "./fixtures/components/TestDIWithPropsComponent.js";
3 | import {render} from "@testing-library/react";
4 | import {expect} from "chai";
5 | import {describe, it} from "node:test";
6 |
7 | describe("Dependency injection must work", () => {
8 | it("di must resolve dependencies", () => {
9 | const comp1 = render( );
10 | const comp2 = render( );
11 | expect(comp1.container.innerHTML).to.equal(comp2.container.innerHTML);
12 |
13 | comp1.rerender( );
14 | expect(comp1.container.innerHTML).to.equal(comp2.container.innerHTML);
15 |
16 | comp1.unmount();
17 | comp2.unmount();
18 | });
19 |
20 | it("di must resolve props", () => {
21 | const comp1 = render( );
22 | const comp2 = render( );
23 | expect(comp1.container.innerHTML).to.equal(comp2.container.innerHTML);
24 |
25 | comp1.rerender( );
26 | expect(comp1.container.innerHTML).to.equal(comp2.container.innerHTML);
27 |
28 | comp1.unmount();
29 | comp2.unmount();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/packages/reca-docs/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/reca/src/hooks/UseServerStore.ts:
--------------------------------------------------------------------------------
1 | import type {ClassConstructor} from "first-di/dist/typings/class-constructor";
2 | import {config} from "../config.js";
3 | import type {Store} from "../stores/Store.js";
4 |
5 | /**
6 | * Todo: add DI here
7 | *
8 | * @param store
9 | * @param props
10 | * @returns
11 | */
12 | export const useServerStore =
>(
13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
14 | store: new (...params: any[]) => T,
15 | props?: P
16 | ): T => {
17 | // Resolve dependencies
18 | const constructorParams: unknown[] = Reflect
19 | .getMetadata("design:paramtypes", store) as ([] | null) ?? [];
20 |
21 | const resolvedParams = constructorParams.map((param: unknown) => {
22 | if (typeof param === "function" && "prototype" in param) { // Check is class
23 | if (param.prototype === Object.prototype) { // Typescript interface in props (props: P)
24 | return props;
25 | }
26 |
27 | // True class
28 | return config.di.resolver(param as ClassConstructor);
29 | }
30 |
31 | // Else props object
32 | return props;
33 | });
34 |
35 | // eslint-disable-next-line new-cap
36 | const resolvedStore = new store(...resolvedParams);
37 |
38 | return resolvedStore;
39 | };
40 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # NPM dependencies
4 | - package-ecosystem: "npm"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly"
8 | day: "wednesday"
9 | time: "09:00"
10 | open-pull-requests-limit: 1
11 | groups:
12 | all-minor-patch:
13 | patterns:
14 | - "*"
15 | update-types:
16 | - "minor"
17 | - "patch"
18 | eslint-plugins:
19 | patterns:
20 | - "eslint*"
21 | - "@typescript-eslint/*"
22 | - "@stylistic/*"
23 | react-ecosystem:
24 | patterns:
25 | - "react*"
26 | - "@types/react"
27 | development-dependencies:
28 | dependency-type: "development"
29 | exclude-patterns:
30 | - "eslint*"
31 | - "react*"
32 | commit-message:
33 | prefix: "chore"
34 | prefix-development: "chore"
35 | include: "scope"
36 | labels:
37 | - "dependencies"
38 | assignees:
39 | - "LabEG"
40 |
41 | # GitHub Actions
42 | - package-ecosystem: "github-actions"
43 | directory: "/"
44 | schedule:
45 | interval: "weekly"
46 | day: "wednesday"
47 | time: "09:00"
48 | commit-message:
49 | prefix: "ci"
50 | include: "scope"
51 | labels:
52 | - "dependencies"
53 | assignees:
54 | - "LabEG"
55 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea for this project
4 | title: '[FEATURE] '
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Feature Description
11 |
12 | A clear and concise description of the feature you'd like to see.
13 |
14 | ## Problem Statement
15 |
16 | What problem would this feature solve? Is your feature request related to a problem?
17 |
18 | **Example:** "I'm frustrated when [...]"
19 |
20 | ## Proposed Solution
21 |
22 | Describe the solution you'd like to see implemented.
23 |
24 | ## Alternatives Considered
25 |
26 | Describe any alternative solutions or features you've considered.
27 |
28 | ## Use Cases
29 |
30 | Provide specific examples of how this feature would be used:
31 |
32 | 1. Use case 1...
33 | 2. Use case 2...
34 |
35 | ## Code Examples
36 |
37 | If applicable, provide example code showing how you envision using this feature:
38 |
39 | ```typescript
40 | // Example of proposed usage
41 | ```
42 |
43 | ## Impact
44 |
45 | - **Who benefits**: [developers, teams, specific use cases]
46 | - **Breaking changes**: [yes/no - explain if yes]
47 | - **Backward compatibility**: [maintained/affected]
48 |
49 | ## Additional Context
50 |
51 | Add any other context, screenshots, or examples about the feature request here.
52 |
53 | ## Willingness to Contribute
54 |
55 | - [ ] I'm willing to submit a PR to implement this feature
56 | - [ ] I can help with testing
57 | - [ ] I can help with documentation
58 |
--------------------------------------------------------------------------------
/packages/reca-docs/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | name: NPM Publish
2 |
3 | # This workflow uses actions that are not certified by GitHub.
4 | # They are provided by a third-party and are governed by
5 | # separate terms of service, privacy policy, and support
6 | # documentation.
7 |
8 | on:
9 | push:
10 | branches: [ main ]
11 | schedule:
12 | - cron: '0 12 * * 3' # Every Wednesday at 12:00 UTC (3 hours after Dependabot runs at 09:00)
13 | workflow_dispatch:
14 |
15 | permissions:
16 | contents: write
17 | id-token: write # Required for npm provenance
18 |
19 | jobs:
20 | build:
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - name: Checkout repository
25 | uses: actions/checkout@v6
26 | with:
27 | fetch-depth: 0
28 |
29 | - name: Setup Node.js
30 | uses: actions/setup-node@v6
31 | with:
32 | node-version: 'latest'
33 | registry-url: https://registry.npmjs.org/
34 | cache: 'npm'
35 |
36 | - name: Configure Git
37 | run: |
38 | git config --global user.email "labeg@mail.ru"
39 | git config --global user.name "Eugene Labutin"
40 |
41 | - name: Install dependencies
42 | run: npm ci
43 |
44 | - name: Run tests
45 | run: npm test
46 |
47 | - name: Build packages
48 | run: npm run build
49 |
50 | - name: Create release
51 | run: npm run release
52 |
53 | - name: Push changes and tags
54 | run: git push && git push --tags
55 |
56 | - name: Copy README to package
57 | run: cp README.md packages/reca/README.md
58 |
59 | - name: Publish to NPM
60 | run: npm publish --provenance --access public -w packages/reca
61 |
--------------------------------------------------------------------------------
/packages/reca/tests/fixtures/components/ToDoComponent.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-array-index-key */
2 | /* eslint-disable react/jsx-no-bind */
3 | /* eslint-disable @typescript-eslint/unbound-method */
4 | /* eslint-disable react/jsx-key */
5 |
6 | import {useStore} from "../../../src/index.js";
7 | import {ToDoAutoStore} from "../stores/ToDoAutoStore.js";
8 |
9 | export const ToDoComponent = (): JSX.Element => {
10 | const store = useStore(ToDoAutoStore);
11 |
12 | return (
13 |
14 |
15 | {
16 | store.todos.map((todo, index) => (
17 |
21 | {todo}
22 |
23 | store.handleDeleteTodo(index)}
26 | type="button"
27 | >
28 | X
29 |
30 |
31 | ))
32 | }
33 |
34 |
35 |
36 |
40 |
41 |
45 | add
46 |
47 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/packages/reca/tests/fixtures/components/PerformanceComponent.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/rules-of-hooks */
2 | /* eslint-disable max-statements */
3 | /* eslint-disable max-lines-per-function */
4 | /* eslint-disable react/jsx-key */
5 |
6 | import {useStore} from "../../../src/index.js";
7 | import {ToDoAutoStore} from "../stores/ToDoAutoStore.js";
8 | import {ToDoStore} from "../stores/ToDoStore.js";
9 |
10 | export const PerformanceComponent = (): JSX.Element => {
11 | const array = new Array(1000).fill(true);
12 |
13 | // Calculate store
14 | const beginTimeSt = performance.now();
15 | const storesSt = array.map(() => useStore(ToDoStore));
16 | const resultSt = performance.now() - beginTimeSt;
17 |
18 | // Calculate auto-store
19 | const beginTimeAu = performance.now();
20 | const storesAu = array.map(() => useStore(ToDoAutoStore));
21 | const resultAu = performance.now() - beginTimeAu;
22 |
23 | const textSt = storesSt.reduce((previousValue, currentValue) => previousValue + currentValue.currentTodo, "");
24 | const textAu = storesAu.reduce((previousValue, currentValue) => previousValue + currentValue.currentTodo, "");
25 |
26 | return (
27 |
28 |
29 | Result Time:
30 | {" "}
31 |
32 |
33 | {resultSt}
34 |
35 |
36 |
37 |
38 | Result Time:
39 | {" "}
40 |
41 |
42 | {resultAu}
43 |
44 |
45 |
46 |
47 | {textSt}
48 |
49 | {textAu}
50 |
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/packages/reca/tests/readme.spec.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
2 |
3 | import {render, fireEvent, waitFor, screen} from "@testing-library/react";
4 | import {SpaceXComponent} from "./fixtures/components/SpaceXComponent";
5 | import {ToDoComponent} from "./fixtures/components/ToDoComponent";
6 | import {expect} from "chai";
7 | import {describe, it} from "node:test";
8 |
9 | describe("Readme samples must work", () => {
10 | it("todo sample must add end delete todo items", () => {
11 | const comp = render( );
12 |
13 | // Todos list empty
14 | expect(comp.container.querySelector(".todos-list")?.outerHTML).to.equal("
");
15 |
16 | // Inputs created
17 | expect(comp.container.querySelector(".todos-input")?.innerHTML).to.equal("add ");
18 |
19 | // Input todo
20 | fireEvent.input(
21 | comp.container.querySelector(".todos-input > input") as HTMLInputElement,
22 | {target: {value: "First ToDo"}}
23 | );
24 |
25 | fireEvent.click(comp.container.querySelector(".todos-input > button") as HTMLButtonElement);
26 |
27 | // Check first todo
28 | expect(comp.container.querySelector(".todo")?.textContent).to.equal("First ToDoX");
29 |
30 | // Remove todo
31 | fireEvent.click(comp.container.querySelector(".todo-delete") as HTMLButtonElement);
32 |
33 | // Check list empty
34 | expect(comp.container.querySelector(".todos-list")?.outerHTML).to.equal("
");
35 | });
36 |
37 | it("todo sample must resolve di and show info from service", async () => {
38 | const comp = render( );
39 |
40 | await waitFor(() => screen.findByText("Company: SpaceX"), {timeout: 5000});
41 |
42 | expect(comp.container.innerHTML).to.equal("Company: SpaceX
Founder: Elon Musk
");
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/packages/reca/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reca",
3 | "version": "2.4.3",
4 | "description": "ReCA - React Clean Architecture state manager",
5 | "main": "dist/index.js",
6 | "type": "module",
7 | "scripts": {
8 | "build": "npm run clean && tsc --project tsconfig.build.json && node ./dist/index.js",
9 | "clean": "rimraf ./dist",
10 | "lint": "eslint --fix ./src/",
11 | "test": "node --import ./ts-loader.js --test --test-reporter=spec --test-reporter-destination=stdout \"tests/**/*.spec.tsx\"",
12 | "test-watch": "node --watch --import ./ts-loader.js --test --test-reporter=spec --test-reporter-destination=stdout \"tests/**/*.spec.tsx\"",
13 | "coverage": "node --import ./ts-loader.js --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info \"tests/**/*.spec.tsx\"",
14 | "release": "standard-version",
15 | "prepublishOnly": "npm run test && npm run build"
16 | },
17 | "keywords": [
18 | "react",
19 | "state manager"
20 | ],
21 | "author": "LabEG",
22 | "license": "MIT",
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/LabEG/reca.git"
26 | },
27 | "bugs": {
28 | "url": "https://github.com/LabEG/reca/issues"
29 | },
30 | "homepage": "https://github.com/LabEG/reca#readme",
31 | "files": [
32 | "dist",
33 | "README.md",
34 | "LICENSE"
35 | ],
36 | "dependencies": {
37 | "first-di": "^3.4.7"
38 | },
39 | "peerDependencies": {
40 | "react": ">=16.0.0",
41 | "reflect-metadata": ">=0.1.0"
42 | },
43 | "devDependencies": {
44 | "@testing-library/react": "^16.3.0",
45 | "@swc-node/register": "^1.11.1",
46 | "@types/chai": "^5.2.3",
47 | "@types/react": "^19.2.7",
48 | "chai": "^6.2.1",
49 | "global-jsdom": "^27.0.0",
50 | "jsdom": "^27.3.0",
51 | "react": "^19.2.3",
52 | "react-dom": "^19.2.3",
53 | "reflect-metadata": "^0.2.2",
54 | "rimraf": "^6.1.2",
55 | "standard-version": "^9.5.0",
56 | "ts-node": "^10.9.2",
57 | "typescript": "^5.9.3"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 |
4 |
5 | ## Related Issue
6 |
7 |
8 | Closes #
9 |
10 | ## Type of Change
11 |
12 |
13 |
14 | - [ ] Bug fix (non-breaking change which fixes an issue)
15 | - [ ] New feature (non-breaking change which adds functionality)
16 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
17 | - [ ] Documentation update
18 | - [ ] Code refactoring
19 | - [ ] Dependency update
20 |
21 | ## Changes Made
22 |
23 |
24 |
25 | -
26 | -
27 | -
28 |
29 | ## Testing
30 |
31 |
32 |
33 | - [ ] I have run `npm test` and all tests pass
34 | - [ ] I have tested the changes manually
35 | - [ ] I have added/updated tests for my changes
36 |
37 | ## Code Quality
38 |
39 | - [ ] My code follows the code style of this project
40 | - [ ] I have performed a self-review of my own code
41 | - [ ] I have commented my code, particularly in hard-to-understand areas
42 | - [ ] My changes generate no new warnings
43 | - [ ] No console.log or debugging code left in
44 |
45 | ## Documentation
46 |
47 | - [ ] I have updated the README.md (if applicable)
48 | - [ ] I have updated the CHANGELOG.md (if applicable)
49 | - [ ] I have added/updated code comments
50 |
51 | ## Breaking Changes
52 |
53 |
54 |
55 | N/A
56 |
57 | ## Screenshots / Examples
58 |
59 |
60 |
61 | ```typescript
62 | // Example code showing the changes
63 | ```
64 |
65 | ## Checklist
66 |
67 | - [ ] This PR has a descriptive title
68 | - [ ] All commits follow [Conventional Commits](https://www.conventionalcommits.org/)
69 | - [ ] I have read the [CONTRIBUTING](../CONTRIBUTING.md) guide
70 | - [ ] This PR is ready for review
71 |
72 | ## Additional Notes
73 |
74 |
75 |
--------------------------------------------------------------------------------
/packages/reca-docs/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --max-width: 1100px;
3 | --border-radius: 12px;
4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
7 |
8 | --foreground-rgb: 0, 0, 0;
9 | --background-start-rgb: 214, 219, 220;
10 | --background-end-rgb: 255, 255, 255;
11 |
12 | --primary-glow: conic-gradient(
13 | from 180deg at 50% 50%,
14 | #16abff33 0deg,
15 | #0885ff33 55deg,
16 | #54d6ff33 120deg,
17 | #0071ff33 160deg,
18 | transparent 360deg
19 | );
20 | --secondary-glow: radial-gradient(
21 | rgba(255, 255, 255, 1),
22 | rgba(255, 255, 255, 0)
23 | );
24 |
25 | --tile-start-rgb: 239, 245, 249;
26 | --tile-end-rgb: 228, 232, 233;
27 | --tile-border: conic-gradient(
28 | #00000080,
29 | #00000040,
30 | #00000030,
31 | #00000020,
32 | #00000010,
33 | #00000010,
34 | #00000080
35 | );
36 |
37 | --callout-rgb: 238, 240, 241;
38 | --callout-border-rgb: 172, 175, 176;
39 | --card-rgb: 180, 185, 188;
40 | --card-border-rgb: 131, 134, 135;
41 | }
42 |
43 | @media (prefers-color-scheme: dark) {
44 | :root {
45 | --foreground-rgb: 255, 255, 255;
46 | --background-start-rgb: 0, 0, 0;
47 | --background-end-rgb: 0, 0, 0;
48 |
49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
50 | --secondary-glow: linear-gradient(
51 | to bottom right,
52 | rgba(1, 65, 255, 0),
53 | rgba(1, 65, 255, 0),
54 | rgba(1, 65, 255, 0.3)
55 | );
56 |
57 | --tile-start-rgb: 2, 13, 46;
58 | --tile-end-rgb: 2, 5, 19;
59 | --tile-border: conic-gradient(
60 | #ffffff80,
61 | #ffffff40,
62 | #ffffff30,
63 | #ffffff20,
64 | #ffffff10,
65 | #ffffff10,
66 | #ffffff80
67 | );
68 |
69 | --callout-rgb: 20, 20, 20;
70 | --callout-border-rgb: 108, 108, 108;
71 | --card-rgb: 100, 100, 100;
72 | --card-border-rgb: 200, 200, 200;
73 | }
74 | }
75 |
76 | * {
77 | box-sizing: border-box;
78 | padding: 0;
79 | margin: 0;
80 | }
81 |
82 | html,
83 | body {
84 | max-width: 100vw;
85 | overflow-x: hidden;
86 | }
87 |
88 | body {
89 | color: rgb(var(--foreground-rgb));
90 | background: linear-gradient(
91 | to bottom,
92 | transparent,
93 | rgb(var(--background-end-rgb))
94 | )
95 | rgb(var(--background-start-rgb));
96 | }
97 |
98 | a {
99 | color: inherit;
100 | text-decoration: none;
101 | }
102 |
103 | @media (prefers-color-scheme: dark) {
104 | html {
105 | color-scheme: dark;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/packages/reca/tests/livecycle.spec.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-lines-per-function */
2 | /* eslint-disable max-statements */
3 | /* eslint-disable react/jsx-no-bind */
4 |
5 | import {TestStoreComponent} from "./fixtures/components/TestStoreComponent.js";
6 | import {render} from "@testing-library/react";
7 | import {TestAutoStoreComponent} from "./fixtures/components/TestAutoStoreComponent.js";
8 | import {expect} from "chai";
9 | import {describe, it} from "node:test";
10 |
11 | describe("Livecycles must work", () => {
12 | it("Store livecycle", () => {
13 | let state = "";
14 | const onLivecycleChange = (newState: string): void => {
15 | state = newState;
16 | };
17 |
18 | /**
19 | * Activate and afterUpdate don't call redraw,
20 | * so state have more livecycle methods than show in view
21 | */
22 |
23 | // Create component
24 | const comp = render( );
25 | expect(comp.container.innerHTML).to.equal("init constructor
");
26 | expect(state).to.equal("init constructor activate");
27 |
28 | // Update component
29 | comp.rerender( );
30 | expect(comp.container.innerHTML).to.equal("init constructor activate update
");
31 | expect(state).to.equal("init constructor activate update afterUpdate");
32 |
33 | // Delete component
34 | comp.unmount();
35 | expect(comp.container.innerHTML).to.equal("");
36 | expect(state).to.equal("init constructor activate update afterUpdate dispose");
37 | });
38 |
39 | it("AutoStore livecycle", () => {
40 | let state = "";
41 | const onLivecycleChange = (newState: string): void => {
42 | state = newState;
43 | };
44 |
45 | /**
46 | * Activate and afterUpdate don't call redraw,
47 | * so state have more livecycle methods than show in view
48 | */
49 |
50 | // Create component
51 | const comp = render( );
52 | expect(comp.container.innerHTML).to.equal("init constructor
");
53 | expect(state).to.equal("init constructor activate");
54 |
55 | // Update component
56 | comp.rerender( );
57 | expect(comp.container.innerHTML).to.equal("init constructor activate update
");
58 | expect(state).to.equal("init constructor activate update afterUpdate");
59 |
60 | // Delete component
61 | comp.unmount();
62 | expect(comp.container.innerHTML).to.equal("");
63 | expect(state).to.equal("init constructor activate update afterUpdate dispose");
64 | });
65 | });
66 |
67 |
--------------------------------------------------------------------------------
/packages/reca/src/hooks/UseClientStore.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/exhaustive-deps */
2 | /* eslint-disable max-lines-per-function */
3 |
4 | import type {ClassConstructor} from "first-di/dist/typings/class-constructor";
5 | import * as React from "react";
6 | import {config} from "../config.js";
7 | import type {Store} from "../stores/Store.js";
8 |
9 | /**
10 | * Todo: add DI here
11 | *
12 | * @param store
13 | * @param props
14 | * @returns
15 | */
16 | export const useClientStore = >(
17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
18 | store: new (...params: any[]) => T,
19 | props?: P
20 | ): T => {
21 | // Render function
22 | const [, setSeed] = React.useState(0);
23 |
24 | // Constructor
25 | let isInit = false;
26 | const [stateStore] = React.useState(() => {
27 | isInit = true;
28 |
29 | // Resolve dependencies
30 | const constructorParams: unknown[] = Reflect
31 | .getMetadata("design:paramtypes", store) as ([] | null) ?? [];
32 |
33 | const resolvedParams = constructorParams.map((param: unknown) => {
34 | if (typeof param === "function" && "prototype" in param) { // Check is class
35 | if (param.prototype === Object.prototype) { // Typescript interface in props (props: P)
36 | return props;
37 | }
38 |
39 | // True class
40 | return config.di.resolver(param as ClassConstructor);
41 | }
42 |
43 | // Else props object
44 | return props;
45 | });
46 |
47 | // eslint-disable-next-line new-cap
48 | const resolvedStore = new store(...resolvedParams);
49 |
50 | resolvedStore.setRedrawFunction(() => {
51 | setSeed(Math.random());
52 | });
53 |
54 | return resolvedStore;
55 | });
56 |
57 | stateStore.isDrawTime = true;
58 |
59 | // Activate and Dispose(Destructor) methods
60 | React.useEffect(() => {
61 | stateStore.activate(props ?? {} as P);
62 |
63 | return () => stateStore.dispose(props ?? {} as P);
64 | }, []);
65 |
66 | // Update method
67 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
68 | if (!isInit) {
69 | stateStore.update(props ?? {} as P);
70 | }
71 |
72 | // PropsUpdate method
73 | React.useMemo(
74 | () => {
75 | if (!isInit) {
76 | stateStore.propsUpdate(props ?? {} as P);
77 | }
78 | },
79 | [props ?? {}]
80 | );
81 |
82 | // AfterUpdate method
83 | React.useEffect(() => {
84 | if (!isInit) {
85 | stateStore.afterUpdate(props ?? {} as P);
86 | }
87 | stateStore.isDrawTime = false;
88 | });
89 |
90 | return stateStore;
91 | };
92 |
--------------------------------------------------------------------------------
/packages/reca/src/stores/Store.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this */
2 | /* eslint-disable @typescript-eslint/no-unused-vars */
3 |
4 | import {config} from "../config.js";
5 |
6 | export class Store {
7 |
8 | /**
9 | * Property for prevent cycle redraw if call method ot set props in AutoStore
10 | * in view redraw time
11 | */
12 | public isDrawTime: boolean = true;
13 |
14 | protected redrawFunction: () => void = () => void 0;
15 |
16 | /**
17 | * Method for override in nested store.
18 | * Run after first rendering component in DOM.
19 | *
20 | * @description
21 | * Encapsulate:
22 | * useEffect(() => {
23 | * stateStore.activate(props);
24 | *
25 | * return () => stateStore.dispose(props);
26 | * }, []);
27 | */
28 | public activate (props: T): void {
29 | // Override
30 | }
31 |
32 | /**
33 | * Method for override in nested store.
34 | * Run before second and others time of rendering component in DOM.
35 | *
36 | * @description
37 | * Encapsulate:
38 | * if (!isInit) {
39 | * stateStore.update(props);
40 | * }
41 | */
42 | public update (props: T): void {
43 | // Override
44 | }
45 |
46 | /**
47 | * Method for override in nested store.
48 | * Run only if props changed.
49 | * Run before second and others time of rendering component in DOM.
50 | *
51 | * @description
52 | * Encapsulate:
53 | * useMemo(
54 | * () => {
55 | * stateStore.propsUpdate(props);
56 | * },
57 | * [props]
58 | * );
59 | */
60 | public propsUpdate (props: T): void {
61 | // Override
62 | }
63 |
64 | /**
65 | * Method for override in nested store.
66 | * Run after second and others time of rendering component in DOM.
67 | *
68 | * @description
69 | * Encapsulate:
70 | * useEffect(() => {
71 | * if (!isInit) {
72 | * stateStore.afterUpdate(props);
73 | * }
74 | * });
75 | */
76 | public afterUpdate (props: T): void {
77 | // Override
78 | }
79 |
80 | /**
81 | * Method for override in nested store.
82 | * Run after second and others rendering component in DOM.
83 | *
84 | * @description
85 | * Encapsulate:
86 | * useEffect(() => {
87 | * stateStore.activate(props);
88 | *
89 | * return () => stateStore.dispose(props);
90 | * }, []);
91 | */
92 | public dispose (props: T): void {
93 | // Override
94 | }
95 |
96 | public setRedrawFunction (updateFunction: () => void): void {
97 | this.redrawFunction = updateFunction;
98 | }
99 |
100 | /**
101 | * Update view on next requestAnimationFrame
102 | */
103 | public redraw (): void {
104 | if (this.isDrawTime) {
105 | return;
106 | }
107 |
108 | if (config.isBrowser) {
109 | requestAnimationFrame(() => this.redrawFunction());
110 | } else {
111 | // SSR don't use redraw, its for unit tests
112 | this.redrawFunction();
113 | }
114 | }
115 |
116 | /**
117 | * Update view component immediately
118 | */
119 | public forceRedraw (): void {
120 | this.redrawFunction();
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/packages/reca/src/stores/AutoStore.ts:
--------------------------------------------------------------------------------
1 | import "reflect-metadata";
2 | import {Store} from "./Store.js";
3 |
4 | export class AutoStore extends Store {
5 |
6 | protected dontObserveProperties: (string | symbol) [] = [
7 | // Methods
8 | "constructor",
9 | "activate",
10 | "update",
11 | "propsUpdate",
12 | "afterUpdate",
13 | "dispose",
14 | "setRedrawFunction",
15 | "redraw",
16 | "forceRedraw",
17 |
18 | // Properties
19 | "isDrawTime",
20 | "dontOverrideMethods",
21 | "redrawFunction"
22 |
23 | ];
24 |
25 | public constructor () {
26 | super();
27 | this.injectProperties();
28 | this.injectMethods();
29 | }
30 |
31 | /**
32 | * Replace properties by getters and setters, inject redraw
33 | */
34 | protected injectProperties (): void {
35 | const properties: (string | symbol)[] = Reflect.ownKeys(this);
36 |
37 | for (const property of properties) {
38 | const isNotAuto = Reflect.getMetadata("reca:notAuto", this, property) as unknown;
39 | if (!this.dontObserveProperties.includes(property) && isNotAuto !== true) {
40 | let propValue = Reflect.get(this, property) as unknown;
41 |
42 | Object.defineProperty(
43 | this,
44 | property,
45 | {
46 | get: () => propValue,
47 | set: (value: unknown) => {
48 | propValue = value;
49 | this.redraw();
50 | }
51 | }
52 | );
53 | }
54 | }
55 | }
56 |
57 | /**
58 | * Wrap methods, inject redraw
59 | */
60 | protected injectMethods (): void {
61 | const methods: (string | symbol)[] = Reflect.ownKeys(Object.getPrototypeOf(this) as object);
62 |
63 | for (const method of methods) {
64 | const isNotAuto = Reflect.getMetadata("reca:notAuto", this, method) as unknown;
65 |
66 | if (!this.dontObserveProperties.includes(method) || isNotAuto !== true) {
67 | const methodFunction = Reflect.get(this, method) as (...params: unknown[]) => unknown;
68 | const isNotRedraw = Reflect.getMetadata("reca:notRedraw", this, method) as unknown;
69 |
70 | Reflect.set(
71 | this,
72 | method,
73 | (...params: unknown[]) => {
74 | if (isNotRedraw !== true) {
75 | this.redraw(); // Before method because in method can be error, how stop flow
76 | }
77 |
78 | const maybePromise = methodFunction.apply(this, [...params]);
79 |
80 | if (maybePromise instanceof Promise) {
81 | return maybePromise
82 | .then((data: unknown) => {
83 | if (isNotRedraw !== true) {
84 | this.redraw();
85 | }
86 | return data;
87 | })
88 | .catch((error: unknown) => {
89 | if (isNotRedraw !== true) {
90 | this.redraw();
91 | }
92 | throw error;
93 | });
94 | }
95 |
96 | return maybePromise;
97 | }
98 | );
99 | }
100 | }
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | We actively support the following versions of `reca` with security updates:
6 |
7 | | Version | Supported |
8 | | ------- | ------------------ |
9 | | 2.x | :white_check_mark: |
10 | | 1.x | :x: |
11 | | < 1.0 | :x: |
12 |
13 | ## Reporting a Vulnerability
14 |
15 | We take the security of `reca` seriously. If you discover a security vulnerability, please follow these steps:
16 |
17 | ### How to Report
18 |
19 | 1. **DO NOT** open a public GitHub issue for security vulnerabilities
20 | 2. Send a detailed report to the repository maintainer via:
21 | - GitHub Security Advisory: [Report a vulnerability](https://github.com/LabEG/reca/security/advisories/new)
22 | - Email: Create an issue in the [issue tracker](https://github.com/LabEG/reca/issues) marked as **Security** (if no sensitive details need to be shared)
23 |
24 | ### What to Include
25 |
26 | Please provide the following information in your report:
27 |
28 | - **Description**: A clear description of the vulnerability
29 | - **Impact**: What could an attacker accomplish by exploiting this vulnerability
30 | - **Reproduction**: Step-by-step instructions to reproduce the issue
31 | - **Version**: The version of `reca` affected
32 | - **Environment**: Relevant environment details (Node.js version, React version, etc.)
33 | - **Suggested Fix** (optional): If you have ideas on how to fix the vulnerability
34 |
35 | ### Response Timeline
36 |
37 | - **Initial Response**: Within 48 hours of receiving the report
38 | - **Status Update**: Within 7 days with either a fix timeline or request for more information
39 | - **Resolution**: Security patches will be released as soon as possible, typically within 14 days for critical issues
40 |
41 | ### Security Update Process
42 |
43 | 1. The vulnerability is confirmed and assessed
44 | 2. A fix is developed and tested
45 | 3. A security advisory is prepared
46 | 4. A new version is released with the fix
47 | 5. The security advisory is published with CVE (if applicable)
48 |
49 | ## Security Best Practices
50 |
51 | When using `reca`:
52 |
53 | ### For Package Consumers
54 |
55 | - Always use the latest stable version
56 | - Regularly update dependencies using `npm update` or `npm audit fix`
57 | - Review the [CHANGELOG](./CHANGELOG.md) for security-related updates
58 | - Use `npm audit` to check for known vulnerabilities in dependencies
59 |
60 | ### For Contributors
61 |
62 | - Follow secure coding practices
63 | - Run `npm audit` before submitting pull requests
64 | - Never commit sensitive information (API keys, passwords, tokens)
65 | - Test changes thoroughly with various configurations
66 |
67 | ## Dependency Security
68 |
69 | This package relies on React and minimal dependencies. We:
70 |
71 | - Monitor security advisories for all dependencies
72 | - Update dependencies promptly when security issues are discovered
73 | - Use `npm audit` in our CI/CD pipeline
74 | - Follow semantic versioning to ensure stable updates
75 |
76 | ## Known Security Considerations
77 |
78 | As a state management library, `reca`:
79 |
80 | - **Runs in client-side applications** - ensure proper data sanitization
81 | - **Does not access network resources directly** - network calls are in your code
82 | - **State management only** - security of data depends on your implementation
83 | - **Client-side state** - sensitive data should not be stored in client state
84 |
85 | However, always ensure you:
86 |
87 | - Install packages from official npm registry
88 | - Verify package integrity using `npm audit`
89 | - Review configuration changes before applying
90 |
91 | ## Disclosure Policy
92 |
93 | When a security vulnerability is fixed:
94 |
95 | 1. We will credit the reporter (unless they wish to remain anonymous)
96 | 2. Details will be disclosed after a fix is available
97 | 3. We will publish a security advisory on GitHub
98 | 4. The vulnerability will be documented in the CHANGELOG
99 |
100 | ## Contact
101 |
102 | For any security-related questions or concerns, please:
103 |
104 | - Open a [GitHub Security Advisory](https://github.com/LabEG/reca/security/advisories/new)
105 | - Create an issue at:
106 |
107 | ---
108 |
109 | Thank you for helping keep `reca` and its users safe!
110 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to ReCA
2 |
3 | Thank you for your interest in contributing! This document provides guidelines for contributing to ReCA (React Clean Architecture).
4 |
5 | ## Code of Conduct
6 |
7 | By participating in this project, you agree to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md).
8 |
9 | ## How to Contribute
10 |
11 | ### Reporting Bugs
12 |
13 | Before creating bug reports, please check the existing issues. When creating a bug report, include:
14 |
15 | - **Clear title and description**
16 | - **Steps to reproduce** the problem
17 | - **Expected behavior** vs **actual behavior**
18 | - **Code samples** that demonstrate the issue
19 | - **Environment details** (Node.js version, npm version, OS)
20 |
21 | ### Suggesting Enhancements
22 |
23 | Enhancement suggestions are welcome! Please provide:
24 |
25 | - **Clear description** of the enhancement
26 | - **Use cases** and why it would be useful
27 | - **Possible implementation** approach (if you have ideas)
28 |
29 | ### Pull Requests
30 |
31 | 1. **Fork the repository** and create your branch from `main`
32 | 2. **Make your changes** following our coding standards
33 | 3. **Test your changes** by running `npm test`
34 | 4. **Commit your changes** using [Conventional Commits](https://www.conventionalcommits.org/):
35 | - `feat:` for new features
36 | - `fix:` for bug fixes
37 | - `docs:` for documentation changes
38 | - `chore:` for maintenance tasks
39 | - `refactor:` for code refactoring
40 | 5. **Push to your fork** and submit a pull request
41 |
42 | ## Development Setup
43 |
44 | ### Prerequisites
45 |
46 | - Node.js (latest LTS version recommended)
47 | - npm or pnpm
48 |
49 | ### Setup Steps
50 |
51 | ```bash
52 | # Clone your fork
53 | git clone https://github.com/YOUR_USERNAME/reca.git
54 | cd reca
55 |
56 | # Install dependencies
57 | npm install
58 |
59 | # Build packages
60 | npm run build
61 |
62 | # Run tests
63 | npm test
64 | ```
65 |
66 | ## Coding Standards
67 |
68 | This project uses @labeg/code-style for linting. Key principles:
69 |
70 | ### Code Style
71 |
72 | - **Line length**: Maximum 120 characters
73 | - **Indentation**: 4 spaces
74 | - **Quotes**: Use double quotes `"` or template literals `` ` ``
75 | - **Semicolons**: Always use semicolons
76 | - **Braces**: Always use braces for control structures
77 | - **Clean Architecture**: Keep stores, services, and components separated
78 |
79 | ### Example
80 |
81 | ```typescript
82 | // Good - Clean Store
83 | export class TodoStore extends AutoStore {
84 | public todos: string[] = [];
85 |
86 | public addTodo(todo: string): void {
87 | this.todos.push(todo);
88 | }
89 | }
90 |
91 | // Bad - Logic in component
92 | const Component = () => {
93 | const [todos, setTodos] = useState([]);
94 | // Don't put business logic here
95 | };
96 | ```
97 |
98 | ### Commit Messages
99 |
100 | Follow [Conventional Commits](https://www.conventionalcommits.org/):
101 |
102 | ```bash
103 | feat: add support for new ESLint rule
104 | fix: correct TypeScript configuration issue
105 | docs: update README with new examples
106 | chore: upgrade dependencies
107 | ```
108 |
109 | ## Testing
110 |
111 | Before submitting your PR:
112 |
113 | ```bash
114 | # Run all tests
115 | npm test
116 |
117 | # Run linter
118 | npm run lint
119 |
120 | # Build packages
121 | npm run build
122 |
123 | # Check for security vulnerabilities
124 | npm audit
125 | ```
126 |
127 | All tests must pass before your PR can be merged.
128 |
129 | ## Review Process
130 |
131 | 1. **Automated checks** run on every PR (tests, linting, security)
132 | 2. **Manual review** by maintainers
133 | 3. **Feedback** may be provided - please address comments
134 | 4. **Approval** - once approved, your PR will be merged
135 |
136 | ## Release Process
137 |
138 | Releases are automated:
139 |
140 | 1. Maintainer merges PR to `main`
141 | 2. Version is bumped automatically
142 | 3. Changelog is generated
143 | 4. Package is published to npm
144 | 5. GitHub release is created
145 |
146 | ## Questions?
147 |
148 | - Open an issue with the `question` label
149 | - Check existing issues and discussions
150 |
151 | ## License
152 |
153 | By contributing, you agree that your contributions will be licensed under the MIT License.
154 |
155 | ---
156 |
157 | Thank you for contributing to ReCA! 🎉
158 |
--------------------------------------------------------------------------------
/packages/reca-docs/app/page.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .description {
11 | display: inherit;
12 | justify-content: inherit;
13 | align-items: inherit;
14 | font-size: 0.85rem;
15 | max-width: var(--max-width);
16 | width: 100%;
17 | z-index: 2;
18 | font-family: var(--font-mono);
19 | }
20 |
21 | .description a {
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | gap: 0.5rem;
26 | }
27 |
28 | .description p {
29 | position: relative;
30 | margin: 0;
31 | padding: 1rem;
32 | background-color: rgba(var(--callout-rgb), 0.5);
33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34 | border-radius: var(--border-radius);
35 | }
36 |
37 | .code {
38 | font-weight: 700;
39 | font-family: var(--font-mono);
40 | }
41 |
42 | .grid {
43 | display: grid;
44 | grid-template-columns: repeat(4, minmax(25%, auto));
45 | max-width: 100%;
46 | width: var(--max-width);
47 | }
48 |
49 | .card {
50 | padding: 1rem 1.2rem;
51 | border-radius: var(--border-radius);
52 | background: rgba(var(--card-rgb), 0);
53 | border: 1px solid rgba(var(--card-border-rgb), 0);
54 | transition: background 200ms, border 200ms;
55 | }
56 |
57 | .card span {
58 | display: inline-block;
59 | transition: transform 200ms;
60 | }
61 |
62 | .card h2 {
63 | font-weight: 600;
64 | margin-bottom: 0.7rem;
65 | }
66 |
67 | .card p {
68 | margin: 0;
69 | opacity: 0.6;
70 | font-size: 0.9rem;
71 | line-height: 1.5;
72 | max-width: 30ch;
73 | }
74 |
75 | .center {
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | position: relative;
80 | padding: 4rem 0;
81 | }
82 |
83 | .center::before {
84 | background: var(--secondary-glow);
85 | border-radius: 50%;
86 | width: 480px;
87 | height: 360px;
88 | margin-left: -400px;
89 | }
90 |
91 | .center::after {
92 | background: var(--primary-glow);
93 | width: 240px;
94 | height: 180px;
95 | z-index: -1;
96 | }
97 |
98 | .center::before,
99 | .center::after {
100 | content: '';
101 | left: 50%;
102 | position: absolute;
103 | filter: blur(45px);
104 | transform: translateZ(0);
105 | }
106 |
107 | .logo {
108 | position: relative;
109 | }
110 | /* Enable hover only on non-touch devices */
111 | @media (hover: hover) and (pointer: fine) {
112 | .card:hover {
113 | background: rgba(var(--card-rgb), 0.1);
114 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
115 | }
116 |
117 | .card:hover span {
118 | transform: translateX(4px);
119 | }
120 | }
121 |
122 | @media (prefers-reduced-motion) {
123 | .card:hover span {
124 | transform: none;
125 | }
126 | }
127 |
128 | /* Mobile */
129 | @media (max-width: 700px) {
130 | .content {
131 | padding: 4rem;
132 | }
133 |
134 | .grid {
135 | grid-template-columns: 1fr;
136 | margin-bottom: 120px;
137 | max-width: 320px;
138 | text-align: center;
139 | }
140 |
141 | .card {
142 | padding: 1rem 2.5rem;
143 | }
144 |
145 | .card h2 {
146 | margin-bottom: 0.5rem;
147 | }
148 |
149 | .center {
150 | padding: 8rem 0 6rem;
151 | }
152 |
153 | .center::before {
154 | transform: none;
155 | height: 300px;
156 | }
157 |
158 | .description {
159 | font-size: 0.8rem;
160 | }
161 |
162 | .description a {
163 | padding: 1rem;
164 | }
165 |
166 | .description p,
167 | .description div {
168 | display: flex;
169 | justify-content: center;
170 | position: fixed;
171 | width: 100%;
172 | }
173 |
174 | .description p {
175 | align-items: center;
176 | inset: 0 0 auto;
177 | padding: 2rem 1rem 1.4rem;
178 | border-radius: 0;
179 | border: none;
180 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
181 | background: linear-gradient(
182 | to bottom,
183 | rgba(var(--background-start-rgb), 1),
184 | rgba(var(--callout-rgb), 0.5)
185 | );
186 | background-clip: padding-box;
187 | backdrop-filter: blur(24px);
188 | }
189 |
190 | .description div {
191 | align-items: flex-end;
192 | pointer-events: none;
193 | inset: auto 0 0;
194 | padding: 2rem;
195 | height: 200px;
196 | background: linear-gradient(
197 | to bottom,
198 | transparent 0%,
199 | rgb(var(--background-end-rgb)) 40%
200 | );
201 | z-index: 1;
202 | }
203 | }
204 |
205 | /* Tablet and Smaller Desktop */
206 | @media (min-width: 701px) and (max-width: 1120px) {
207 | .grid {
208 | grid-template-columns: repeat(2, 50%);
209 | }
210 | }
211 |
212 | @media (prefers-color-scheme: dark) {
213 | .vercelLogo {
214 | filter: invert(1);
215 | }
216 |
217 | .logo {
218 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
219 | }
220 | }
221 |
222 | @keyframes rotate {
223 | from {
224 | transform: rotate(360deg);
225 | }
226 | to {
227 | transform: rotate(0deg);
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/packages/reca/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [2.4.3](https://github.com/LabEG/reca/compare/v2.0.17...v2.4.3) (2025-12-17)
6 |
7 | ### [2.4.2](https://github.com/LabEG/reca/compare/v2.0.16...v2.4.2) (2025-12-14)
8 |
9 | ### [2.4.1](https://github.com/LabEG/reca/compare/v2.0.15...v2.4.1) (2025-12-14)
10 |
11 | ## [2.4.0](https://github.com/LabEG/reca/compare/v2.0.14...v2.4.0) (2025-12-14)
12 |
13 |
14 | ### Features
15 |
16 | * add README copying step to npm publish workflow ([ee93ac9](https://github.com/LabEG/reca/commit/ee93ac9ffbc358cb0f3a1d595badc05e2e2f2175))
17 |
18 | ### [2.3.8](https://github.com/LabEG/reca/compare/v2.0.13...v2.3.8) (2025-12-14)
19 |
20 | ### [2.3.7](https://github.com/LabEG/reca/compare/v2.0.12...v2.3.7) (2025-12-14)
21 |
22 | ### [2.3.6](https://github.com/LabEG/reca/compare/v2.0.11...v2.3.6) (2025-12-14)
23 |
24 | ### [2.3.5](https://github.com/LabEG/reca/compare/v2.0.10...v2.3.5) (2025-12-14)
25 |
26 |
27 | ### Bug Fixes
28 |
29 | * update build status badge in README.md ([8ce28a1](https://github.com/LabEG/reca/commit/8ce28a1c4d1db9e926020754d024b9ebab9b5c04))
30 |
31 | ### [2.3.4](https://github.com/LabEG/reca/compare/v2.0.9...v2.3.4) (2025-12-14)
32 |
33 |
34 | ### Bug Fixes
35 |
36 | * update README and package.json for accurate badge links and repository metadata ([d834244](https://github.com/LabEG/reca/commit/d834244dd115b98c368d37a271dba2161c49d383))
37 |
38 | ### [2.3.3](https://github.com/LabEG/reca/compare/v2.0.8...v2.3.3) (2025-12-14)
39 |
40 |
41 | ### Bug Fixes
42 |
43 | * simplify NPM publish command by removing unnecessary directory change ([a218baa](https://github.com/LabEG/reca/commit/a218baa7f722281e27d9e3adc957f9a50a2491cd))
44 |
45 | ### [2.3.2](https://github.com/LabEG/reca/compare/v2.0.7...v2.3.2) (2025-12-14)
46 |
47 | ### [2.3.1](https://github.com/LabEG/reca/compare/v2.0.6...v2.3.1) (2025-12-14)
48 |
49 |
50 | ### Bug Fixes
51 |
52 | * remove dry-run build verification step from test workflow ([ba43452](https://github.com/LabEG/reca/commit/ba434522259363d3fae55fc41329565e2b39995c))
53 |
54 | ## [2.3.0](https://github.com/LabEG/reca/compare/v2.0.5...v2.3.0) (2025-12-14)
55 |
56 |
57 | ### Features
58 |
59 | * add issue templates for bug reports and feature requests, and implement security policy ([212d1a7](https://github.com/LabEG/reca/commit/212d1a7eba238117674a9ac93f3371157ab1e7b6))
60 |
61 |
62 | ### Bug Fixes
63 |
64 | * add test script to package.json for linting during testing ([3f0ca92](https://github.com/LabEG/reca/commit/3f0ca921720cc6e87af7bef2ec392e81572786d3))
65 | * correct files field in package.json to include necessary files ([2fa2202](https://github.com/LabEG/reca/commit/2fa220248faf3460b085a3056fbfba257ce67804))
66 | * reorder badges and improve README structure for clarity ([1cd8c17](https://github.com/LabEG/reca/commit/1cd8c17084d55a1a48193dfe1d2c3e2f199f73de))
67 | * update badge order and remove duplicate Codacy badge in README ([f3d0370](https://github.com/LabEG/reca/commit/f3d0370d4fd373faf4505986f1ea60fa8f9b0ace))
68 | * update workflows and documentation to use 'main' branch instead of 'master' ([d47bbef](https://github.com/LabEG/reca/commit/d47bbef2dc9bef04fd8ac48f6ae8445a85ebdc93))
69 |
70 | ## [2.2.0](https://github.com/LabEG/reca/compare/v2.0.4...v2.2.0) (2025-04-10)
71 |
72 |
73 | ### Features
74 |
75 | * update packages versions ([2067058](https://github.com/LabEG/reca/commit/2067058f8500acfcc2e6e6a054858a2fb673ad7d))
76 |
77 | ### [2.1.3](https://github.com/LabEG/reca/compare/v2.0.3...v2.1.3) (2025-01-03)
78 |
79 | ### [2.1.2](https://github.com/LabEG/reca/compare/v2.0.2...v2.1.2) (2025-01-03)
80 |
81 | ### [2.1.1](https://github.com/LabEG/reca/compare/v2.0.1...v2.1.1) (2025-01-03)
82 |
83 | ## [2.1.0](https://github.com/LabEG/reca/compare/v0.0.8...v2.1.0) (2025-01-03)
84 |
85 |
86 | ### Features
87 |
88 | * add notAuto and notRedraw decorators for auto store ([b14f3ea](https://github.com/LabEG/reca/commit/b14f3eae7b330fb244c1c72038dda041edb75f98))
89 | * add performance unit test ([9e1e1fe](https://github.com/LabEG/reca/commit/9e1e1feab7e6924823dcafe5abd4d241c936ea93))
90 | * add support notAuto decorator for auto store properties ([7729767](https://github.com/LabEG/reca/commit/772976752da7f40cfc280d19554ea98db3fddd14))
91 | * add support server side components ([ab2feee](https://github.com/LabEG/reca/commit/ab2feee6e8aef6c6f0a3c69a5819ec1154a81a9d))
92 | * add support unit tests for nodejs engine, write unit test for sample todo component ([8937e1a](https://github.com/LabEG/reca/commit/8937e1a7ba7fcef95d70060737ab9be11bf3baa7))
93 | * update packages versions ([8fb150d](https://github.com/LabEG/reca/commit/8fb150dd83a81fce01ef99983f9dcd4111529480))
94 |
95 |
96 | ### Bug Fixes
97 |
98 | * exports decorators for module type ([78330cc](https://github.com/LabEG/reca/commit/78330ccb62451c199bc0688f312990f02a35592d))
99 | * props interface in params detection ([788afc3](https://github.com/LabEG/reca/commit/788afc3627f06b8b757a25b933e6db7585386c8d))
100 | * test for publishing ([8730a3d](https://github.com/LabEG/reca/commit/8730a3d9453addf8b437e42568a69778bc762b3c))
101 |
102 | ### [2.0.1](https://github.com/LabEG/reca/compare/v1.1.13...v2.0.1) (2023-12-22)
103 |
--------------------------------------------------------------------------------
/packages/reca-docs/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import styles from "./page.module.css";
3 | import {AutoStore, reflection, useStoreAsync} from "reca";
4 | import {JSX} from "react";
5 |
6 | @reflection
7 | class TStore extends AutoStore {
8 |
9 | public prop = 0;
10 |
11 | // eslint-disable-next-line class-methods-use-this
12 | public async testAsync (): Promise {
13 | return await Promise.resolve(5);
14 | }
15 |
16 | }
17 |
18 | // eslint-disable-next-line max-lines-per-function
19 | const Home = async (): Promise => {
20 | // eslint-disable-next-line react-hooks/rules-of-hooks
21 | const store = await useStoreAsync(TStore);
22 |
23 | const num = await store.testAsync();
24 | // eslint-disable-next-line no-console
25 | console.log("111111111111", store.prop, num);
26 | return (
27 |
28 |
29 |
30 | Get started by editing
31 | {" "}
32 |
33 |
34 | app/page.tsx
35 |
36 |
37 |
38 |
57 |
58 |
59 |
60 |
68 |
69 |
70 |
151 |
152 | );
153 | };
154 |
155 | export default Home;
156 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, caste, color, religion, or sexual
10 | identity and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the overall
26 | community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or advances of
31 | any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email address,
35 | without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | labeg@mail.ru or [discord server](https://discordapp.com/channels/974049080454045796/974049142022209566).
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series of
86 | actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or permanent
93 | ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within the
113 | community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.1, available at
119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120 |
121 | Community Impact Guidelines were inspired by
122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123 |
124 | For answers to common questions about this code of conduct, see the FAQ at
125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126 | [https://www.contributor-covenant.org/translations][translations].
127 |
128 | [homepage]: https://www.contributor-covenant.org
129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130 | [Mozilla CoC]: https://github.com/mozilla/diversity
131 | [FAQ]: https://www.contributor-covenant.org/faq
132 | [translations]: https://www.contributor-covenant.org/translations
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [2.0.18](https://github.com/LabEG/reca/compare/v2.4.3...v2.0.18) (2025-12-17)
6 |
7 | ### [2.0.17](https://github.com/LabEG/reca/compare/v2.4.2...v2.0.17) (2025-12-14)
8 |
9 | ### [2.0.16](https://github.com/LabEG/reca/compare/v2.4.1...v2.0.16) (2025-12-14)
10 |
11 | ### [2.0.15](https://github.com/LabEG/reca/compare/v2.4.0...v2.0.15) (2025-12-14)
12 |
13 | ### [2.0.14](https://github.com/LabEG/reca/compare/v2.3.8...v2.0.14) (2025-12-14)
14 |
15 | ### [2.0.13](https://github.com/LabEG/reca/compare/v2.3.7...v2.0.13) (2025-12-14)
16 |
17 | ### [2.0.12](https://github.com/LabEG/reca/compare/v2.3.6...v2.0.12) (2025-12-14)
18 |
19 | ### [2.0.11](https://github.com/LabEG/reca/compare/v2.3.5...v2.0.11) (2025-12-14)
20 |
21 | ### [2.0.10](https://github.com/LabEG/reca/compare/v2.3.4...v2.0.10) (2025-12-14)
22 |
23 | ### [2.0.9](https://github.com/LabEG/reca/compare/v2.3.3...v2.0.9) (2025-12-14)
24 |
25 | ### [2.0.8](https://github.com/LabEG/reca/compare/v2.3.2...v2.0.8) (2025-12-14)
26 |
27 | ### [2.0.7](https://github.com/LabEG/reca/compare/v2.3.1...v2.0.7) (2025-12-14)
28 |
29 | ### [2.0.6](https://github.com/LabEG/reca/compare/v2.3.0...v2.0.6) (2025-12-14)
30 |
31 | ### [2.0.5](https://github.com/LabEG/reca/compare/v2.2.0...v2.0.5) (2025-04-10)
32 |
33 | ### [2.0.4](https://github.com/LabEG/reca/compare/v2.1.3...v2.0.4) (2025-01-03)
34 |
35 | ### [2.0.3](https://github.com/LabEG/reca/compare/v2.1.2...v2.0.3) (2025-01-03)
36 |
37 | ### [2.0.2](https://github.com/LabEG/reca/compare/v2.1.1...v2.0.2) (2025-01-03)
38 |
39 | ### [2.0.1](https://github.com/LabEG/reca/compare/v2.1.0...v2.0.1) (2025-01-03)
40 |
41 | ### [1.1.13](https://github.com/LabEG/reca/compare/v1.1.12...v1.1.13) (2023-12-16)
42 |
43 | ### [1.1.12](https://github.com/LabEG/reca/compare/v1.1.11...v1.1.12) (2023-12-16)
44 |
45 | ### [1.1.11](https://github.com/LabEG/reca/compare/v1.1.10...v1.1.11) (2023-12-02)
46 |
47 | ### [1.1.10](https://github.com/LabEG/reca/compare/v1.1.9...v1.1.10) (2023-12-02)
48 |
49 | ### [1.1.9](https://github.com/LabEG/reca/compare/v1.1.8...v1.1.9) (2023-09-01)
50 |
51 | ### [1.1.8](https://github.com/LabEG/reca/compare/v1.1.5...v1.1.8) (2023-07-14)
52 |
53 | ### [1.1.7](https://github.com/LabEG/reca/compare/v1.1.6...v1.1.7) (2023-06-25)
54 |
55 | ### [1.1.6](https://github.com/LabEG/reca/compare/v1.1.2...v1.1.6) (2023-06-25)
56 |
57 | ### [1.1.5](https://github.com/LabEG/reca/compare/v1.1.4...v1.1.5) (2023-02-09)
58 |
59 | ### [1.1.4](https://github.com/LabEG/reca/compare/v1.1.3...v1.1.4) (2023-02-09)
60 |
61 | ### [1.1.3](https://github.com/LabEG/reca/compare/v1.1.2...v1.1.3) (2023-02-08)
62 |
63 | ### [1.1.2](https://github.com/LabEG/reca/compare/v1.1.1...v1.1.2) (2022-12-10)
64 |
65 | ### [1.1.1](https://github.com/LabEG/reca/compare/v1.1.0...v1.1.1) (2022-11-22)
66 |
67 | ## [1.1.0](https://github.com/LabEG/reca/compare/v0.0.13...v1.1.0) (2022-11-22)
68 |
69 |
70 | ### Features
71 |
72 | * add support notAuto decorator for auto store properties ([7729767](https://github.com/LabEG/reca/commit/772976752da7f40cfc280d19554ea98db3fddd14))
73 |
74 |
75 | ### Bug Fixes
76 |
77 | * exports decorators for module type ([78330cc](https://github.com/LabEG/reca/commit/78330ccb62451c199bc0688f312990f02a35592d))
78 |
79 | ### [0.0.13](https://github.com/LabEG/reca/compare/v0.0.12...v0.0.13) (2022-07-26)
80 |
81 |
82 | ### Features
83 |
84 | * add notAuto and notRedraw decorators for auto store ([b14f3ea](https://github.com/LabEG/reca/commit/b14f3eae7b330fb244c1c72038dda041edb75f98))
85 |
86 | ### [0.0.12](https://github.com/LabEG/reca/compare/v0.0.11...v0.0.12) (2022-07-13)
87 |
88 | ### [0.0.11](https://github.com/LabEG/reca/compare/v0.0.10...v0.0.11) (2022-07-13)
89 |
90 |
91 | ### Bug Fixes
92 |
93 | * props interface in params detection ([788afc3](https://github.com/LabEG/reca/commit/788afc3627f06b8b757a25b933e6db7585386c8d))
94 | * test for publishing ([8730a3d](https://github.com/LabEG/reca/commit/8730a3d9453addf8b437e42568a69778bc762b3c))
95 |
96 | ### [0.0.10](https://github.com/LabEG/reca/compare/v0.0.9...v0.0.10) (2022-07-01)
97 |
98 | ### [0.0.9](https://github.com/LabEG/reca/compare/v0.0.8...v0.0.9) (2022-06-30)
99 |
100 |
101 | ### Features
102 |
103 | * add performance unit test ([9e1e1fe](https://github.com/LabEG/reca/commit/9e1e1feab7e6924823dcafe5abd4d241c936ea93))
104 | * add support unit tests for nodejs engine, write unit test for sample todo component ([8937e1a](https://github.com/LabEG/reca/commit/8937e1a7ba7fcef95d70060737ab9be11bf3baa7))
105 |
106 | ### [0.0.8](https://github.com/LabEG/reca/compare/v0.0.7...v0.0.8) (2022-06-15)
107 |
108 |
109 | ### Features
110 |
111 | * complete autostore, add autostore tests, add propsUpdate livacycle method ([7930f4a](https://github.com/LabEG/reca/commit/7930f4a7f39e8560ea13598f5d442fcc52197b43))
112 |
113 | ### [0.0.7](https://github.com/LabEG/reca/compare/v0.0.6...v0.0.7) (2022-05-29)
114 |
115 |
116 | ### Bug Fixes
117 |
118 | * autostore self redraw ([5ce5a36](https://github.com/LabEG/reca/commit/5ce5a364f80b4c92b739577eaa2869bf7c804da4))
119 |
120 | ### [0.0.6](https://github.com/LabEG/reca/compare/v0.0.5...v0.0.6) (2022-05-28)
121 |
122 |
123 | ### Features
124 |
125 | * complete autostore ([72bd447](https://github.com/LabEG/reca/commit/72bd44744c535118e7516221168580aad9a78085))
126 | * fix build and publish new version ([4c7b67e](https://github.com/LabEG/reca/commit/4c7b67ec163a08ce6061b31463a14b8d24cdf109))
127 |
128 |
129 | ### Bug Fixes
130 |
131 | * module import ([9c5ea11](https://github.com/LabEG/reca/commit/9c5ea11de04a8bbb0ab557547edab69a99384b87))
132 |
133 | ### [0.0.5](https://github.com/LabEG/reca/compare/v0.0.4...v0.0.5) (2022-05-28)
134 |
135 | ### [0.0.4](https://github.com/LabEG/reca/compare/v0.0.3...v0.0.4) (2022-05-28)
136 |
137 |
138 | ### Features
139 |
140 | * add support pass props in contructor ([c22c639](https://github.com/LabEG/reca/commit/c22c639d5b5f66394fc50baf9c32d6280e1da045))
141 | * begin making autostore ([c0dea00](https://github.com/LabEG/reca/commit/c0dea005e561dc1c2f661c7cc04de789281188b6))
142 | * replace enzyme by resct test library, update react to version 18 ([cca36a7](https://github.com/LabEG/reca/commit/cca36a76b8d0243d976c93181a8001f4947b3f67))
143 |
144 | ### 0.0.3 (2022-05-11)
145 |
146 |
147 | ### Bug Fixes
148 |
149 | * complete husky setup ([5f2bb1d](https://github.com/LabEG/reca/commit/5f2bb1d5b182b50b6fd4b4286946113d9af6bd38))
150 | * livecycle methods ([66889f8](https://github.com/LabEG/reca/commit/66889f81ca236efdec14dbb850533a1f6edd5b86))
151 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReCA - React Clean Architecture state manager
2 |
3 | [](https://www.npmjs.com/package/reca)
4 | [](https://github.com/LabEG/reca/blob/main/LICENSE)
5 | 
6 | [](https://www.codacy.com/gh/LabEG/reca/dashboard?utm_source=github.com&utm_medium=referral&utm_content=LabEG/reca&utm_campaign=Badge_Grade)
7 | 
8 | [](https://github.com/LabEG/reca/security/code-scanning)
9 | [](CODE_OF_CONDUCT.md)
10 |
11 | Created at the intersection of Functional style and OOP technologies. It is based on the simplicity of the functional style of the view, enriched with OOP technologies for writing business logic. Perfect for beginner developers and complex enterprise applications
12 |
13 | ## Features
14 |
15 | - **Microstores** - calculations state of components don't affect to other components, small CPU usage for update states,
16 | - **Direct Functions Call** - don't need heavy CPU utilization for search function in reducer, just call the function directly,
17 | - **No Boilerplate** - write only business code without those debt,
18 | - **Dependency Injection** - override any part of your application for unit test or other customer,
19 | - **Microfrontend** - perfect support microfrontends out the box without any boilerplates,
20 | - **Simple Data Flow** - don't need search functions call chain for debug your reducers,
21 | - **Code Organization** - structures the code easily even for large enterprise applications,
22 | - **Extra Small Size** - only 1kb of minified code.
23 |
24 | ## Comparison with Other Libraries
25 |
26 | | Feature | ReCA | Zustand | MobX | Redux |
27 | |---------|------|----------|------|-------|
28 | | **Bundle Size** | ~1KB | ~1KB | ~16KB | ~8KB |
29 | | **Boilerplate** | Minimal | Minimal | Medium | Heavy |
30 | | **Learning Curve** | Easy | Easy | Medium | Steep |
31 | | **TypeScript** | Built-in | Good | Good | Good |
32 | | **Performance** | Excellent | Excellent | Excellent | Good |
33 | | **Dependency Injection** | ✅ Built-in | ❌ Manual | ❌ Manual | ❌ Manual |
34 | | **Clean Architecture** | ✅ Native | ❌ Limited | ⚠️ Requires setup | ⚠️ Requires setup |
35 | | **Microstores** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ Monostore |
36 | | **SSR Support** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
37 | | **Middleware** | Via DI | ✅ Yes | ❌ Limited | ✅ Yes |
38 | | **Async Actions** | ✅ Native | ✅ Native | ✅ Native | ⚠️ Requires thunk/saga |
39 |
40 | ### Why Choose ReCA?
41 |
42 | - **Smallest footprint** - Only 1KB minified, same as Zustand but with more features
43 | - **Zero boilerplate** - No actions, reducers, or dispatchers needed
44 | - **Enterprise-ready** - Built-in DI and Clean Architecture support
45 | - **Developer-friendly** - Simple API, easy to learn and use
46 | - **Flexible** - Choose between AutoStore (automatic) or Store (manual control)
47 | - **Type-safe** - Full TypeScript support out of the box
48 |
49 | ## Installation
50 |
51 | ### Using npm
52 |
53 | ```bash
54 | npm install reca reflect-metadata
55 | ```
56 |
57 | ### Using yarn
58 |
59 | ```bash
60 | yarn add reca reflect-metadata
61 | ```
62 |
63 | ### Using pnpm
64 |
65 | ```bash
66 | pnpm add reca reflect-metadata
67 | ```
68 |
69 | ### Setup
70 |
71 | After installation, import `reflect-metadata` at the entry point of your application (e.g., `index.tsx` or `main.tsx`):
72 |
73 | ```typescript
74 | import "reflect-metadata";
75 | import React from "react";
76 | import ReactDOM from "react-dom/client";
77 | import { App } from "./App";
78 |
79 | ReactDOM.createRoot(document.getElementById("root")!).render( );
80 | ```
81 |
82 | > **Note:** The `reflect-metadata` package is required for dependency injection functionality. Make sure to import it before any other imports in your application entry point.
83 |
84 | ## Examples
85 |
86 | ### Quick Start - Counter Example
87 |
88 | A simple example to get you started with ReCA:
89 |
90 | ```typescript
91 | // counter.store.ts
92 | import { AutoStore } from "reca";
93 |
94 | export class CounterStore extends AutoStore {
95 | public count: number = 0;
96 |
97 | public increment(): void {
98 | this.count++;
99 | }
100 |
101 | public decrement(): void {
102 | this.count--;
103 | }
104 | }
105 |
106 | // Counter.tsx
107 | import { useStore } from "reca";
108 | import { CounterStore } from "./stores/counter.store";
109 |
110 | export const Counter = () => {
111 | const store = useStore(CounterStore);
112 |
113 | return (
114 |
115 |
Count: {store.count}
116 | +1
117 | -1
118 |
119 | );
120 | };
121 | ```
122 |
123 | ### ToDo Example
124 |
125 | Create your Store by inheriting from AutoStore, and use it in a component via useStore hook.
126 |
127 | ``` typescript
128 | // todo.store.ts
129 | import {AutoStore} from "reca";
130 | import type {FormEvent} from "react";
131 |
132 | export class ToDoStore extends AutoStore {
133 |
134 | public currentTodo: string = "";
135 |
136 | public todos: string[] = [];
137 |
138 | public handleAddTodo (): void {
139 | this.todos.push(this.currentTodo);
140 | }
141 |
142 | public handleDeleteTodo (index: number): void {
143 | this.todos.splice(index, 1);
144 | }
145 |
146 | public handleCurrentEdit (event: FormEvent): void {
147 | this.currentTodo = event.currentTarget.value;
148 | }
149 |
150 | }
151 |
152 |
153 | // todo.component.ts
154 | import {useStore} from "reca";
155 | import {ToDoStore} from "../stores/todo.store";
156 |
157 | export const ToDoComponent = (): JSX.Element => {
158 | const store = useStore(ToDoStore);
159 |
160 | return (
161 |
162 |
163 | {
164 | store.todos.map((todo, index) => (
165 |
166 | {todo}
167 |
168 | store.handleDeleteTodo(index)}
171 | type="button"
172 | >
173 | X
174 |
175 |
176 | ))
177 | }
178 |
179 |
180 |
181 |
185 |
186 |
190 | add
191 |
192 |
193 |
194 | );
195 | };
196 | ```
197 |
198 | ### Example low-level Store
199 |
200 | Also, if you need uncompromising performance, you can use the low-level Store. But you will need to start redrawing manually using the `this.redraw()` method. Also you must pass arrow function to all used HTMLElement events, such as onClick.
201 |
202 | ``` typescript
203 | // todo.store.ts
204 | import {Store} from "reca";
205 | import type {FormEvent} from "react";
206 |
207 | export class ToDoStore extends Store {
208 |
209 | public currentTodo: string = "";
210 |
211 | public todos: string[] = [];
212 |
213 | public handleAddTodo (): void {
214 | this.todos.push(this.currentTodo);
215 | this.redraw();
216 | }
217 |
218 | public handleDeleteTodo (index: number): void {
219 | this.todos.splice(index, 1);
220 | this.redraw();
221 | }
222 |
223 | public handleCurrentEdit (event: FormEvent): void {
224 | this.currentTodo = event.currentTarget.value;
225 | this.redraw();
226 | }
227 | }
228 |
229 |
230 | // todo.component.ts
231 | import {useStore} from "reca";
232 | import {ToDoStore} from "../stores/todo.store";
233 |
234 | export const ToDoComponent = (): JSX.Element => {
235 | const store = useStore(ToDoStore);
236 |
237 | return (
238 |
239 | ...
240 |
241 |
242 | store.handleCurrentEdit()}
244 | value={store.currentTodo}
245 | />
246 |
247 | store.handleAddTodo()}
249 | type="button"
250 | >
251 | add
252 |
253 |
254 |
255 | );
256 | };
257 | ```
258 |
259 | ### Advanced Example - Dependency Injection for Enterprise Applications
260 |
261 | This example demonstrates how to build scalable enterprise applications using ReCA with Dependency Injection. It shows the simplicity of business logic organization following Clean Architecture principles.
262 |
263 | The example includes:
264 |
265 | - **Service Layer** - encapsulates business logic and external API calls
266 | - **Model Layer** - defines data structures
267 | - **Store Layer** - manages state and coordinates services
268 | - **Component Layer** - pure view logic
269 |
270 | This architecture makes your code:
271 |
272 | - **Testable** - easily mock services for unit tests
273 | - **Maintainable** - clear separation of concerns
274 | - **Scalable** - add new features without modifying existing code
275 | - **Flexible** - swap implementations through DI (e.g., Repository, Provider, Logger)
276 |
277 | ```typescript
278 | // SpaceXCompanyInfo.ts
279 | export class SpaceXCompanyInfo {
280 |
281 | public name: string = "";
282 |
283 | public founder: string = "";
284 |
285 | public employees: number = 0;
286 |
287 | public applyData (json: object): this {
288 | Object.assign(this, json);
289 | return this;
290 | }
291 |
292 | }
293 |
294 |
295 | // SpaceXService.ts
296 | import {reflection} from "first-di";
297 | import {SpaceXCompanyInfo} from "../models/SpaceXCompanyInfo";
298 |
299 | @reflection
300 | export class SpaceXService {
301 |
302 | public async getCompanyInfo (): Promise {
303 | const response = await fetch("https://api.spacexdata.com/v3/info");
304 | const json: unknown = await response.json();
305 |
306 | // ... and manies manies lines of logics
307 |
308 | if (typeof json === "object" && json !== null) {
309 | return new SpaceXCompanyInfo().applyData(json);
310 | }
311 | throw new Error("SpaceXService.getCompanyInfo: response object is not json");
312 | }
313 |
314 | }
315 |
316 |
317 | // SpaceXStore.ts
318 | import {reflection} from "first-di";
319 | import {AutoStore} from "reca";
320 | import {SpaceXCompanyInfo} from "../models/SpaceXCompanyInfo.js";
321 | import {SpaceXService} from "../services/SpaceXService.js";
322 |
323 | @reflection
324 | export class SpaceXStore extends AutoStore {
325 |
326 | public companyInfo: SpaceXCompanyInfo = new SpaceXCompanyInfo();
327 |
328 | public constructor (
329 | private readonly spaceXService: SpaceXService,
330 | // private readonly logger: Logger
331 | ) {
332 | super();
333 | }
334 |
335 | public activate (): void {
336 | this.fetchCompanyInfo();
337 | }
338 |
339 | private async fetchCompanyInfo (): Promise {
340 | try {
341 | this.companyInfo = await this.spaceXService.getCompanyInfo();
342 | } catch (error) {
343 | // Process exceptions, ex: this.logger.error(error.message);
344 | }
345 | }
346 |
347 | }
348 |
349 |
350 | // SpaceXComponent.tsx
351 | import {useStore} from "reca";
352 | import {SpaceXStore} from "../stores/SpaceXStore.js";
353 |
354 | export const TestStoreComponent = (): JSX.Element => {
355 | const store = useStore(SpaceXStore);
356 |
357 | return (
358 |
359 |
360 | Company:
361 | {" "}
362 |
363 | {store.companyInfo.name}
364 |
365 |
366 |
367 | Founder:
368 | {" "}
369 |
370 | {store.companyInfo.founder}
371 |
372 |
373 | );
374 | };
375 |
376 | ```
377 |
378 | ## Documentation and Resources
379 |
380 | ### Documentation
381 |
382 | - **[Wiki](https://github.com/LabEG/reca/wiki)** - Comprehensive guides, tutorials, and API reference
383 | - **[API Documentation](https://github.com/LabEG/reca/wiki)** - Detailed API documentation for all features
384 |
385 | ### Community and Support
386 |
387 | - **[Discord Server](https://discordapp.com/channels/974049080454045796/974049142022209566)** - Join our community for real-time help and discussions
388 | - **[GitHub Discussions](https://github.com/LabEG/reca/discussions)** - Ask questions and share ideas
389 | - **[GitHub Issues](https://github.com/LabEG/reca/issues)** - Report bugs or request features
390 |
391 | ### Contributing
392 |
393 | We welcome contributions! See our:
394 |
395 | - **[Contributing Guide](CONTRIBUTING.md)** - Learn how to contribute to the project
396 | - **[Code of Conduct](CODE_OF_CONDUCT.md)** - Our community guidelines
397 | - **[Security Policy](SECURITY.md)** - How to report security vulnerabilities
398 |
399 | ## License
400 |
401 | ReCA is [MIT licensed](https://github.com/LabEG/reca/blob/main/LICENSE).
402 |
--------------------------------------------------------------------------------