{"id":21915536,"url":"https://github.com/terotests/ts2redux","last_synced_at":"2025-04-18T21:50:17.211Z","repository":{"id":33138312,"uuid":"152080675","full_name":"terotests/ts2redux","owner":"terotests","description":"Compile standard TypeScript classes to Redux or React Context API","archived":false,"fork":false,"pushed_at":"2022-12-09T05:12:59.000Z","size":1897,"stargazers_count":39,"open_issues_count":13,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-03-25T14:22:04.321Z","etag":null,"topics":["react","react-redux","reactcontextapi","redux","typescript"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/terotests.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-10-08T13:01:13.000Z","updated_at":"2020-11-16T21:02:06.000Z","dependencies_parsed_at":"2023-01-14T23:34:29.852Z","dependency_job_id":null,"html_url":"https://github.com/terotests/ts2redux","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/terotests%2Fts2redux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/terotests%2Fts2redux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/terotests%2Fts2redux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/terotests%2Fts2redux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/terotests","download_url":"https://codeload.github.com/terotests/ts2redux/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249553420,"owners_count":21290221,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["react","react-redux","reactcontextapi","redux","typescript"],"created_at":"2024-11-28T19:12:40.856Z","updated_at":"2025-04-18T21:50:17.188Z","avatar_url":"https://github.com/terotests.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TypeScript to Redux\n\nCompile simple TypeScript classes into both Redux or React Context API state machines, which work together with Redux Devtools (yes, also React Context changes can be viewed from Redux Devtools, time travel also works! 🎉)\n\nToo good to be true?\n\nYes, it is true, but the compiler is still quite young and Please Do check the [Limitations](https://github.com/terotests/ts2redux#limitations) before you test the library.\n\n## Installation\n\n```\nnpm i -g ts2redux\n```\n\nRun test app by cloning [Repository](https://github.com/terotests/ts2redux) and then\n\n```\nnpm install\nnpm test\n```\n\n## Why?\n\nIt is not likely that state management gets much easier than this:\n\n1. State is written using as TypeScript `class` - initializers, reducers, actions are derived from that\n2. You can choose Redux or Context API (or both for that matter)\n3. Partial Redux Devtools support for React Context API\n4. Just use normal `async` - no fancy library needed for async operations\n5. Typed with TypeScript\n\nAlso the library imposes no direct dependencies, after it has compiled the sources, you do not need the compiler any more - the resulting files have not dependencies to anything else than proven libraries like React, Immer etc.\n\nAnd as an added bonus, we get selector support with `reselect` using method getters!\n\n## Acknowledgements\n\nThis library would not have been possible without following great OS tools:\n\n- [ts-simple-ast](https://github.com/dsherret/ts-simple-ast) for AST code management (special thanks for library author David Sherret for extremely fast responses while I was having problems during development!)\n- [immer](https://github.com/mweststrate/immer) for easy immutable transformations\n- [reselect](https://github.com/reduxjs/reselect) selector library for Redux\n- [yargs](https://github.com/yargs/yargs) for command line processing\n- [Redux Devtools Extensions](https://github.com/zalmoxisus/redux-devtools-extension)\n- and of course [Redux](https://github.com/reduxjs/redux) and [React and the new Context API](https://reactjs.org/docs/context.html)\n\nAlso inspiration sources were fellow coders at [Koodiklinikka](https://github.com/koodiklinikka), developers at [Leonidas](https://leonidasoy.fi/) and several blog article writers [1](https://daveceddia.com/context-api-vs-redux/)[2](https://medium.freecodecamp.org/replacing-redux-with-the-new-react-context-api-8f5d01a00e8c)\n\n## Introduction\n\nThe simplest way of writing a stateful model is simply creating a simple TypeScript class would be like this\n\n```typescript\nexport class SimpleModel {\n  items: any[] = [];\n  async getItems() {\n    // get some data from the internet\n    this.items = (await axios.get(\n      \"https://jsonplaceholder.typicode.com/todos\"\n    )).data;\n  }\n}\n```\n\nOr if you prefer the classical increment / decrement example\n\n```typescript\nexport class IncModel {\n  cnt: number = 0;\n  increment() {\n    this.cnt++;\n  }\n  decrement() {\n    this.cnt--;\n  }\n}\n```\n\nThe question asked was: would it be possible to transfer this simple state representation automatically to Redux? Or even to React Context API?\n\nTurns out with a little bit of [compiler magic](https://github.com/dsherret/ts-simple-ast) we can transform the _idea_ of the class into both Redux and React Context API representations. The Redux compiling will create necessary Actions, Enumerations and Reducers, Combined Reducers and MapStateToProps, MapDispatchToProps to manage the component state correctly.\n\nThe compiler does not need much help, we need to add the [JSDoc](http://usejsdoc.org/) comment property before the class and also we need to remember to give types to the properties of the class.\n\n```typescript\n/**\n * @redux true\n */\nexport class IncModel {\n  // ...\n}\n```\n\nThen we can compile the model\n\n```\n  ts2redux \u003cpath\u003e\n```\n\nAnd the directory will have `reducers/` directory where `IncModel` and `SimpleModel` are defined [IncModel.tsx](https://github.com/terotests/ts2redux/blob/master/src/frontend/models/reducers/IncModel.tsx) and  \n[SimpleModel.tsx](https://github.com/terotests/ts2redux/blob/master/src/frontend/models/reducers/IncModel.tsx) together with all Redux ceremony and more.\n\n## Custom combineReducers\n\nThe generated `index.ts` will export `reducerObject` with all the parameters\nrequired for `combineReducers`\n\n## Using with React Connected Router\n\nUse the exported `reducerObject` from `index.ts` as parameter to `combineReducers` and follow the installation instructions from the Connected React Router.\n\nIf you want to user router from the model itself use Generic Dispatcher described below.\n\n## Generic Dispatcher\n\nGeneric dispatch is available to `async` functions. To create a generic dispatcher you can add JSDoc comment `@dispatch true`\n\n```\n  /**\n   * @dispatch true\n   * @param action\n   */\n  async MyDispatcher(action: any) {\n    // you can give any dispatcher action as parameter to this function\n  }\n```\n\nSometimes you want to create a generic dispatcher, for example when you want to\nconnect your state to Connected React Router you should call it something like this\n\n```typescript\nthis.MyDispatcher(push(\"/path/to/somewhere\"));\n```\n\nOriginal example from [Connected React Router](https://github.com/supasate/connected-react-router/blob/master/FAQ.md#how-to-navigate-with-redux-action) documentation.\n\n## Private functions\n\nPrivate functions and functions which return value are not compiled as reducers\n\n```typescript\n/**\n * @redux true\n */\nexport class MyModel {\n  // will not be compiled as reducer\n  private someCalculation(value) {}\n  // will not be compiled as reducer\n  someCalculation(value): number {\n    return 100;\n  }\n}\n```\n\n## Limitations\n\n### Async Functions can not mutate state deeply (synchronous can)\n\nIf you want to mutate state deeply from `async` function you must call first syncronous function.\n\n`async` function can read state but can only assign (`=`) to class properties, which generates a dispatch. Do not mutate state deeply in asyncronous functions, that will not work and will generate error\n\n```typescript\n// this is OK\nthis.items = [];\n// this is error, no dispatch generated, Redux will complain about this too\nthis.items.sort(/*... */);\n```\n\n### Functions can only have one parameter\n\n```typescript\nclass Foo {\n  // OK\n  hello(message: { sender: string; receiver: string }) {}\n  // this is ERROR\n  hello(sender: string, receiver: string) {}\n}\n```\n\nThe reason for this is just simplicity: the first parameter is compiled directly to the actions payload. In the future the compiler might compile functions with variable number of parameters directly to the payload, but this is not supported at the moment.\n\n### React Context API -components are not removed from Redux Devtools after unmount\n\nIf you generate a lot of Redux Context API -components and Redux Devtools is enabled, history of unmounted components is visible in the Redux Devtools debugging history. In some cases this may be desirable, in some cases not.\n\nIn case the component is unmounted, it's listeners are unsubscribed and time travel will not work.\n\n## Using React Context API\n\n```typescript\n// for the new ReactContext API\nimport { IncModelConsumer, IncModelProvider } from \"./models/reducers/IncModel\";\n```\n\nFor React Context API we simply create a upper level `\u003cmodel\u003eProvider` and lower in the VDOM tree use `\u003cmodel\u003eConsumer` to render components or to call methods of the model.\n\n```typescript\n\u003cIncModelProvider\u003e\n  \u003cIncModelConsumer\u003e\n    {state =\u003e (\n      \u003cdiv\u003e\n        \u003cdiv\u003e{state.cnt}\u003c/div\u003e\n        \u003cbutton onClick={state.increment}\u003e+\u003c/button\u003e\n        \u003cbutton onClick={state.decrement}\u003e-\u003c/button\u003e\n      \u003c/div\u003e\n    )}\n  \u003c/IncModelConsumer\u003e\n\u003c/IncModelProvider\u003e\n```\n\n## Using Redux\n\nFor Redux the compiler generates the main reducer import in\n\n```typescript\nimport { reducers } from \"./models/reducers/\";\n```\n\nThis is pretty standard Redux stuff, the main reducer is given then to the `createStore`\n\n```typescript\nlet store = createStore(reducers /** other params*/);\n```\n\nAfter which you create `\u003cProvider store={store}\u003e` normally.\n\nThe IncModel -component would look like this:\n\n```typescript\n// impor the IncModel\nimport * as container from \"../models/reducers/IncModel\";\n\n// abstract properties version of the component\nexport interface Props extends container.Props {}\n\n// this component can be re-used\nexport const AbstractInc = (props: Props) =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003cdiv\u003e{props.cnt}\u003c/div\u003e\n      \u003cbutton onClick={props.increment}\u003e+\u003c/button\u003e\n      \u003cbutton onClick={props.decrement}\u003e-\u003c/button\u003e\n    \u003c/div\u003e\n  );\n};\n// Connect the abstract component to the Redux model\nexport const ReduxInc = container.StateConnector(AbstractInc);\n```\n\n# Selectors\n\nSelectors are great, if you want to avoid expensive recalculations and optimize rendering performance using PureComponents.\n\nTo create a selector, define function with `get` -modifier like `get someProperty() : someReturnValueType`. This will create a new property `someProperty` which can be used as a cached result of some computation based on the model.\n\nFor example see code from [TodoList.ts](https://github.com/terotests/ts2redux/blob/master/src/frontend/models/TodoList.ts#L33-L38)\n\n```typescript\nexport class TodoList {\n\n  // ... some model parameters used to transform the list...\n  items: TodoListItem[] = []\n  sortOrder:SortOrder = SortOrder.ASC\n  listStart:number = 0\n  listPageLength:number = 10\n\n  // use this like \u003cPureList items={props.listToDisplay}/\u003e\n  get listToDisplay() : TodoListItem[] {\n    return this.items\n      .filter( item =\u003e item.completed )\n      .sort( sortFn(this.sortOrder) )\n      .slice( this.listStart, this.listStart + this.listPageLength)\n  }\n```\n\nThe advantage of selector is that value is memoized and will only update if parameters affecting it's value will change. In the example above,`listToDisplay` is recalculated only if the value of `items`, `sortOrder`, `listStart` or `listPageLength`changes.\n\nIf property above is given to a `PureComponent` like this\n\n```jsx\n\u003cPureList items={props.listToDisplay} /\u003e\n```\n\nThe component will render only when parameters affecting it's computation change.\n\n# Examples\n\nSome example of Models are available in [src/frontend/models](https://github.com/terotests/ts2redux/tree/master/src/frontend/models) -directory.\n\n## Error handling in async functions\n\nIn typical Redux code you want to have some kind of loading state\n\n```typescript\nexport type TaskState = \"UNDEFINED\" | \"RUNNING\" | \"LOADED\" | \"ERROR\";\n```\n\nAny kind of loading state is pretty easy to implement, for example\n\n```typescript\nclass TodoList {\n  items: TodoListItem[] = [];\n  state: TaskState = \"UNDEFINED\";\n  async getItems() {\n    if (this.state === \"RUNNING\") return;\n    try {\n      this.state = \"RUNNING\";\n      this.items = (await axios.get(\n        \"https://jsonplaceholder.typicode.com/todos\"\n      )).data;\n      this.state = \"LOADED\";\n    } catch (e) {\n      this.state = \"ERROR\";\n    }\n  }\n}\n```\n\n# License\n\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fterotests%2Fts2redux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fterotests%2Fts2redux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fterotests%2Fts2redux/lists"}