├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── contexts │ ├── index.ts │ └── types.ts ├── helpers │ ├── createAsyncAction.ts │ ├── createSignal.ts │ ├── createStore.ts │ └── types.ts ├── hooks │ ├── types.ts │ ├── useAction.ts │ ├── useActions.ts │ ├── useAllSignals.ts │ ├── useAsyncActions.ts │ ├── useOperations.ts │ └── useSignal.ts ├── index.ts ├── interfaces │ ├── builder.ts │ └── builderCase.ts ├── providers │ ├── index.tsx │ ├── reducer.ts │ └── types.ts └── tests │ ├── gx │ ├── signals │ │ └── counter.ts │ └── store │ │ └── index.ts │ ├── signals.test.ts │ └── store.test.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": ["standard-with-typescript", "plugin:react/recommended"], 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["react", "immutable"], 13 | "rules": { 14 | "immutable/no-mutation": "error", 15 | "no-console": "error" 16 | }, 17 | "ignorePatterns": ["node_modules/*", "dist/*", "/src/tests/*"] // Add the files or folders you want to exclude here 18 | } 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | tsconfig.json 4 | .babelrc 5 | yarn.lock 6 | .eslintrc.json 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Gx 2 | 3 | We welcome contributions to `Gx` from anyone interested in improving the project. This document outlines the guidelines and process for contributing to the project. 4 | 5 | ## Ways to Contribute 6 | 7 | There are many ways to contribute to the project, including: 8 | 9 | 1. Reporting bugs and issues 10 | 2. Suggesting new features 11 | 3. Writing or improving documentation 12 | 4. Submitting code changes (pull requests) 13 | 14 | ## Reporting Bugs and Issues 15 | 16 | If you find a bug or issue with `Gx`, please report it by [creating an issue here](https://github.com/react-gx/gx/issues) with an issue name like `issue - [name]` (**ie: issue - bug with createSignal function**). When reporting an issue, please provide as much detail as possible, including steps to reproduce the problem. 17 | 18 | ## Suggesting New Features 19 | 20 | If you have an idea for a new feature or enhancement for `Gx`, please [creating an issue here](https://github.com/react-gx/gx/issues) with an issue name like `feature - [name]` (**ie: feature - add feature 1**). When suggesting a new feature, please describe it in detail and explain how it would benefit the project. 21 | 22 | ## Writing or Improving Documentation 23 | 24 | We welcome contributions to `Gx`'s documentation. If you notice an error or omission in the documentation, or if you have an idea for how to improve it, please update the [README](https://github.com/react-gx/gx/blob/main/README.md). When contributing to documentation, please follow the existing style and formatting guidelines. 25 | 26 | # Submitting Code Changes 27 | 28 | If you'd like to submit a code change to `Gx`, please follow these steps: 29 | 30 | 1. Fork the repository into your own GitHub account 31 | 2. Clone the repository to your local machine 32 | 3. Create a new branch for your changes 33 | 4. Make your changes 34 | 5. Commit your changes 35 | 6. Push your changes to your fork 36 | 7. Create a pull request 37 | 38 | Thank you for your interest in contributing to `Gx` ! We appreciate your help in making the project better. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2023] [kombou mbianda armel dilane] 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GX - Global State Management for React Applications 2 | 3 | `React` and `React Native` Library for managing global state. 4 | 5 | [![npm version](https://badge.fury.io/js/%40dilane3%2Fgx.svg)](https://badge.fury.io/js/%40dilane3%2Fgx) 6 | [![npm downloads](https://img.shields.io/npm/dm/%40dilane3%2Fgx)](https://www.npmjs.com/package/@dilane3/gx) 7 | [![GitHub license](https://img.shields.io/github/license/react-gx/gx)](https://github.com/react-gx/gx/blob/main/LICENSE) 8 | 9 | ![logo](https://lh4.googleusercontent.com/k2V9Oh-tfABeDjwovtMUqE-lt6cULH0c1EFgb-XNTFh1lt5DVGTGhl3Ty3fMF3xhCBY=w2400) 10 | 11 | This library aims to provide you an `easy` and `fast` way to set up and manage the global state of your **`react`** application. 12 | 13 | ## Documentation 14 | 15 | You can read the entire [documentation](https://gx.dilane3.com) and see how to use this library perfectly. 16 | 17 | But, If you want to start directly with the library continue reading this small documentation here. 18 | 19 | ## Installation 20 | 21 | You can use `npm` or `yarn` to install this library into your react application. 22 | 23 | ### Using npm 24 | 25 | ```bash 26 | npm install @dilane3/gx 27 | ``` 28 | 29 | ### Using yarn 30 | 31 | ```bash 32 | yarn add @dilane3/gx 33 | ``` 34 | 35 | ## Prerequisites 36 | 37 | ```bash 38 | Since version `1.4.0` of `gx`, you can use it with `strict mode` enabled. 39 | ``` 40 | 41 | But, if you are using a version below `1.4.0`, you have to disable `strict mode` in your react application. 42 | 43 | ### Disabling strict mode on React 44 | 45 | **Before** 46 | 47 | ```jsx 48 | import React, { StrictMode } from "react"; 49 | 50 | function App() { 51 | return ( 52 | 53 | { 54 | // Your application here 55 | } 56 | 57 | ); 58 | } 59 | 60 | export default App; 61 | ``` 62 | 63 | **After** 64 | 65 | ```jsx 66 | import React, { Fragment } from "react"; 67 | 68 | function App() { 69 | return ( 70 | 71 | { 72 | // Your application here 73 | } 74 | 75 | ); 76 | } 77 | 78 | export default App; 79 | ``` 80 | 81 | ### Disabling strict mode on Next.js 82 | 83 | Open the `next.config.js` file and add the following code. 84 | 85 | ```js 86 | module.exports = { 87 | reactStrictMode: false, 88 | }; 89 | ``` 90 | 91 | ## Definition of concepts 92 | 93 | **GX** comes with some new concepts like `signal`, `action`, and `store`. 94 | 95 | ### 1. Signal 96 | 97 | **Signal** represent a specific state that your application has to manage. 98 | For example, for managing users and products inside your ecommerce application you will have to create two separate signals called `usersSignal` and `productsSignal`. 99 | 100 | For handle it, there is a special `createSignal` function for this case. 101 | 102 | ### 2. Action 103 | 104 | **Actions** represent functions that act to the state and make it changing over the time. 105 | 106 | You have to specify these `actions` when you create yours `signals`. 107 | 108 | ```txt 109 | Since version `1.4.0` of `gx`, you can use `async` actions. 110 | ``` 111 | 112 | You can read more about it on the [documentation](https://gx.dilane3.com/docs/guide/async-actions) 113 | 114 | ### 3. Store 115 | 116 | **Store** is a collection of `signals`. We know that in an application, we can manage many state separately, so `gx` gives you the possibility to centralize all your state into a special place. The state becomes easier to manage like that. 117 | 118 | For handle it, there is a special `createStore` function for this case, which takes an array of `signals`. 119 | 120 | ## Usage 121 | 122 | ### First step: Setting up the code structure. 123 | 124 | For structuring your code very well you have to follow these steps. 125 | 126 | - Create a directory called `gx` or whatever you want inside the `src` directory 127 | - Inside the `gx` directory, create two others one called `signals` and `store`. 128 | - Inside the signals directory you will create files that will contains your state declaration with actions that act to this state. (**ie: counter.js**) 129 | - Inside the store directory, just create an index.js file. We will see how to fill it. 130 | 131 | Here is the result. 132 | 133 | ![structure](https://lh3.googleusercontent.com/_z_JTStNFHyXTmjz4GrcphAN6BC_CeKYxN1zwyxWGC-ujpIcVTqthesXT6Lfe8b4t1M=w2400) 134 | 135 | ### Second step: Creating your signals. 136 | 137 | Inside the `signals` directory, create a file called `counter.js` for example. 138 | 139 | ```js 140 | import { createSignal } from "@dilane3/gx"; 141 | 142 | const counterSignal = createSignal({ 143 | name: "counter", 144 | state: 0, 145 | actions: { 146 | increment: (state, payload) => { 147 | return state + payload; 148 | }, 149 | 150 | decrement: (state, payload) => { 151 | return state - payload; 152 | }, 153 | }, 154 | }); 155 | 156 | export default counterSignal; 157 | ``` 158 | 159 | If you want to use `async` actions, you can learn more about it on the [documentation](https://gx.dilane3.com/docs/guide/async-actions) 160 | 161 | ### Third step: Creating your store. 162 | 163 | Inside the `store` directory, create an `index.js` file. 164 | 165 | ```js 166 | import { createStore } from "@dilane3/gx"; 167 | import counterSignal from "../signals/counter"; 168 | 169 | export default createStore([counterSignal]); 170 | ``` 171 | 172 | ### Fourth step: Using your store. 173 | 174 | Inside your `App.js` file, import your store and wrap your application with the `GXProvider` component. 175 | 176 | ```js 177 | import React from "react"; 178 | import store from "./gx/store"; 179 | import GXProvider from "@dilane3/gx"; 180 | 181 | function App() { 182 | return ( 183 | 184 | { 185 | // Your application here 186 | } 187 | 188 | ); 189 | } 190 | 191 | export default App; 192 | ``` 193 | 194 | ### Fifth step: Using your signals. 195 | 196 | Create a component called `Counter` inside the Counter.js file. Then import two hooks from `gx` called `useSignal` and `useActions` like follow. 197 | 198 | ```js 199 | import React from "react"; 200 | import { useSignal, useActions } from "@dilane3/gx"; 201 | 202 | function Counter() { 203 | // State 204 | const counter = useSignal("counter"); 205 | 206 | // Actions 207 | const { increment, decrement } = useActions("counter"); 208 | 209 | return ( 210 |
211 |

Counter App

212 | 213 |

Count: {counter}

214 | 215 | 216 | 217 |
218 | ); 219 | } 220 | 221 | export default Counter; 222 | ``` 223 | 224 | Note that the `useSignal` hook takes the name of the signal as a parameter and return the state contained inside that signal. 225 | 226 | The `useAction` hook takes the name of the signal too and returns an object that contains all the actions of this signal. 227 | 228 | Actually, if you click on the increment button, the counter will increase by one and if you click on the decrement button, the counter will decrease by one. 229 | 230 | ### Sixth step: Adding operations to your signals. 231 | 232 | This feature comes with the version `1.3.0` of `gx`. It allows you to add operations to your signals. 233 | **Operations** are functions that use your current state and apply some filters on it. They return the result of the operation without changing the state. 234 | 235 | For example, if you want to know if the counter is even or odd, you can create an operation called `isEven` like follow. 236 | 237 | ```js 238 | import { createSignal } from "@dilane3/gx"; 239 | 240 | const counterSignal = createSignal({ 241 | name: "counter", 242 | state: 0, 243 | actions: { 244 | increment: (state, payload) => { 245 | return state + payload; 246 | }, 247 | 248 | decrement: (state, payload) => { 249 | return state - payload; 250 | }, 251 | }, 252 | 253 | // Operations section 254 | operations: { 255 | isEven: (state) => { 256 | return state % 2 === 0; 257 | }, 258 | }, 259 | }); 260 | 261 | export default counterSignal; 262 | ``` 263 | 264 | Then, you can use it inside your component like follow. 265 | 266 | ```js 267 | import React from "react"; 268 | import { useSignal, useActions, useOperations } from "@dilane3/gx"; 269 | 270 | function Counter() { 271 | // State 272 | const counter = useSignal("counter"); 273 | 274 | // Actions 275 | const { increment, decrement } = useActions("counter"); 276 | 277 | // Operations 278 | const { isEven } = useOperations("counter"); 279 | 280 | return ( 281 |
282 |

Counter App

283 | 284 |

Count: {counter}

285 | 286 |

is even: {isEven() ? "yes" : "no"}

287 | 288 | 289 | 290 |
291 | ); 292 | } 293 | 294 | export default Counter; 295 | ``` 296 | 297 | ## API 298 | 299 | ### `createSignal` 300 | 301 | This function takes an object as a parameter and returns a signal. 302 | 303 | The object must contain the following properties: 304 | 305 | | Properties | Type | Description | 306 | | ---------- | -------- | ------------------------------------------------------ | 307 | | `name` | `string` | The name of the signal. It must be unique. | 308 | | `state` | `any` | The initial state of the signal. | 309 | | `actions` | `object` | An object that contains all the actions of the signal. | 310 | 311 | Structure of the `actions` object: 312 | 313 | ```js 314 | { 315 | actionName: (state, payload) => { 316 | // Do something with the state and the payload 317 | return state; 318 | }; 319 | } 320 | ``` 321 | 322 | All actions must return the new state of the signal. 323 | 324 | ### `createStore` 325 | 326 | This function takes an array of signals as a parameter and returns a store. 327 | 328 | ```js 329 | const store = createStore([signal1, signal2, signal3]); 330 | ``` 331 | 332 | ### `GXProvider` 333 | 334 | This component takes a store as a parameter and wraps your application with it. 335 | 336 | ```jsx 337 | const App = () => ( 338 | 339 | { 340 | // Your application here 341 | } 342 | 343 | ); 344 | ``` 345 | 346 | ### `useSignal` 347 | 348 | This hook takes the name of the signal as a parameter and returns the state contained inside that signal. 349 | 350 | ```js 351 | const counter = useSignal("counter"); 352 | ``` 353 | 354 | ### `useActions` 355 | 356 | This hook takes the name of the signal as a the first parameter and returns an object that contains all the actions of this signal. 357 | 358 | ```js 359 | const { increment, decrement } = useActions("counter"); 360 | ``` 361 | 362 | ### `useAction` 363 | 364 | This hook takes the name of the signal as the first parameter and the name of the action as the second one and then return that action. 365 | 366 | ```js 367 | const increment = useAction("counter", "increment"); 368 | ``` 369 | 370 | See more on the [documentation](https://gx.dilane3.com/docs/guide/hooks/useAction) 371 | 372 | ## TypeScript Support 373 | 374 | `GX` support TypeScript, so that you can use it directly into your application. 375 | 376 | See how to integrate it on the [documentation](https://gx.dilane3.com/docs/typescript) website 377 | 378 | ## License 379 | 380 | [MIT](https://choosealicense.com/licenses/mit/) 381 | 382 | ## Author 383 | 384 | - Github: [**@dilane3**](https://github.com/dilane3) 385 | - Twitter: [**@dilanekombou**](https://twitter.com/DilaneKombou) 386 | - LinkedIn: [**@dilanekombou**](https://www.linkedin.com/in/dilane-kombou-6824b2207/) 387 | - website: [**dilane3.com**](https://dilane3.com/) 388 | 389 | ## Contributing 390 | 391 | Contributions, issues and feature requests are welcome! 392 | See the [Contributing Guide](./CONTRIBUTING.md). 393 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dilane3/gx", 3 | "version": "1.4.1", 4 | "private": false, 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "type": "module", 8 | "author": { 9 | "name": "dilane3", 10 | "email": "komboudilane125@gmail.com", 11 | "url": "https://dilane3.com", 12 | "twitter": "https://twitter.com/dilanekombou", 13 | "github": "https://github.com/dilane3" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/react-gx/gx", 18 | "issues": "https://github.com/react-gx/gx/issues" 19 | }, 20 | "devDependencies": { 21 | "@testing-library/jest-dom": "^5.14.1", 22 | "@testing-library/react": "^13.0.0", 23 | "@testing-library/user-event": "^13.2.1", 24 | "@types/jest": "^27.0.1", 25 | "@types/node": "^20.8.6", 26 | "@types/react": "^18.2.28", 27 | "@typescript-eslint/eslint-plugin": "^6.4.0", 28 | "eslint": "^8.0.1", 29 | "eslint-config-standard-with-typescript": "^39.0.0", 30 | "eslint-plugin-immutable": "^1.0.0", 31 | "eslint-plugin-import": "^2.25.2", 32 | "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", 33 | "eslint-plugin-promise": "^6.0.0", 34 | "eslint-plugin-react": "^7.33.2", 35 | "react-scripts": "^5.0.1", 36 | "typescript": "^5.2.2" 37 | }, 38 | "peerDependencies": { 39 | "react": "^18.2.0", 40 | "react-dom": "^18.2.0" 41 | }, 42 | "scripts": { 43 | "build": "tsc", 44 | "test": "react-scripts test", 45 | "deploy": "npm publish --access public", 46 | "lint": "npx eslint src --ext .ts,.tsx", 47 | "pack": "npm pack --pack-destination ./../packages/gx/" 48 | }, 49 | "keywords": [ 50 | "react", 51 | "react-gx", 52 | "gx", 53 | "redux", 54 | "zustand", 55 | "mobx", 56 | "management", 57 | "state", 58 | "state-management", 59 | "react-state-management", 60 | "global", 61 | "global-state" 62 | ], 63 | "eslintConfig": { 64 | "extends": [ 65 | "react-app", 66 | "react-app/jest" 67 | ] 68 | }, 69 | "browserslist": { 70 | "production": [ 71 | ">0.2%", 72 | "not dead", 73 | "not op_mini all" 74 | ], 75 | "development": [ 76 | "last 1 chrome version", 77 | "last 1 firefox version", 78 | "last 1 safari version" 79 | ] 80 | }, 81 | "engines": { 82 | "node": ">=16.0.0", 83 | "npm": ">=7.0.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | import { type GXContextType } from './types.js' 3 | 4 | const GXContext = createContext({ 5 | signals: [], 6 | dispatch: () => {}, 7 | asyncDispatch: () => {} 8 | }) 9 | 10 | export default GXContext 11 | -------------------------------------------------------------------------------- /src/contexts/types.ts: -------------------------------------------------------------------------------- 1 | import type IBuilderCase from '../interfaces/builderCase.js' 2 | import { type GXAction } from '../providers/types.js' 3 | 4 | /** 5 | * Type that represents a signal 6 | */ 7 | export interface GXSignalType { 8 | // Name of the signal 9 | name: string 10 | 11 | // State inside the signal 12 | state: T 13 | 14 | // Actions of the signal 15 | actions?: Array> 16 | 17 | // Operations of the signal 18 | operations?: Array> 19 | 20 | // Async actions of the signal 21 | asyncActions?: Array> 22 | } 23 | 24 | /** 25 | * Type that represents Actions 26 | */ 27 | export interface GXActionType { 28 | // Represent the type of the action 29 | type: string 30 | 31 | // The handler function 32 | handler: (state: T, payload: P) => T 33 | } 34 | 35 | /** 36 | * Type that represents operations 37 | */ 38 | export interface GXOperationType { 39 | // Represent the type of the operation 40 | type: string 41 | 42 | // The handle function 43 | handler: (state: T, payload: P) => Q 44 | } 45 | 46 | /** 47 | * Type that represents async actions 48 | */ 49 | export interface GXAsyncActionType { 50 | // Represent the type of the operation 51 | type: string 52 | 53 | // List of cases 54 | steps: IBuilderCase 55 | } 56 | 57 | /** 58 | * Type of dispatched action 59 | */ 60 | export interface DispatchedActionType { 61 | // The type of the action 62 | type: string 63 | 64 | // The payload of the action 65 | payload: any 66 | } 67 | 68 | /** 69 | * Type of the signals context 70 | */ 71 | export interface GXContextType { 72 | // Signals 73 | signals: GXSignalType[] 74 | 75 | // Dispatch 76 | dispatch: React.Dispatch 77 | 78 | // Async Dispatch 79 | asyncDispatch: (action: GXAction) => any 80 | } 81 | -------------------------------------------------------------------------------- /src/helpers/createAsyncAction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AsyncActionStatuses, 3 | type CreateAsyncActionProp, 4 | type CreateAsyncActionReturnType 5 | } from './types.js' 6 | 7 | /** 8 | * This function create an async action with different statuses 9 | * @param handler Function that perform asynchronous task 10 | * @returns 11 | */ 12 | const createAsyncAction = ( 13 | handler: CreateAsyncActionProp 14 | ): CreateAsyncActionReturnType => { 15 | return { 16 | pending: AsyncActionStatuses.PENDING, 17 | fulfilled: AsyncActionStatuses.FULFILLED, 18 | rejected: AsyncActionStatuses.REJECTED, 19 | handler 20 | } 21 | } 22 | 23 | export default createAsyncAction 24 | -------------------------------------------------------------------------------- /src/helpers/createSignal.ts: -------------------------------------------------------------------------------- 1 | import { type CreateSignalOptionType } from './types.js' 2 | import { 3 | type GXActionType, 4 | type GXAsyncActionType, 5 | type GXOperationType 6 | } from '../contexts/types.js' 7 | import { Builder } from '../interfaces/builder.js' 8 | 9 | /** 10 | * Create a signal with a state and actions for managing this state 11 | * @param options 12 | * @returns 13 | */ 14 | const createSignal = (options: CreateSignalOptionType) => { 15 | const actions: Array> = [] 16 | const operations: Array> = [] 17 | const asyncActions: Array> = [] 18 | 19 | // Convert the actions object to an array 20 | const actionsTable = Object.entries(options.actions || {}) 21 | 22 | for (const action of actionsTable) { 23 | actions.push({ 24 | type: `${options.name}/${action[0]}`, 25 | handler: action[1] 26 | }) 27 | } 28 | 29 | // Convert the operations object to an array 30 | const operationsTable = Object.entries(options.operations || {}) 31 | 32 | for (const operation of operationsTable) { 33 | operations.push({ 34 | type: `${options.name}/${operation[0]}`, 35 | handler: operation[1] 36 | }) 37 | } 38 | 39 | // Convert the async Actions object to an array 40 | const builder = new Builder() 41 | 42 | const asyncActionsTable = Object.entries( 43 | options.asyncActions ? options.asyncActions(builder) : {} 44 | ) 45 | 46 | for (const action of asyncActionsTable) { 47 | asyncActions.push({ 48 | type: `${options.name}/${action[0]}`, 49 | steps: action[1] 50 | }) 51 | } 52 | 53 | // Create a signal 54 | const signal = { 55 | name: options.name, 56 | state: options.state, 57 | actions, 58 | operations, 59 | asyncActions 60 | } 61 | 62 | return signal 63 | } 64 | 65 | export default createSignal 66 | -------------------------------------------------------------------------------- /src/helpers/createStore.ts: -------------------------------------------------------------------------------- 1 | import { type GXSignalType } from '../contexts/types.js' 2 | import { type CreateStoreType } from './types.js' 3 | 4 | /** 5 | * Function that create a store by collection a list of signals 6 | * @param signals List of signals 7 | * @returns 8 | */ 9 | const createStore = (signals: GXSignalType[]): CreateStoreType => { 10 | return { 11 | getSignals: () => signals 12 | } 13 | } 14 | 15 | export default createStore 16 | -------------------------------------------------------------------------------- /src/helpers/types.ts: -------------------------------------------------------------------------------- 1 | import { type GXSignalType } from "../contexts/types.js"; 2 | import { type Builder } from "../interfaces/builder.js"; 3 | import type IBuilderCase from "../interfaces/builderCase.js"; 4 | 5 | /** 6 | * Type of the create signal option function 7 | */ 8 | export interface CreateSignalOptionType { 9 | // Name of the signal 10 | name: string; 11 | 12 | // State of the signal 13 | state: T; 14 | 15 | // Actions of the signal 16 | actions?: Action; 17 | 18 | // Operations of the signal 19 | operations?: Operation; 20 | 21 | // Async actions of the signal 22 | asyncActions?: AsyncAction; 23 | } 24 | 25 | /** 26 | * Type of the returned data of create store function 27 | */ 28 | export interface CreateStoreType { 29 | // Function that return the list of signals 30 | getSignals: () => GXSignalType[]; 31 | } 32 | 33 | /** 34 | * Type of Action 35 | */ 36 | export type Action = Record T>; 37 | 38 | /** 39 | * Type of Operation 40 | */ 41 | export type Operation = Record any>; 42 | 43 | /** 44 | * Type of Async Action 45 | */ 46 | export type AsyncAction = ( 47 | builder: Builder 48 | ) => Record>; 49 | 50 | export type CreateAsyncActionProp = (payload?: any) => Promise; 51 | 52 | export interface CreateAsyncActionReturnType { 53 | pending: AsyncActionStatusesType; 54 | fulfilled: AsyncActionStatusesType; 55 | rejected: AsyncActionStatusesType; 56 | handler: CreateAsyncActionProp; 57 | } 58 | 59 | export type AsyncActionResponse

= Promise<{ 60 | state: P; 61 | data: T; 62 | error: Error | null; 63 | status: AsyncActionStatusesType; 64 | }>; 65 | 66 | export const AsyncActionStatuses = { 67 | PENDING: "PENDING", 68 | FULFILLED: "FULFILLED", 69 | REJECTED: "REJECTED", 70 | } as const; 71 | 72 | export type AsyncActionStatusesType = 73 | (typeof AsyncActionStatuses)[keyof typeof AsyncActionStatuses]; 74 | -------------------------------------------------------------------------------- /src/hooks/types.ts: -------------------------------------------------------------------------------- 1 | import { type AsyncActionStatusesType } from '../helpers/types.js' 2 | 3 | export type Actions = Record void> 4 | 5 | export type AsyncActions = Record Promise<{ 6 | state: T, 7 | data: any | null, 8 | error: Error | null, 9 | status: AsyncActionStatusesType 10 | }>> 11 | 12 | export type Operations

= Record P> 13 | -------------------------------------------------------------------------------- /src/hooks/useAction.ts: -------------------------------------------------------------------------------- 1 | import { type Actions } from './types.js' 2 | import useActions from './useActions.js' 3 | 4 | const useAction = (signalName: string, action: string) => { 5 | if (!signalName || typeof signalName !== 'string') { throw new Error('Provide a signalName as a first argument of useAction') } 6 | 7 | if (!action || typeof action !== 'string') { throw new Error('Provide an action as second argument of useAction') } 8 | 9 | const actions = useActions(signalName, action) 10 | 11 | return Object.values(actions)[0] 12 | } 13 | 14 | export default useAction 15 | -------------------------------------------------------------------------------- /src/hooks/useActions.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import GXContext from '../contexts/index.js' 3 | import { type GXActionType } from '../contexts/types.js' 4 | import { type Actions } from './types.js' 5 | 6 | const useActions = ( 7 | signalName: string, 8 | ...actions: string[] 9 | ): T => { 10 | if (!signalName || typeof signalName !== 'string') { 11 | throw new Error('Provide a signalName as first argument of useActions') 12 | } 13 | 14 | // Get Global Context 15 | const { signals, dispatch } = useContext(GXContext) 16 | 17 | // Some handlers 18 | 19 | /** 20 | * Get actions of a signal 21 | * @param signalName 22 | * @returns 23 | */ 24 | const handleGetActions = (signalName: string) => { 25 | const signal = signals.find((signal) => signal.name === signalName) 26 | 27 | if (signal) { 28 | if (!actions || actions.length === 0) return signal.actions 29 | 30 | const filteredActions: Array> = [] 31 | 32 | for (const action of actions) { 33 | const actionName = `${signalName}/${action}` 34 | 35 | const retrievedAction = signal.actions.find( 36 | (act) => act.type === actionName 37 | ) 38 | 39 | if (retrievedAction) filteredActions.push(retrievedAction) 40 | else throw new Error(`Action ${actionName} not found`) 41 | } 42 | 43 | return filteredActions 44 | } else throw new Error(`Signal ${signalName} not found`) 45 | } 46 | 47 | const handleFormatActions = (): T => { 48 | // Get actions 49 | const nonFormattedActions = handleGetActions(signalName) 50 | 51 | // Formatted actions 52 | const formattedActions = {} as any 53 | 54 | for (const action of nonFormattedActions) { 55 | // Get action name 56 | const actionName = action.type.split('/')[1] 57 | 58 | formattedActions[actionName] = (payload?: any) => { 59 | dispatch({ 60 | type: action.type, 61 | isAsync: false, 62 | payload 63 | }) 64 | } 65 | } 66 | 67 | return formattedActions 68 | } 69 | 70 | return handleFormatActions() 71 | } 72 | 73 | export default useActions 74 | -------------------------------------------------------------------------------- /src/hooks/useAllSignals.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import GXContext from "../contexts/index.js" 3 | 4 | export default function useAllSignals() { 5 | // Global state that manage all signals 6 | const { signals } = useContext(GXContext) 7 | 8 | return signals 9 | } -------------------------------------------------------------------------------- /src/hooks/useAsyncActions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useCallback, 3 | useContext, 4 | useEffect, 5 | useMemo, 6 | useRef, 7 | useState, 8 | } from "react"; 9 | import { type AsyncActions } from "./types.js"; 10 | import GXContext from "../contexts/index.js"; 11 | import { type GXAsyncActionType } from "../contexts/types.js"; 12 | import { AsyncActionStatuses } from "../helpers/types.js"; 13 | import { BuilderCase } from "../interfaces/builderCase.js"; 14 | 15 | const useAsyncActions =

>( 16 | signalName: string, 17 | ...actions: string[] 18 | ) => { 19 | if (!signalName || typeof signalName !== "string") { 20 | throw new Error( 21 | "Provide a signalName as first argument of useAsyncActions" 22 | ); 23 | } 24 | 25 | // Get Global Context 26 | const { signals, asyncDispatch } = useContext(GXContext); 27 | 28 | // Get state from signals 29 | // Extract type from P generic type 30 | type StateType = P extends AsyncActions ? U : any; 31 | 32 | const state = useMemo(() => { 33 | const signal = signals.find((signal) => signal.name === signalName); 34 | 35 | if (signal) return signal.state; 36 | else throw new Error(`Signal ${signalName} not found`); 37 | }, [signals]); 38 | 39 | // Refs 40 | // Define a ref to block the execution of async action callback twice 41 | const isAsyncActionCallbackRunning = useRef<{ [key: string]: Boolean }>({}); 42 | 43 | // Async action callback 44 | const asyncActionCallback = useRef( 45 | async (action: GXAsyncActionType, payload?: any) => { 46 | // Prevent the execution of async action callback twice 47 | if (isAsyncActionCallbackRunning.current[action.type]) 48 | return new Promise((resolve) => { 49 | resolve({ 50 | status: AsyncActionStatuses.PENDING, 51 | state, 52 | error: null, 53 | data: null, 54 | }); 55 | }); 56 | 57 | // Set the ref to true 58 | isAsyncActionCallbackRunning.current[action.type] = true; 59 | 60 | // Dispatch pending action 61 | asyncDispatch({ 62 | type: action.type, 63 | isAsync: true, 64 | status: AsyncActionStatuses.PENDING, 65 | }); 66 | 67 | try { 68 | // Execute async action 69 | const response = await ( 70 | action.steps as BuilderCase 71 | ).asyncAction.handler(payload); 72 | 73 | // Dispatch fulfilled action 74 | const data = asyncDispatch({ 75 | type: action.type, 76 | isAsync: true, 77 | status: AsyncActionStatuses.FULFILLED, 78 | payload: response, 79 | }); 80 | 81 | return { 82 | state: data, 83 | data: response, 84 | error: null, 85 | status: AsyncActionStatuses.FULFILLED, 86 | }; 87 | } catch (error) { 88 | // Dispatch rejected action 89 | const data = asyncDispatch({ 90 | type: action.type, 91 | isAsync: true, 92 | status: AsyncActionStatuses.REJECTED, 93 | payload: error, 94 | }); 95 | 96 | return { 97 | state: data, 98 | data: null, 99 | error: new Error(error), 100 | status: AsyncActionStatuses.REJECTED, 101 | }; 102 | } finally { 103 | // Set the ref to false 104 | isAsyncActionCallbackRunning.current[action.type] = false; 105 | } 106 | } 107 | ); 108 | 109 | // Some handlers 110 | 111 | /** 112 | * Get async actions of a signal 113 | * @param signalName 114 | * @returns 115 | */ 116 | const handleGetAsyncActions = (signalName: string) => { 117 | const signal = signals.find((signal) => signal.name === signalName); 118 | 119 | if (signal) { 120 | if (!actions || actions.length === 0) return signal.asyncActions || []; 121 | 122 | const filteredActions: Array> = []; 123 | 124 | for (const action of actions) { 125 | const actionName = `${signalName}/${action}`; 126 | 127 | const retrievedAction = signal.asyncActions.find( 128 | (act) => act.type === actionName 129 | ); 130 | 131 | if (retrievedAction) filteredActions.push(retrievedAction); 132 | else throw new Error(`Async Action ${actionName} not found`); 133 | } 134 | 135 | return filteredActions; 136 | } else throw new Error(`Signal ${signalName} not found`); 137 | }; 138 | 139 | /** 140 | * Format async actions 141 | * @returns 142 | */ 143 | const handleFormatAsyncActions = (): P => { 144 | // Get actions 145 | const nonFormattedActions = handleGetAsyncActions(signalName); 146 | 147 | // Formatted actions 148 | const formattedActions = nonFormattedActions.map((action) => { 149 | // Get action name 150 | const actionName = action.type.split("/")[1]; 151 | 152 | return [ 153 | actionName, 154 | async (payload?: any) => { 155 | return asyncActionCallback.current(action, payload); 156 | }, 157 | ]; 158 | }); 159 | 160 | return Object.fromEntries(formattedActions); 161 | }; 162 | 163 | return handleFormatAsyncActions(); 164 | }; 165 | 166 | export default useAsyncActions; 167 | -------------------------------------------------------------------------------- /src/hooks/useOperations.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import GXContext from '../contexts/index.js' 3 | import { type Operations } from './types.js' 4 | 5 | const useOperations = (signalName: string) => { 6 | // Get Global Context 7 | const { signals } = useContext(GXContext) 8 | 9 | if (!signalName || typeof signalName !== 'string') { 10 | throw new Error( 11 | 'Provide a signalName as a first argument of useOperations' 12 | ) 13 | } 14 | 15 | const handleFormatOperations = (): T => { 16 | const signal = signals.find((signal) => signal.name === signalName) 17 | 18 | if (!signal) throw new Error(`Signal ${signalName} not found`) 19 | 20 | // Get actions 21 | const nonFormattedOperations = signal.operations 22 | 23 | // Formatted actions 24 | const formattedOperations = {} as any 25 | 26 | for (const operation of nonFormattedOperations) { 27 | // Get action name 28 | const operationName = operation.type.split('/')[1] 29 | 30 | formattedOperations[operationName] = (payload?: any) => { 31 | return operation.handler(signal.state, payload) 32 | } 33 | } 34 | 35 | // return formattedOperations; 36 | 37 | return formattedOperations 38 | } 39 | 40 | return handleFormatOperations() 41 | } 42 | 43 | // Définir un type générique pour représenter une fonction 44 | // type FunctionType any> = T; 45 | 46 | // Définir un type générique pour représenter un objet contenant des fonctions 47 | // type FunctionObject = { 48 | // [K in keyof T]: T[K] extends Function ? FunctionType : never; 49 | // }; 50 | 51 | // type FunctionObject = { 52 | // [K in keyof T]: T[K] extends (payload: infer Arg) => infer R 53 | // ? (payload: Arg) => R 54 | // : (payload?: any) => any; 55 | // }; 56 | 57 | // // Utilisation d'une fonction auxiliaire pour extraire le type du second paramètre 58 | // type SecondParamType = T extends (a: any, b: infer P) => any ? P : any; 59 | 60 | // // Utilisation d'une fonction auxiliaire pour extraire le type de retour d'une fonction 61 | // type ReturnTypeFunc = T extends (...args: any[]) => infer R ? R : any; 62 | 63 | export default useOperations 64 | -------------------------------------------------------------------------------- /src/hooks/useSignal.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react' 2 | import GXContext from '../contexts/index.js' 3 | 4 | const useSignal = (signalName: string) => { 5 | const { signals } = useContext(GXContext) 6 | const memoizedSignals = useMemo(() => signals, [signals]) 7 | 8 | /** 9 | * Get state of a signal base on its name 10 | * @param signalName 11 | * @returns 12 | */ 13 | const handleGetSignalState = (signalName: string): T => { 14 | const signal = memoizedSignals.find(signal => signal.name === signalName) 15 | 16 | if (signal) { 17 | return signal.state 18 | } 19 | 20 | // Throw error if signal not found 21 | throw new Error(`Signal ${signalName} not found`) 22 | } 23 | 24 | return handleGetSignalState(signalName) 25 | } 26 | 27 | export default useSignal 28 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Provider 2 | import GXProvider from "./providers/index.js"; 3 | 4 | // Constants 5 | import { AsyncActionStatuses } from "./helpers/types.js"; 6 | 7 | // Types 8 | import { type AsyncActionResponse } from "./helpers/types.js"; 9 | 10 | // Helpers functions 11 | import createSignal from "./helpers/createSignal.js"; 12 | import createStore from "./helpers/createStore.js"; 13 | import createAsyncAction from "./helpers/createAsyncAction.js"; 14 | 15 | // Hooks 16 | import useAction from "./hooks/useAction.js"; 17 | import useActions from "./hooks/useActions.js"; 18 | import useAsyncActions from "./hooks/useAsyncActions.js"; 19 | import useAllSignals from "./hooks/useAllSignals.js"; 20 | import useSignal from "./hooks/useSignal.js"; 21 | import useOperations from "./hooks/useOperations.js"; 22 | 23 | export default GXProvider; 24 | 25 | export { 26 | createSignal, 27 | createStore, 28 | createAsyncAction, 29 | useAction, 30 | useActions, 31 | useAsyncActions, 32 | useAllSignals, 33 | useSignal, 34 | useOperations, 35 | AsyncActionStatuses, 36 | AsyncActionResponse 37 | }; 38 | 39 | // "build": "tsc && npx babel dist --out-dir cjs --extensions '.js' --source-maps inline --copy-files", 40 | -------------------------------------------------------------------------------- /src/interfaces/builder.ts: -------------------------------------------------------------------------------- 1 | import { type CreateAsyncActionReturnType } from "../helpers/types.js"; 2 | import type IBuilderCase from "./builderCase.js"; 3 | import { BuilderCase } from "./builderCase.js"; 4 | 5 | export default interface IBuilder { 6 | use: (asyncAction: CreateAsyncActionReturnType) => IBuilderCase; 7 | } 8 | 9 | /** 10 | * @class Builder 11 | * @implements IBuilder 12 | * @description 13 | * Builder class to initialize a new builder case instance and return it in order to 14 | * chain the builder case methods, or onPending, onFulfilled, onRejected methods to define 15 | * the async action steps. 16 | */ 17 | export class Builder implements IBuilder { 18 | /** 19 | * This method takes an async action object and assign it to the builder case instance 20 | * @param asyncAction An async action object 21 | * @returns IBuilderCase 22 | */ 23 | use(asyncAction: CreateAsyncActionReturnType): IBuilderCase { 24 | const builderCase = new BuilderCase(); 25 | 26 | builderCase.asyncAction = asyncAction; 27 | builderCase.cases = []; 28 | 29 | return builderCase; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/interfaces/builderCase.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AsyncActionStatuses, 3 | type AsyncActionStatusesType, 4 | type CreateAsyncActionReturnType 5 | } from '../helpers/types.js' 6 | 7 | /** 8 | * Interface for builder case 9 | */ 10 | export default interface IBuilderCase { 11 | case: ( 12 | status: AsyncActionStatusesType, 13 | handler: (state: T, payload?: P) => T 14 | ) => IBuilderCase 15 | 16 | onPending: (handler: (state: T, payload?: P) => T) => IBuilderCase 17 | 18 | onFulfilled: (handler: (state: T, payload?: P) => T) => IBuilderCase 19 | 20 | onRejected: (handler: (state: T, payload?: P) => T) => IBuilderCase 21 | } 22 | 23 | /** 24 | * Builder case class for managing different cases of the asynchronous task 25 | * @param _cases List of cases defined for a specific asynchronous task 26 | */ 27 | export class BuilderCase implements IBuilderCase { 28 | private _cases: Array> 29 | private _asyncAction: CreateAsyncActionReturnType | undefined 30 | 31 | constructor () { 32 | this._cases = [] 33 | this._asyncAction = undefined 34 | } 35 | 36 | // Getters 37 | 38 | /** 39 | * Get the list of cases 40 | */ 41 | get cases () { 42 | return this._cases 43 | } 44 | 45 | /** 46 | * Get the async action 47 | */ 48 | get asyncAction () { 49 | return this._asyncAction 50 | } 51 | 52 | // Setters 53 | 54 | /** 55 | * Update the async action 56 | * @param asyncAction Async Action value 57 | */ 58 | set asyncAction (asyncAction: CreateAsyncActionReturnType) { 59 | this._asyncAction = asyncAction 60 | } 61 | 62 | /** 63 | * Update the cases 64 | */ 65 | set cases (cases: Array>) { 66 | this._cases = cases 67 | } 68 | 69 | /** 70 | * Method that add a new case into the _cases list and return a new case builder object 71 | * @param status Status of the asynchronous task 72 | * @param handler Function that is executed depending on the specific status 73 | * @returns 74 | */ 75 | case ( 76 | status: AsyncActionStatusesType, 77 | handler: (state: T, payload?: P) => T 78 | ): IBuilderCase { 79 | this._cases.push({ 80 | status, 81 | handler 82 | }) 83 | 84 | return this 85 | } 86 | 87 | /** 88 | * Method that add a pending case into the _cases list and return a new case builder object 89 | * @param handler Function that is executed depending on the specific status 90 | * @returns 91 | */ 92 | onPending (handler: (state: T, payload?: P) => T): IBuilderCase { 93 | return this.case(AsyncActionStatuses.PENDING, handler) 94 | } 95 | 96 | /** 97 | * Method that add a fulfilled case into the _cases list and return a new case builder object 98 | * @param handler Function that is executed depending on the specific status 99 | * @returns 100 | */ 101 | onFulfilled (handler: (state: T, payload?: P) => T): IBuilderCase { 102 | return this.case(AsyncActionStatuses.FULFILLED, handler) 103 | } 104 | 105 | /** 106 | * Method that add a rejected case into the _cases list and return a new case builder object 107 | * @param handler Function that is executed depending on the specific status 108 | * @returns 109 | **/ 110 | onRejected (handler: (state: T, payload?: P) => T): IBuilderCase { 111 | return this.case(AsyncActionStatuses.REJECTED, handler) 112 | } 113 | } 114 | 115 | /** 116 | * Case interface 117 | */ 118 | export interface Case { 119 | status: AsyncActionStatusesType 120 | handler: (state: T, payload?: P) => T 121 | } 122 | -------------------------------------------------------------------------------- /src/providers/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useReducer, useTransition } from "react"; 2 | import GXContext from "../contexts/index.js"; 3 | import { type GXAction, type GXProviderProps } from "./types.js"; 4 | import gxReducer from "./reducer.js"; 5 | import { type BuilderCase } from "../interfaces/builderCase.js"; 6 | 7 | export default function GXProvider({ children, store }: GXProviderProps) { 8 | // Global state that manage all signals 9 | const [signals, dispatch] = useReducer(gxReducer, store.getSignals()); 10 | 11 | // Wrap your dispatch function with useTransition 12 | const [, startTransition] = useTransition(); 13 | 14 | // Your state management logic using useContext and useReducer 15 | const syncDispatch = (action: GXAction) => { 16 | startTransition(() => { 17 | dispatch(action); 18 | }); 19 | }; 20 | 21 | const asyncDispatch = useCallback((action: GXAction) => { 22 | const signalName = action.type.split("/")[0]; 23 | 24 | const newState = signals.map( 25 | ({ name, operations, actions, asyncActions, state: prevState }) => { 26 | let state = prevState; 27 | 28 | // Capture the target signal (a state and a bunch of async actions) from the array of signals. 29 | // Capture the action from array of async actions (of the target signal). 30 | // Run the async action and update the signal state. 31 | if (name === signalName) { 32 | if (action.isAsync) { 33 | for (const { type, steps } of asyncActions) { 34 | if (type === action.type) { 35 | state = (steps as BuilderCase).cases 36 | .find((c) => c.status === action.status) 37 | .handler(state, action.payload); 38 | break; 39 | } 40 | } 41 | } 42 | } 43 | 44 | return { 45 | name, 46 | operations, 47 | state, 48 | actions, 49 | asyncActions, 50 | }; 51 | } 52 | ); 53 | 54 | // Find the new state of the target signal 55 | const signal = newState.find((signal) => signal.name === signalName); 56 | 57 | dispatch({ 58 | type: action.type, 59 | isAsync: action.isAsync, 60 | status: action.status, 61 | payload: signal.state, 62 | }); 63 | 64 | return signal.state 65 | }, []); 66 | 67 | // Context value 68 | const contextValue = { 69 | signals, 70 | dispatch: syncDispatch, 71 | asyncDispatch 72 | }; 73 | 74 | return ( 75 | {children} 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/providers/reducer.ts: -------------------------------------------------------------------------------- 1 | import { type GXSignalType } from '../contexts/types.js' 2 | import { type GXAction } from './types.js' 3 | 4 | const gxReducer = ( 5 | signals: GXSignalType[], 6 | action: GXAction 7 | ): GXSignalType[] => { 8 | const signalName = action.type.split('/')[0] 9 | 10 | // Loop through all signals, make updates on targeted states 11 | // and returns a new array of signals (immutability). 12 | return signals.map( 13 | ({ name, operations, actions, asyncActions, state: prevState }) => { 14 | let state = prevState 15 | 16 | // Capture the target signal (a state and a bunch of actions) from the array of signals. 17 | // Capture the action from array of actions (of the target signal). 18 | // Run the action and update the signal state. 19 | if (name === signalName) { 20 | if (!action.isAsync) { 21 | for (const { type, handler } of actions) { 22 | if (type === action.type) { 23 | state = handler(prevState, action.payload) 24 | break 25 | } 26 | } 27 | } else { 28 | state = action.payload 29 | } 30 | } 31 | 32 | return { 33 | name, 34 | operations, 35 | state, 36 | actions, 37 | asyncActions 38 | } 39 | } 40 | ) 41 | } 42 | 43 | export default gxReducer 44 | -------------------------------------------------------------------------------- /src/providers/types.ts: -------------------------------------------------------------------------------- 1 | import { type AsyncActionStatusesType, type CreateStoreType } from '../helpers/types.js' 2 | 3 | /** 4 | * Props of the GX Provider 5 | */ 6 | export interface GXProviderProps { 7 | // Children component of the GX Provider 8 | children: React.ReactNode 9 | 10 | // Collection of signals 11 | store: CreateStoreType 12 | } 13 | 14 | /** 15 | * Type of the actions 16 | */ 17 | export interface GXAction { 18 | // Type of the action 19 | type: string 20 | 21 | // Nature of the action 22 | isAsync: boolean 23 | 24 | status?: AsyncActionStatusesType 25 | 26 | // Payload of the action 27 | payload?: any 28 | } 29 | -------------------------------------------------------------------------------- /src/tests/gx/signals/counter.ts: -------------------------------------------------------------------------------- 1 | import createSignal from "../../../helpers/createSignal.js"; 2 | 3 | const counterSignal = createSignal({ 4 | name: "counter", 5 | state: 0, 6 | actions: { 7 | increment: (state, payload) => { 8 | return state + payload; 9 | }, 10 | 11 | decrement: (state, payload) => { 12 | return state + payload; 13 | }, 14 | }, 15 | }); 16 | 17 | export default counterSignal; 18 | -------------------------------------------------------------------------------- /src/tests/gx/store/index.ts: -------------------------------------------------------------------------------- 1 | import createStore from "../../../helpers/createStore.js"; 2 | import counterSignal from "../signals/counter.js"; 3 | 4 | // Create a store 5 | const store = createStore([counterSignal]); 6 | 7 | export default store; 8 | -------------------------------------------------------------------------------- /src/tests/signals.test.ts: -------------------------------------------------------------------------------- 1 | import counterSignal from "./gx/signals/counter.js"; 2 | 3 | test("should create a signal", () => { 4 | // Expectations 5 | expect(counterSignal).not.toBeNull(); 6 | expect(counterSignal.name).toEqual("counter"); 7 | expect(counterSignal.state).toEqual(0); 8 | expect(counterSignal.actions.length).toEqual(2); 9 | expect(counterSignal.actions[0].handler(counterSignal.state, 3)).toEqual(3); 10 | }); 11 | -------------------------------------------------------------------------------- /src/tests/store.test.ts: -------------------------------------------------------------------------------- 1 | import counterSignal from './gx/signals/counter.js'; 2 | import store from './gx/store/index.js'; 3 | 4 | test("should create a store containing signals", () => { 5 | // Expectations 6 | expect(store.getSignals()).not.toBeNull(); 7 | expect(store.getSignals()).toEqual([counterSignal]); 8 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "rootDir": "src", 5 | "target": "ESNext", 6 | "lib": [ 7 | "ES2015", 8 | "ESNext", 9 | "dom", 10 | "dom.iterable", 11 | "esnext" 12 | ], 13 | "allowJs": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "allowSyntheticDefaultImports": true, 17 | "strict": false, 18 | "forceConsistentCasingInFileNames": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "module": "ESNext", 21 | "moduleResolution": "node", 22 | "resolveJsonModule": true, 23 | "isolatedModules": true, 24 | "noEmit": false, 25 | "jsx": "react-jsx", 26 | "declaration": true, 27 | "sourceMap": false, 28 | }, 29 | "include": [ 30 | "src" 31 | ], 32 | "exclude": [ 33 | "node_modules", 34 | "dist", 35 | "src/tests" 36 | ] 37 | } 38 | --------------------------------------------------------------------------------