├── .editorconfig ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .node-version ├── LICENSE ├── README.md ├── docs ├── api-form │ ├── FormActions.md │ └── createForm.md ├── api-router │ ├── Link.md │ ├── RouterActions.md │ ├── createUseRouter.md │ └── getRouterState.md ├── api │ ├── ChainedReducer.md │ ├── Epic.md │ ├── Handle.md │ ├── createModule.md │ ├── createSelector.md │ ├── rx.md │ ├── useActions.md │ ├── useMappedState.md │ └── useSelector.md ├── introduction │ ├── examples.md │ ├── motivation.md │ ├── quick-start.md │ ├── roadmap.md │ └── starter-kits.md └── using-typeless │ ├── actions.md │ ├── code-splitting.md │ └── hmr.md ├── examples ├── .gitignore ├── README.md ├── package.json ├── src │ ├── basic-form │ │ ├── components │ │ │ └── FormInput.tsx │ │ ├── features │ │ │ └── example │ │ │ │ ├── form.ts │ │ │ │ ├── interface.ts │ │ │ │ ├── module.tsx │ │ │ │ └── symbol.ts │ │ ├── index.html │ │ └── index.tsx │ ├── basic-hmr │ │ ├── components │ │ │ └── App.tsx │ │ ├── features │ │ │ └── counter │ │ │ │ ├── components │ │ │ │ └── Counter.tsx │ │ │ │ ├── interface.ts │ │ │ │ ├── module.tsx │ │ │ │ └── symbol.ts │ │ ├── index.html │ │ └── index.tsx │ ├── basic-routing │ │ ├── index.html │ │ ├── index.tsx │ │ └── router.ts │ ├── code-splitting │ │ ├── features │ │ │ ├── main │ │ │ │ ├── components │ │ │ │ │ └── MainView.tsx │ │ │ │ ├── interface.ts │ │ │ │ ├── module.tsx │ │ │ │ └── symbol.ts │ │ │ ├── subA │ │ │ │ ├── components │ │ │ │ │ └── SubAView.tsx │ │ │ │ ├── interface.ts │ │ │ │ ├── module.tsx │ │ │ │ └── symbol.ts │ │ │ ├── subB │ │ │ │ ├── components │ │ │ │ │ └── SubBView.tsx │ │ │ │ ├── interface.ts │ │ │ │ ├── module.tsx │ │ │ │ └── symbol.ts │ │ │ └── subC │ │ │ │ ├── components │ │ │ │ └── SubCView.tsx │ │ │ │ ├── interface.ts │ │ │ │ ├── module.tsx │ │ │ │ └── symbol.ts │ │ ├── index.html │ │ └── index.tsx │ ├── counter │ │ ├── index.html │ │ └── index.tsx │ ├── libraries.d.ts │ ├── real-api │ │ ├── features │ │ │ └── cat │ │ │ │ ├── components │ │ │ │ └── CatView.tsx │ │ │ │ ├── interface.ts │ │ │ │ ├── module.tsx │ │ │ │ └── symbol.ts │ │ ├── index.html │ │ └── index.tsx │ └── socket-hmr │ │ ├── components │ │ └── App.tsx │ │ ├── features │ │ └── socket │ │ │ ├── components │ │ │ └── SocketView.tsx │ │ │ ├── interface.ts │ │ │ ├── module.tsx │ │ │ └── symbol.ts │ │ ├── index.html │ │ ├── index.tsx │ │ └── socket.tsx ├── tsconfig.json └── yarn.lock ├── lerna.json ├── package.json ├── packages ├── tsconfig.base.json ├── typeless-form │ ├── .prettierignore │ ├── README.md │ ├── __tests__ │ │ ├── type │ │ │ ├── TypeTester.ts │ │ │ ├── createForm.test.ts │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.notStrict.json │ │ └── unit │ │ │ ├── empty.test.ts │ │ │ └── tsconfig.json │ ├── package.json │ ├── src │ │ ├── FormContext.ts │ │ ├── createForm.tsx │ │ └── index.ts │ ├── tsconfig.cjs.json │ ├── tsconfig.json │ └── tslint.json ├── typeless-router │ ├── .babelrc │ ├── .prettierignore │ ├── README.md │ ├── __tests__ │ │ ├── type │ │ │ ├── TypeTester.ts │ │ │ ├── routing.test.ts │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.notStrict.json │ │ └── unit │ │ │ ├── routing.test.tsx │ │ │ └── tsconfig.json │ ├── package.json │ ├── src │ │ ├── Link.tsx │ │ ├── index.ts │ │ ├── module.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tsconfig.cjs.json │ ├── tsconfig.json │ └── tslint.json └── typeless │ ├── .babelrc │ ├── .prettierignore │ ├── README.md │ ├── __tests__ │ ├── type │ │ ├── Epic.test.ts │ │ ├── TypeTester.ts │ │ ├── createModule.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.notStrict.json │ │ └── useMappedState.test.ts │ └── unit │ │ ├── ChainedReducer.test.ts │ │ ├── createModule.test.tsx │ │ ├── createSelector.test.ts │ │ ├── custom-scheduler.test.tsx │ │ ├── epic-hmr.test.tsx │ │ ├── epic-ignore.test.tsx │ │ ├── epic-order.test.tsx │ │ ├── epic-unit.test.ts │ │ ├── helpers.tsx │ │ ├── integration.test.tsx │ │ ├── lib.d.ts │ │ ├── registry.test.ts │ │ ├── tsconfig.json │ │ ├── useMappedState.test.tsx │ │ ├── useSelector.test.tsx │ │ └── util.test.ts │ ├── package.json │ ├── rx │ └── package.json │ ├── src │ ├── ChainedReducer.ts │ ├── Epic.ts │ ├── Notify.ts │ ├── Registry.ts │ ├── StateLogger.ts │ ├── Store.ts │ ├── TypelessContext.tsx │ ├── createModule.ts │ ├── createSelector.ts │ ├── index.ts │ ├── onHmr.tsx │ ├── rx │ │ ├── ofType.ts │ │ ├── rx.ts │ │ └── waitForType.ts │ ├── types.ts │ ├── useActions.ts │ ├── useMappedState.ts │ ├── useRegistry.ts │ ├── useSelector.ts │ └── utils.ts │ ├── tsconfig.cjs.json │ ├── tsconfig.json │ └── tslint.json ├── prettier.config.js ├── tsconfig.json ├── tsconfig.prod.json ├── website ├── README.md ├── core │ └── Footer.js ├── i18n │ └── en.json ├── package.json ├── pages │ └── en │ │ ├── help.js │ │ ├── index.js │ │ └── users.js ├── sidebars.json ├── siteConfig.js ├── static │ ├── css │ │ ├── code-block-buttons.css │ │ └── custom.css │ ├── img │ │ ├── dev │ │ │ ├── 001-analysis.svg │ │ │ ├── 002-architecture.svg │ │ │ ├── 003-bug.svg │ │ │ ├── 004-cloud.svg │ │ │ ├── 005-database.svg │ │ │ ├── 006-deployment.svg │ │ │ ├── 007-graphic-design.svg │ │ │ ├── 008-developer.svg │ │ │ ├── 009-development.svg │ │ │ ├── 010-dynamic.svg │ │ │ ├── 011-ecommerce.svg │ │ │ ├── 012-encryption.svg │ │ │ ├── 013-engineering.svg │ │ │ ├── 014-information.svg │ │ │ ├── 015-injection.svg │ │ │ ├── 016-intelligent.svg │ │ │ ├── 017-interaction.svg │ │ │ ├── 018-launch.svg │ │ │ ├── 019-layout.svg │ │ │ ├── 020-mining.svg │ │ │ ├── 021-network.svg │ │ │ ├── 022-platform.svg │ │ │ ├── 023-programming.svg │ │ │ ├── 024-requirement.svg │ │ │ ├── 025-security.svg │ │ │ ├── 026-seo-and-web.svg │ │ │ ├── 027-server.svg │ │ │ ├── 028-server-1.svg │ │ │ ├── 029-server-2.svg │ │ │ ├── 030-support.svg │ │ │ ├── 031-testing.svg │ │ │ ├── 032-ux.svg │ │ │ ├── 033-web.svg │ │ │ ├── 034-website.svg │ │ │ ├── 035-wireframe.svg │ │ │ └── 036-writing.svg │ │ ├── docusaurus.svg │ │ ├── favicon.png │ │ ├── favicon │ │ │ └── favicon.ico │ │ ├── github-brands.svg │ │ ├── logo-white.svg │ │ ├── logo.svg │ │ └── oss_logo.png │ └── js │ │ └── code-block-buttons.js └── yarn.lock └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 80 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | push: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Get yarn cache directory path 17 | id: yarn-cache-dir-path 18 | run: echo "::set-output name=dir::$(yarn cache dir)" 19 | 20 | - uses: actions/cache@v1 21 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 22 | with: 23 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 24 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 25 | restore-keys: | 26 | ${{ runner.os }}-yarn- 27 | 28 | - name: Use Node.js 12 29 | uses: actions/setup-node@v1 30 | with: 31 | node-version: 12.x 32 | 33 | - name: install yarn 34 | run: yarn install --frozen-lockfile 35 | 36 | - name: test 37 | run: yarn run test 38 | 39 | - name: report coverage 40 | run: bash <(curl -s https://codecov.io/bash) 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | website/build 13 | *.tsbuildinfo 14 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 12.16.3 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Łukasz Sentkiewicz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typeless 2 | 3 | TypeScript + React Hooks + RxJS = 😻 4 | 5 | ![Build Status](https://github.com/typeless-js/typeless/workflows/ci/badge.svg) 6 | [![npm module](https://badge.fury.io/js/typeless.svg)](https://www.npmjs.org/package/typeless) 7 | 8 | ## Installation 9 | 10 | Required peer dependencies: `react@^16.8` and `rxjs^@6` 11 | 12 | ```bash 13 | npm i typeless 14 | yarn add typeless 15 | ``` 16 | 17 | ## Why Typeless? 18 | 19 | Creating scalable React apps with TypeScript can be painful. There are many small libraries that can be combined, but none of them provide a complete solution for building complex applications. 20 | `typeless` provide all building blocks: actions creators, reducers, epics with a minimal overhead of type annotation. 21 | 22 | ## Features 23 | 24 | - Designed for TypeScript and type safety. Only minimal type annotations are required, all types are inferred where possible. 25 | - Simple and developer friendly syntax with React hooks. 26 | - Event-driven architecture using RxJS. 27 | - Reducers and epics are loaded dynamically in React components. There is no single `reducers.ts` or `epics.ts` file. 28 | - Code splitting for reducers and epics work out of the box. 29 | - HMR works out of the box. 30 | 31 | ## Quick start 32 | 33 | [https://typeless.js.org/introduction/quick-start](https://typeless.js.org/introduction/quick-start) 34 | 35 | ## License 36 | 37 | MIT 38 | -------------------------------------------------------------------------------- /docs/api-form/FormActions.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: FormActions 3 | title: FormActions 4 | hide_title: true 5 | sidebar_label: FormActions 6 | --- 7 | 8 | 9 | 10 | # FormActions 11 | The actions creators for form module. 12 | 13 | ## Actions 14 | #### Your application can dispatch below actions. 15 | 1. `blur: (field: string)` mark field as touched 16 | 2. `change: (field: string, value: any)` update the field value 17 | 3. `changeMany: (values: object)` update multiple values, if a field is not defined in`values` it won't be set to undefined 18 | 4. `replace: (values: object)` replace all values with the provided values 19 | 5. `touchAll` mark all fields as touched 20 | 6. `submit` validate data and trigger setSubmitFailed/setSubmitSucceeded 21 | 7. `reset` reset the module, clear values, errors and touched status 22 | 8. `resetTouched` reset touched status 23 | 24 | #### Your application should not dispatch below actions, they are used only internally. 25 | 1. `setErrors: (errors: object)` set validation errors (result from `validate` function) 26 | 2. `setSubmitSucceeded` dispatched if submit was successful 27 | 3. `setSubmitFailed` dispatched if submit was not successful (there are validation errors) 28 | 4. `validate` trigger validation -------------------------------------------------------------------------------- /docs/api-router/Link.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Link 3 | title: Link 4 | hide_title: true 5 | sidebar_label: Link 6 | --- 7 | 8 | 9 | 10 | # Link 11 | A basic react component for navigation. It has exactly the same props as a native `` component. 12 | 13 | #### Example 14 | 15 | ```tsx 16 | import { Link } from 'typeless-router'; 17 | 18 | 19 | export function Foo() { 20 | return ( 21 |
22 | 23 |
24 | ); 25 | } -------------------------------------------------------------------------------- /docs/api-router/RouterActions.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: RouterActions 3 | title: RouterActions 4 | hide_title: true 5 | sidebar_label: RouterActions 6 | --- 7 | 8 | 9 | 10 | # RouterActions 11 | The actions creators for router module. 12 | 13 | ## Actions 14 | 1. `$init` typeless lifecycle method 15 | 2. `$unmounted` typeless lifecycle method 16 | 3. `dispose` dispatch this action to stop listening for history API changes. Can be useful in unit testing. 17 | 4. `locationChange: (location: RouterLocation)` dispatched by router module after location changed. 18 | 5. `push: (location: LocationChange)` dispatch this action to change the location, and add a new entry to the stack. 19 | 6. `replace: (location: LocationChange)` similar to `push`, but it doesn't add a new entry to the stack. 20 | 21 | 22 | 23 | ## Types 24 | ```ts 25 | type LocationChange = 26 | | string 27 | | { 28 | pathname: string; 29 | search?: string; 30 | }; 31 | 32 | interface RouterLocation { 33 | pathname: string; 34 | search: string; 35 | type: 'push' | 'replace'; 36 | } 37 | ``` -------------------------------------------------------------------------------- /docs/api-router/createUseRouter.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: createUseRouter 3 | title: createUseRouter 4 | hide_title: true 5 | sidebar_label: createUseRouter 6 | --- 7 | 8 | 9 | 10 | # createUseRouter(options) 11 | Create a new router module. 12 | 13 | #### Arguments 14 | 1. `options: HistoryOptions` - the options: 15 | - `type: 'browser' | 'hash'` - use `browser` for html5 navigation, or use `hash` to keep routing after `#` in the url. Default `browser`. 16 | #### Returns 17 | `{Function}` - a React hook used to mount the module. 18 | 19 | 20 | #### Example 21 | 22 | ```tsx 23 | // router.ts 24 | import { createUseRouter } from 'typeless-router'; 25 | 26 | const useRouter = createUseRouter(); 27 | 28 | // App.tsx 29 | import { useRouter } from './router'; 30 | 31 | export function App() { 32 | // recommended to mount the module in the main component 33 | useRouter(); 34 | 35 | return
...
36 | } -------------------------------------------------------------------------------- /docs/api-router/getRouterState.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: getRouterState 3 | title: getRouterState 4 | hide_title: true 5 | sidebar_label: getRouterState 6 | --- 7 | 8 | 9 | 10 | # getRouterState 11 | A getter for router state. 12 | 13 | 14 | 15 | ## Types 16 | 17 | ```ts 18 | interface RouterLocation { 19 | pathname: string; 20 | search: string; 21 | type: 'push' | 'replace'; 22 | } 23 | 24 | interface RouterState { 25 | location: RouterLocation | null; 26 | prevLocation: RouterLocation | null; 27 | } 28 | ``` -------------------------------------------------------------------------------- /docs/api/Handle.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Handle 3 | title: Handle 4 | hide_title: true 5 | sidebar_label: Handle 6 | --- 7 | 8 | # Handle 9 | Handle is an object created by `createModule`. It allows attaching epic handlers and reducers. 10 | 11 | ## Mounting handle 12 | Returned handle by `createModule` is a React hook function. Invoke it to mount it in the application. 13 | 14 | #### Example 15 | 16 | ```tsx 17 | // module.ts 18 | import { handle } from './interface'; 19 | 20 | // export it with `use` prefix 21 | export const useMyModule = handle; 22 | 23 | // or use directly in the module component 24 | export function MyModule() { 25 | handle(); 26 | 27 | return ( 28 |
foo
29 | ) 30 | } 31 | ``` 32 | 33 | ## Methods 34 | ### `epic()` 35 | Initialize a new epic. Epics are used to handle side effects. 36 | Calling `epic()` multiple times will reset previously created epic. 37 | #### Returns 38 | [`{Epic}`](/api/Epic) - the created epic 39 | 40 | #### Example 41 | ```ts 42 | import { handle } from './interface'; 43 | 44 | handle.epic() 45 | .on(SomeActions.foo, () => {...}) 46 | .on(SomeActions.bar, () => {...}) 47 | ``` 48 | 49 | --- 50 | 51 | ### `reducer(initialState: TState)` 52 | Initialize a new chained reducer. Reducers are used to modify the state. 53 | Calling `reducer()` multiple times will reset previously created reducer. 54 | #### Arguments 55 | 1. `initialState: object`- the initial state. 56 | #### Returns 57 | [`{ChainedReducer}`](/api/ChainedReducer) - the created chained reducer 58 | 59 | #### Example 60 | ```ts 61 | import { handle } from './interface'; 62 | 63 | handle.reducer({user: null, isLoading: false}) 64 | .on(SomeActions.foo, () => {...}) 65 | .on(SomeActions.bar, () => {...}) 66 | ``` 67 | 68 | --- 69 | 70 | ### `reset()` 71 | Reset created epic and reducer. 72 | -------------------------------------------------------------------------------- /docs/api/createSelector.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: createSelector 3 | title: Create Selector 4 | hide_title: true 5 | sidebar_label: createSelector 6 | --- 7 | 8 | # createSelector(...selectors, resultFunc) 9 | 10 | Create a memoized selector from the state getters. 11 | 12 | #### Arguments 13 | 14 | 1. `selectors: ...(Selector | [StateGetter, (state) => result])`- an arguments of input selector. Each element can be either: 15 | - another selector created with `createSelector` 16 | - a tuple with two elements: a state getter created by `createModule`, a selector function 17 | 2. `resultFunc: (...args: any) => any` - the result function for computing input arguments. 18 | 19 | #### Returns 20 | 21 | `() => object` - the memoized function for returning computed state. If you want to use selector in React Component, use [`useSelector`](/api/useSelector). 22 | 23 | #### Example 24 | 25 | ```tsx 26 | // symbol.ts 27 | export const TodoSymbol = Symbol('todo'); 28 | 29 | // interface.ts 30 | import { createModule } from 'typeless'; 31 | import { TodoSymbol } from './interface'; 32 | 33 | export const [handle, TodoActions, getTodoState] = createModule(TodoSymbol) 34 | .withActions({ }) 35 | .withState; 36 | 37 | export interface TodoState { 38 | filter: 'all' | 'not-deleted'; 39 | todos: Array<{ 40 | id: number; 41 | text: string; 42 | isDeleted: boolean; 43 | }> 44 | } 45 | 46 | // selectors.ts 47 | import { createSelector } from 'typeless'; 48 | import { getTodoState } from './interface'; 49 | 50 | export const getTodos = createSelector( 51 | [getTodoState, state => state.filter], 52 | [getTodoState, state => state.todos], 53 | (filter, todos) => { 54 | if (filter === 'all') { 55 | return todos; 56 | } 57 | return todos.filter(x => !x.isDeleted); 58 | } 59 | ); 60 | 61 | // components/TodoList.tsx 62 | import { useSelector } from 'typeless'; 63 | import { getTodos } from '../selectors'; 64 | 65 | function TodoList() { 66 | // use your selector with useSelector 67 | const todos = useSelector(getTodos); 68 | return ( 69 |
70 | {todos.map(todo => ( 71 | 72 | ))} 73 |
74 | ); 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/api/rx.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: rx 3 | title: Rx 4 | hide_title: true 5 | sidebar_label: Rx 6 | --- 7 | 8 | # Rx 9 | `Rx` is a convenience re-export of rxjs. Check [this issue](https://github.com/ReactiveX/rxjs/issues/3622) for motivation. 10 | 11 | The entry point contains: 12 | - all exports from `rxjs/operators` 13 | - Following exports from `rxjs`: 14 | `Subject`, `forkJoin`, `empty`, `of`, `timer`, `from`, `defer`, `Observable`, `interval` 15 | - Renamed exports from `rxjs`: 16 | - `concat` -> `concatObs` 17 | - `merge` -> `mergeObs` 18 | - `race` -> `raceObs` 19 | - `throwError` -> `throwObs` 20 | - Following exports from `rxjs/internal-compatibility`: 21 | `fromPromise` 22 | 23 | ## Additional operators 24 | 25 | ### `ofType(actionCreator)` 26 | Filter actions based on the provided action creator or action creators. 27 | #### Arguments 28 | 1. `actionCreator: AC | AC[]` - the action creator or array of action creators. 29 | #### Returns 30 | `{OperatorFunction}` - the rxjs operator function. 31 | 32 | #### Example 33 | ```ts 34 | // symbol.ts 35 | export const MySymbol = Symbol('my'); 36 | 37 | // interface.ts 38 | import { createModule } from 'typeless'; 39 | import { MySymbol } from './symbol'; 40 | 41 | export const [handle, MyActions] = createModule(MySymbol) 42 | .withActions({ 43 | loadUser: null, 44 | userLoaded: (user: User) => ({ payload: { user } }), 45 | cancel: null, 46 | }); 47 | 48 | // module.ts 49 | import * as Rx from 'typeless/rx'; 50 | import { handle, MyActions } from 'typeless'; 51 | 52 | handle.epic() 53 | .on(MyActions.loadUser, (_, { action$ }) => 54 | API.loadUser().pipe( 55 | Rx.map(user => MyActions.userLoader(user)), 56 | // cancel operation if `MyActions.cancel()` is dispatched 57 | Rx.takeUntil(action$.pipe(Rx.ofType(MyActions.cancel))) 58 | ) 59 | ); 60 | ``` 61 | 62 | ### waitForType(actionCreator) 63 | Wait for a single action creator. 64 | #### Arguments 65 | 1. `actionCreator: AC ` - the action creator to wait for. 66 | #### Returns 67 | `{OperatorFunction}` - the rxjs operator function. 68 | 69 | #### Example 70 | ```ts 71 | // symbol.ts 72 | export const MySymbol = Symbol('my'); 73 | 74 | // interface.ts 75 | import { createModule } from 'typeless'; 76 | import { MySymbol } from './symbol'; 77 | 78 | export const [handle, MyActions] = createModule(MySymbol) 79 | .withActions({ 80 | deleteUser: null, 81 | userDeleted: null, 82 | errorOccurred: null, 83 | confirmDelete: (confirm: boolean) => ({ payload: { confirm } }), 84 | }); 85 | 86 | // module.ts 87 | import * as Rx from 'typeless/rx'; 88 | import { handle, MyActions } from 'typeless'; 89 | 90 | handle.epic() 91 | // show a confirmation dialog and wait for Yes/No click 92 | .on(MyActions.deleteUser, (_, { action$ }) => 93 | action$.pipe( 94 | Rx.waitForType(MyActions.confirmDelete), 95 | Rx.filter(action => action.payload.confirm) 96 | Rx.mergeMap(() => API.deleteUser()), 97 | Rx.mapTo(MyActions.userDeleted()), 98 | Rx.catchError(e => { 99 | console.error(e); 100 | return Rx.of(MyActions.errorOccurred()); 101 | }) 102 | ) 103 | ); 104 | ``` -------------------------------------------------------------------------------- /docs/api/useActions.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: useActions 3 | title: useActions 4 | hide_title: true 5 | sidebar_label: useActions 6 | --- 7 | 8 | 9 | 10 | # useActions(actionCreators) 11 | React Hook for binding actions creators with `dispatch`. 12 | 13 | #### Arguments 14 | 1. `actionCreators: {[action: string]: ActionCreator}` - the action creators created by [`createModule`](createModule). 15 | 16 | #### Returns 17 | `{[action: string]: Function}` - the mapped actions. 18 | 19 | 20 | #### Example 21 | 22 | ```tsx 23 | // symbol.ts 24 | export const CounterSymbol = Symbol('counter'); 25 | 26 | // interface.ts 27 | import { createModule } from 'typeless'; 28 | import { CounterSymbol } from './symbol'; 29 | 30 | export const [handle, CounterActions] = createModule(CounterSymbol) 31 | .withActions({ 32 | increase: null, 33 | }); 34 | 35 | // module.tsx 36 | import { CounterActions } from './interface'; 37 | 38 | export function Counter() { 39 | const { increase } = useActions(CounterActions); 40 | return 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/api/useMappedState.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: useMappedState 3 | title: useMappedState 4 | hide_title: true 5 | sidebar_label: useMappedState 6 | --- 7 | 8 | # useMappedState(stateGetters, mapperFn[, equalityFn][, deps]) 9 | 10 | React Hook for accessing the State. 11 | For most use cases it's enough to use a shorthand version `getCounterState.useState()`. 12 | 13 | #### Arguments 14 | 15 | 1. `stateGetters: Array` - the array of state getters created by `createdModule`. 16 | 2. `mapperFn: (state1: object, state2: object) => object` - the function for mapping provided states. It will be executed on every store change. The number of arguments is equal to the number of elements in `stateGetters`. 17 | 3. `equalityFn?: (a: unknown, b: unknown) => boolean` - the function for checking that new result value of `mapperFn` equals the old value. If returns `false`, `useMappedState` execute re-render Component.([Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#Description) algorithm used by default) 18 | 4. `deps?: unknown[]` - the external dependencies used inside the hook. Omit it or pass `[]` if there are dependencies. 19 | 20 | #### Returns 21 | 22 | `{object}` - the object returned by `mapperFn`. 23 | 24 | #### Example 25 | 26 | ```tsx 27 | // symbol.ts 28 | export const CounterSymbol = Symbol('counter'); 29 | 30 | // interface.ts 31 | import { createModule } from 'typeless'; 32 | import { CounterSymbol } from './symbol'; 33 | 34 | export const [handle, CounterActions] = createModule(CounterSymbol) 35 | .withActions({ 36 | startCount: null, 37 | }) 38 | .withState(); 39 | 40 | interface CounterState { 41 | count: number; 42 | isLoading: number; 43 | } 44 | 45 | // module.tsx 46 | import { handle, CounterActions, getCounterState } from './interface'; 47 | 48 | // mount reducer actions 49 | handle.reducer({ count: 0, isLoading: false }); 50 | 51 | export function Counter() { 52 | handle(); 53 | const { startCount } = useActions(CounterActions); 54 | const { isLoading, count } = useMappedState([getCounterState], state => state); 55 | // or 56 | // const { isLoading, count } = getCounterState.useState(); 57 | 58 | return ( 59 |
60 | 63 |
count: {count}
64 |
65 | ); 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/api/useSelector.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: useSelector 3 | title: useSelector 4 | hide_title: true 5 | sidebar_label: useSelector 6 | --- 7 | 8 | # useSelector(selector[, equalityFn]) 9 | 10 | React Hook for accessing the Selector result. 11 | 12 | #### Arguments 13 | 14 | 1. `selector: Selector`- a selector function created with `createSelector`. 15 | 2. `equalityFn?: (a: unknown, b: unknown) => boolean` - the function for checking that new result value of `Selector` equals the old value. For more info: [useMappedState](/api/useMappedState) 16 | 17 | #### Returns 18 | 19 | `{object}` - the object returned by `Selector`. 20 | 21 | #### Example 22 | 23 | Full example: [`createSelector`](/api/createSelector#example) 24 | 25 | ```tsx 26 | // components/TodoList.tsx 27 | function TodoList() { 28 | // use your selector with useSelector 29 | const todos = useSelector(getTodos); 30 | return ( 31 |
32 | {todos.map(todo => ( 33 | 34 | ))} 35 |
36 | ); 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/introduction/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: examples 3 | title: Examples 4 | hide_title: true 5 | sidebar_label: Examples 6 | --- 7 | 8 | # Examples 9 | 10 | ### Counter 11 | A simple button with an async counter. 12 |
Live demo 13 | Source code 14 | 15 | ---- 16 | 17 | ### Real API 18 | Load 😺 from API with cancellation and retry on errors. 19 | Live demo 20 | Source code 21 | 22 | 23 | ---- 24 | 25 | ### Code-Splitting 26 | A simple demo with code splitting for modules using React Suspense and React Lazy. 27 | Live demo 28 | Source code 29 | 30 | ---- 31 | 32 | ### Basic HMR 33 | A basic demo with HMR support. 34 | Live demo 35 | Source code 36 | 37 | ---- 38 | 39 | ### Sockets with HMR 40 | Subscribe to the socket with developer-friendly HMR support. 41 | Create epic handlers that are reloadable or non-reloadable. 42 | Live demo 43 | Source code 44 | 45 | ---- 46 | 47 | ### Routing 48 | A simple example of routing with `typeless-router` 49 | Live demo 50 | Source code 51 | 52 | ---- 53 | 54 | ### Form 55 | A simple example of forms with `typeless-form` 56 | Live demo 57 | Source code 58 | -------------------------------------------------------------------------------- /docs/introduction/motivation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: motivation 3 | title: Motivation 4 | hide_title: true 5 | sidebar_label: Motivation 6 | --- 7 | 8 | # Motivation 9 | 10 | ## Problems 11 | 12 | Creating Redux applications in TypeScript can be overwhelming. Many developers face the same problems: 13 | 14 | - **Too much boilerplate code** 15 | Creating actions types, actions creators, defining RootAction and RootState types require a lot of typing! 16 | 17 | - **Double annotation problem** 18 | Many libraries work nice with JavaScript, but they are problematic with TypeScript. 19 | When using [redux-saga](https://github.com/redux-saga/redux-saga) you can't infer types from `yield` statements, and you must add extra annotations to your code. It can cause potential bugs if you provide the wrong type. 20 | The `connect` function must match exactly `props` of your connected component. Any small mistake causes a big compiler error if there are many properties. Fixing such mistakes is tedious and frustrating. 21 | 22 | - **Too many libraries** 23 | "What libraries should I use? redux-observables, redux-saga, redux-thunk, redux-xxx?" 24 | Over-analyzing can cause [Analysis paralysis](https://en.wikipedia.org/wiki/Analysis_paralysis). 25 | 26 | - **Lack of guidelines** 27 | There are no official guidelines on how to lazy load reducers, sagas or epics. Code splitting is critical when scaling bigger apps. 28 | Custom solutions for lazy loading usually don't work with HMR causing poor developer experience. 29 | 30 | ## Base Concepts 31 | 32 | - **Designed for TypeScript** 33 | All APIs are designed for TypeScript and type-safety: 34 | 35 | - TypeScript will boost your productivity, not slows you down. 36 | - Only the necessary annotations are required: state, action arguments. 37 | - No typecasting. Everything is inferred automatically. 95% of the code looks like pure JavaScript. 38 | - No RootAction, RootEpic, RootState or other helper types. 39 | 40 | - **Provide all building blocks** 41 | Typeless includes everything to build mid-sized or enterprise level apps. 42 | You don't need to rely on multiple small libraries. 43 | 44 | - **Modularity** 45 | Proper modularity is critical for building scalable apps. 46 | There is no need to create root files for epics, reducers, types, etc. Once you create a new module, you can attach it from any place. Similar to standard React components. 47 | 48 | - **Opinionated** 49 | All common use cases and problems are solved by default. No need to over-think how to fix trivial issues. 50 | All recommendations and best practices are provided! 51 | -------------------------------------------------------------------------------- /docs/introduction/roadmap.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: roadmap 3 | title: Roadmap 4 | hide_title: true 5 | sidebar_label: Roadmap 6 | --- 7 | 8 | # Roadmap 9 | 10 | - **Server-side rendering** 11 | It cannot be implemented at this moment. `React.Lazy` doesn't support SSR. 12 | - **Testing example** 13 | Work in progress. Everything is testable, but need to create explicit examples. 14 | -------------------------------------------------------------------------------- /docs/introduction/starter-kits.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: starter-kits 3 | title: Starter Kits 4 | hide_title: true 5 | sidebar_label: Starters Kits 6 | --- 7 | 8 | # Starter Kits 9 | 10 | ### Create React App 11 | [Repository](https://github.com/typeless-js/create-react-app-starter) 12 | Starter kit based on Create React App. 13 | Features: 14 | - example user module 15 | - routing for auth/anonymous users 16 | - forms 17 | - lazy modules 18 | - blueprints 19 | -------------------------------------------------------------------------------- /docs/using-typeless/actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: actions 3 | title: Actions 4 | hide_title: true 5 | sidebar_label: Actions 6 | --- 7 | 8 | # Actions -------------------------------------------------------------------------------- /docs/using-typeless/code-splitting.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: code-splitting 3 | title: Code-Splitting 4 | hide_title: true 5 | sidebar_label: Code-Splitting 6 | --- 7 | 8 | # Code-Splitting 9 | Reducers and epics are part of your React components. You can use [React.lazy()](https://reactjs.org/docs/code-splitting.html) to implement code-splitting. 10 | Check [Code-Splitting](/introduction/examples#code-splitting) for full working example. 11 | 12 | #### Example 13 | 14 | 15 | ```tsx 16 | // src/features/foo/module.tsx 17 | 18 | import { useModule } from './interface' 19 | 20 | export default function FooModule() { 21 | useModule(); 22 | 23 | return ; 24 | } 25 | 26 | // components/MyComponent.tsx 27 | const Foo = React.lazy(() => import('src/features/foo/module')); 28 | 29 | export function MyComponent() { 30 | const { showFoo } = useMappedState(state => state.showFoo); 31 | 32 | if (showFoo) { 33 | return ; 34 | } 35 | return
Foo hidden
; 36 | } 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /docs/using-typeless/hmr.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: hmr 3 | title: HMR 4 | hide_title: true 5 | sidebar_label: HMR 6 | --- 7 | 8 | # HMR 9 | To enable Hot Module Replacement, you must wrap your root app with `` and invoke `startHmr()` in `module.hot.accept`. 10 | There is no need for methods like `replaceReducer` or `replaceEpic`. Everything is updated automatically! 11 | Check [Basic HMR](/introduction/examples#basic-hmr) for full working example. 12 | 13 | #### Example 14 | ```tsx 15 | // src/index.tsx 16 | import React from 'react'; 17 | import ReactDOM from 'react-dom'; 18 | import { Hmr, startHmr, DefaultTypelessProvider } from 'typeless'; 19 | 20 | const MOUNT_NODE = document.getElementById('app'); 21 | 22 | const render = () => { 23 | const App = require('./components/App').App; 24 | ReactDOM.unmountComponentAtNode(MOUNT_NODE); 25 | ReactDOM.render( 26 | // 👈 27 | 28 | 29 | 30 | , 31 | MOUNT_NODE 32 | ); 33 | }; 34 | 35 | if (module.hot) { 36 | module.hot.accept('./components/App', () => { 37 | // 👇👇👇 38 | startHmr(); 39 | render() 40 | }); 41 | } 42 | render(); 43 | ``` -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Getting started 4 | 5 | run following command 6 | 7 | ```console 8 | yarn install 9 | yarn start 10 | ``` 11 | 12 | and access to following URL 13 | 14 | - http://localhost:1234/basic-form/index.html 15 | - http://localhost:1234/basic-hmr/index.html 16 | - http://localhost:1234/basic-routing/index.html 17 | - http://localhost:1234/code-splitting/index.html 18 | - http://localhost:1234/counter/index.html 19 | - http://localhost:1234/real-api/index.html 20 | - http://localhost:1234/socket-hmr/index.html 21 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "prestart": "rm -rf .cache dist && tsc", 7 | "start": "parcel ./src/*/index.html" 8 | }, 9 | "devDependencies": { 10 | "@types/react": "^16.9.17", 11 | "@types/react-dom": "^16.9.4", 12 | "eventemitter3": "^4.0.0", 13 | "parcel-bundler": "^1.12.4", 14 | "react": "^16.12.0", 15 | "react-dom": "^16.12.0", 16 | "rxjs": "^6.5.3", 17 | "typeless": "^1.3.0", 18 | "typeless-form": "^1.3.0", 19 | "typeless-router": "^1.3.0", 20 | "typescript": "^3.7.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/src/basic-form/components/FormInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormContext } from 'typeless-form'; 3 | 4 | interface FormInputProps { 5 | name: string; 6 | } 7 | 8 | export const FormInput = (props: FormInputProps) => { 9 | const { name, ...rest } = props; 10 | const data = React.useContext(FormContext); 11 | if (!data) { 12 | throw new Error(`${name} cannot be used without FormContext`); 13 | } 14 | const hasError = data.touched[name] && !!data.errors[name]; 15 | const value = data.values[name]; 16 | return ( 17 |
18 | data.actions.blur(name)} 21 | onChange={e => { 22 | data.actions.change(name, e.target.value); 23 | }} 24 | {...rest} 25 | /> 26 | {hasError &&
{data.errors[name]}
} 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /examples/src/basic-form/features/example/form.ts: -------------------------------------------------------------------------------- 1 | import { createForm } from 'typeless-form'; 2 | import { ExampleFormSymbol } from './symbol'; 3 | 4 | interface ExampleForm { 5 | foo: string; 6 | bar: string; 7 | } 8 | 9 | export const [ 10 | useExampleForm, 11 | ExampleFormActions, 12 | getExampleFormState, 13 | ExampleFormProvider, 14 | ] = createForm({ 15 | symbol: ExampleFormSymbol, 16 | validator(errors, data) { 17 | if (!data.foo) { 18 | errors.foo = 'This field is required'; 19 | } 20 | if (!data.bar) { 21 | errors.bar = 'This field is required'; 22 | } else if (data.bar.length < 3) { 23 | errors.bar = 'Minimum 3 characters'; 24 | } 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /examples/src/basic-form/features/example/interface.ts: -------------------------------------------------------------------------------- 1 | import { createModule } from 'typeless'; 2 | import { ExampleSymbol } from './symbol'; 3 | 4 | export const [handle] = createModule(ExampleSymbol); 5 | -------------------------------------------------------------------------------- /examples/src/basic-form/features/example/module.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useActions } from 'typeless'; 3 | import * as Rx from 'typeless/rx'; 4 | import { handle } from './interface'; 5 | import { 6 | useExampleForm, 7 | ExampleFormProvider, 8 | ExampleFormActions, 9 | getExampleFormState, 10 | } from './form'; 11 | import { FormInput } from '../../components/FormInput'; 12 | 13 | handle.epic().on(ExampleFormActions.setSubmitSucceeded, () => { 14 | alert(JSON.stringify(getExampleFormState().values, null, 2)); 15 | return Rx.empty(); 16 | }); 17 | 18 | export function ExampleModule() { 19 | handle(); 20 | useExampleForm(); 21 | 22 | const { submit } = useActions(ExampleFormActions); 23 | 24 | return ( 25 | 26 |
{ 28 | e.preventDefault(); 29 | submit(); 30 | }} 31 | > 32 | 33 | 34 | 35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /examples/src/basic-form/features/example/symbol.ts: -------------------------------------------------------------------------------- 1 | export const ExampleSymbol = Symbol('example'); 2 | export const ExampleFormSymbol = Symbol('example-form'); 3 | -------------------------------------------------------------------------------- /examples/src/basic-form/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/src/basic-form/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { DefaultTypelessProvider } from 'typeless'; 4 | import { ExampleModule } from './features/example/module'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('app') 11 | ); 12 | -------------------------------------------------------------------------------- /examples/src/basic-hmr/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CounterModule from '../features/counter/module'; 3 | 4 | export function App() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /examples/src/basic-hmr/features/counter/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useActions } from 'typeless'; 3 | import { CounterActions, getCounterState } from '../interface'; 4 | 5 | export function Counter() { 6 | const { increase } = useActions(CounterActions); 7 | const { count } = getCounterState.useState(); 8 | return ( 9 |
10 | 11 |
count: {count}
12 | 13 | Edit this text in your IDE. The counter value should remain the same. 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/src/basic-hmr/features/counter/interface.ts: -------------------------------------------------------------------------------- 1 | import { createModule } from 'typeless'; 2 | import { CounterSymbol } from './symbol'; 3 | 4 | export const [useModule, CounterActions, getCounterState] = createModule( 5 | CounterSymbol 6 | ) 7 | .withActions({ 8 | increase: null, 9 | }) 10 | .withState(); 11 | 12 | export interface CounterState { 13 | count: number; 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/basic-hmr/features/counter/module.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CounterActions, CounterState, useModule } from './interface'; 3 | import { Counter } from './components/Counter'; 4 | 5 | const initialState: CounterState = { 6 | count: 0, 7 | }; 8 | 9 | useModule.reducer(initialState).on(CounterActions.increase, state => { 10 | state.count++; 11 | }); 12 | 13 | export default function CounterModule() { 14 | useModule(); 15 | 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /examples/src/basic-hmr/features/counter/symbol.ts: -------------------------------------------------------------------------------- 1 | export const CounterSymbol = Symbol('counter'); 2 | -------------------------------------------------------------------------------- /examples/src/basic-hmr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/src/basic-hmr/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Hmr, startHmr, DefaultTypelessProvider } from 'typeless'; 4 | 5 | const MOUNT_NODE = document.getElementById('app'); 6 | 7 | const render = () => { 8 | const App = require('./components/App').App; 9 | ReactDOM.unmountComponentAtNode(MOUNT_NODE); 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | MOUNT_NODE 17 | ); 18 | }; 19 | 20 | if (module.hot) { 21 | module.hot.accept(() => { 22 | startHmr(); 23 | render(); 24 | }); 25 | } 26 | render(); 27 | -------------------------------------------------------------------------------- /examples/src/basic-routing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/src/basic-routing/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { DefaultTypelessProvider } from 'typeless'; 4 | import { Link, getRouterState } from 'typeless-router'; 5 | import { useRouter } from './router'; 6 | 7 | function App() { 8 | useRouter(); 9 | const { location } = getRouterState.useState(); 10 | 11 | return ( 12 |
13 | page a | page b 14 |
15 | Current location: {location.pathname} 16 |
17 | ); 18 | } 19 | 20 | ReactDOM.render( 21 | 22 | 23 | , 24 | document.getElementById('app') 25 | ); 26 | -------------------------------------------------------------------------------- /examples/src/basic-routing/router.ts: -------------------------------------------------------------------------------- 1 | import { createUseRouter } from 'typeless-router'; 2 | 3 | export const useRouter = createUseRouter(); 4 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/main/components/MainView.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { useActions } from 'typeless'; 3 | import { MainActions, ViewType, getMainState } from '../interface'; 4 | 5 | const SubA = React.lazy(() => 6 | import(/* webpackChunkName: "subA" */ '../../subA/module') 7 | ); 8 | const SubB = React.lazy(() => 9 | import(/* webpackChunkName: "subB" */ '../../subB/module') 10 | ); 11 | const SubC = React.lazy(() => 12 | import(/* webpackChunkName: "subC" */ '../../subC/module') 13 | ); 14 | 15 | export function MainView() { 16 | const { show } = useActions(MainActions); 17 | const { viewType } = getMainState.useState(); 18 | 19 | const renderContent = () => { 20 | switch (viewType) { 21 | case 'subA': { 22 | return ; 23 | } 24 | case 'subB': { 25 | return ; 26 | } 27 | case 'subC': { 28 | return ; 29 | } 30 | } 31 | }; 32 | 33 | return ( 34 |
45 | Show View:{' '} 46 | 58 |
59 | Loading...
}>{renderContent()} 60 |
61 |
62 | Open Dev Tools, and change Network speed to "Slow 3G". 63 |
64 | Choose an option from the above select. 65 |
66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/main/interface.ts: -------------------------------------------------------------------------------- 1 | import { createModule } from 'typeless'; 2 | import { MainSymbol } from './symbol'; 3 | 4 | export const [useModule, MainActions, getMainState] = createModule(MainSymbol) 5 | .withActions({ 6 | show: (viewType: ViewType) => ({ payload: { viewType } }), 7 | }) 8 | .withState(); 9 | 10 | export type ViewType = 'subA' | 'subB' | 'subC'; 11 | 12 | export interface MainState { 13 | viewType: ViewType | null; 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/main/module.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useModule, MainState, MainActions } from './interface'; 3 | import { MainView } from './components/MainView'; 4 | 5 | const initialState: MainState = { 6 | viewType: null, 7 | }; 8 | 9 | useModule 10 | .reducer(initialState) 11 | // 12 | .on(MainActions.show, (state, { viewType }) => { 13 | state.viewType = viewType; 14 | }); 15 | 16 | export default function CatModule() { 17 | useModule(); 18 | 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/main/symbol.ts: -------------------------------------------------------------------------------- 1 | export const MainSymbol = Symbol('main'); 2 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subA/components/SubAView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useActions } from 'typeless'; 3 | import { SubAActions, getSubAState } from '../interface'; 4 | 5 | export function SubAView() { 6 | const { increase } = useActions(SubAActions); 7 | const { counter } = getSubAState.useState(); 8 | 9 | return ( 10 |
11 | Module A.
12 | Counter {counter}
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subA/interface.ts: -------------------------------------------------------------------------------- 1 | import { createModule } from 'typeless'; 2 | import { SubASymbol } from './symbol'; 3 | 4 | export const [useModule, SubAActions, getSubAState] = createModule(SubASymbol) 5 | .withActions({ 6 | increase: null, 7 | }) 8 | .withState(); 9 | 10 | export interface SubAState { 11 | counter: number; 12 | } 13 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subA/module.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SubAActions, SubAState, useModule } from './interface'; 3 | import { SubAView } from './components/SubAView'; 4 | 5 | const initialState: SubAState = { 6 | counter: 0, 7 | }; 8 | 9 | useModule 10 | .reducer(initialState) 11 | // 12 | .on(SubAActions.increase, state => { 13 | state.counter++; 14 | }); 15 | 16 | export default function SubAModule() { 17 | useModule(); 18 | 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subA/symbol.ts: -------------------------------------------------------------------------------- 1 | export const SubASymbol = Symbol('subA'); 2 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subB/components/SubBView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useActions } from 'typeless'; 3 | import { SubBActions, getSubBState } from '../interface'; 4 | 5 | export function SubBView() { 6 | const { decrease } = useActions(SubBActions); 7 | const { counter } = getSubBState.useState(); 8 | 9 | return ( 10 |
11 | Module B.
12 | Counter {counter}
13 |
14 | Counter will reset if you unmount this module. 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subB/interface.ts: -------------------------------------------------------------------------------- 1 | import { createModule } from 'typeless'; 2 | import { SubBSymbol } from './symbol'; 3 | 4 | export const [useModule, SubBActions, getSubBState] = createModule(SubBSymbol) 5 | .withActions({ 6 | decrease: null, 7 | $unmounting: null, 8 | }) 9 | .withState(); 10 | 11 | export interface SubBState { 12 | counter: number; 13 | } 14 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subB/module.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SubBActions, SubBState, useModule } from './interface'; 3 | import { SubBView } from './components/SubBView'; 4 | 5 | const initialState: SubBState = { 6 | counter: 0, 7 | }; 8 | 9 | useModule 10 | .reducer(initialState) 11 | .on(SubBActions.decrease, state => { 12 | state.counter--; 13 | }) 14 | .on(SubBActions.$unmounting, state => { 15 | state.counter = 0; 16 | }); 17 | 18 | export default function SubBModule() { 19 | useModule(); 20 | 21 | return ; 22 | } 23 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subB/symbol.ts: -------------------------------------------------------------------------------- 1 | export const SubBSymbol = Symbol('subB'); 2 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subC/components/SubCView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useActions } from 'typeless'; 3 | import { SubCActions, getSubCState } from '../interface'; 4 | 5 | export function SubCView() { 6 | const { double } = useActions(SubCActions); 7 | const { counter } = getSubCState.useState(); 8 | 9 | return ( 10 |
11 | Module C.
12 | Counter {counter}
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subC/interface.ts: -------------------------------------------------------------------------------- 1 | import { createModule } from 'typeless'; 2 | import { SubCSymbol } from './symbol'; 3 | 4 | export const [useModule, SubCActions, getSubCState] = createModule(SubCSymbol) 5 | .withActions({ 6 | double: null, 7 | }) 8 | .withState(); 9 | 10 | export interface SubCState { 11 | counter: number; 12 | } 13 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subC/module.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SubCActions, SubCState, useModule } from './interface'; 3 | import { SubCView } from './components/SubCView'; 4 | 5 | const initialState: SubCState = { 6 | counter: 1, 7 | }; 8 | 9 | useModule.reducer(initialState).on(SubCActions.double, state => { 10 | state.counter *= 2; 11 | }); 12 | 13 | export default function CatModule() { 14 | useModule(); 15 | 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /examples/src/code-splitting/features/subC/symbol.ts: -------------------------------------------------------------------------------- 1 | export const SubCSymbol = Symbol('subC'); 2 | -------------------------------------------------------------------------------- /examples/src/code-splitting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/src/code-splitting/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { DefaultTypelessProvider } from 'typeless'; 4 | import MainModule from './features/main/module'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('app') 11 | ); 12 | -------------------------------------------------------------------------------- /examples/src/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/src/counter/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import * as Rx from 'typeless/rx'; 4 | import { createModule, useActions, DefaultTypelessProvider } from 'typeless'; 5 | 6 | /* == Module Interface == */ 7 | 8 | export const [useModule, CounterActions, getCounterState] = createModule( 9 | Symbol('counter') 10 | ) 11 | // Create Actions Creators 12 | .withActions({ 13 | startCount: null, // null means no args 14 | countDone: (count: number) => ({ payload: { count } }), 15 | }) 16 | .withState(); 17 | 18 | export interface CounterState { 19 | isLoading: boolean; 20 | count: number; 21 | } 22 | 23 | /* == Module Implementation == */ 24 | 25 | const initialState: CounterState = { 26 | isLoading: false, 27 | count: 0, 28 | }; 29 | 30 | // Create Epic for side effects 31 | useModule 32 | .epic() 33 | // Listen for `count` and dispatch `countDone` with 500ms delay 34 | .on(CounterActions.startCount, () => 35 | Rx.of(CounterActions.countDone(1)).pipe(Rx.delay(500)) 36 | ); 37 | 38 | // Create a reducer 39 | // Under the hood it uses `immer` and state mutations are allowed 40 | useModule 41 | .reducer(initialState) 42 | .on(CounterActions.startCount, state => { 43 | state.isLoading = true; 44 | }) 45 | .on(CounterActions.countDone, (state, { count }) => { 46 | state.isLoading = false; 47 | state.count += count; 48 | }); 49 | 50 | /* == Use Module in React == */ 51 | 52 | export function Counter() { 53 | // load epic and reducer 54 | useModule(); 55 | 56 | // wrap actions with `dispatch` 57 | const { startCount } = useActions(CounterActions); 58 | // get state from store 59 | const { isLoading, count } = getCounterState.useState(); 60 | 61 | return ( 62 |
63 | 66 |
count: {count}
67 |
68 | ); 69 | } 70 | 71 | ReactDOM.render( 72 | 73 | 74 | , 75 | document.getElementById('app') 76 | ); 77 | -------------------------------------------------------------------------------- /examples/src/libraries.d.ts: -------------------------------------------------------------------------------- 1 | declare var require: (path: string) => T; 2 | 3 | // for parcel's hot module replacement method 4 | declare var module: { hot?: { accept(cb: () => void) } }; 5 | -------------------------------------------------------------------------------- /examples/src/real-api/features/cat/components/CatView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useActions } from 'typeless'; 3 | import { CatActions, getCatState } from '../interface'; 4 | 5 | export function CatView() { 6 | const { loadCat, cancel } = useActions(CatActions); 7 | const { viewType, cat, error } = getCatState.useState(); 8 | 9 | const boxStyle: React.CSSProperties = { 10 | height: 200, 11 | display: 'flex', 12 | alignItems: 'center', 13 | justifyContent: 'center', 14 | }; 15 | 16 | const renderContent = () => { 17 | switch (viewType) { 18 | case 'details': { 19 | return ( 20 | <> 21 | {cat ? ( 22 | 23 | ) : ( 24 |
No cat loaded yet
25 | )} 26 | 29 | 30 | ); 31 | } 32 | case 'loading': { 33 | return ( 34 | <> 35 |
36 | 37 | 38 | ); 39 | } 40 | case 'error': { 41 | return ( 42 | <> 43 |
44 | 😿
45 | An error occurred: {error} 46 |
47 | 48 | 49 | ); 50 | } 51 | } 52 | }; 53 | 54 | return ( 55 |
66 | {renderContent()} 67 |
68 | Click on load multiple times. There is 50% chance for errors. 69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /examples/src/real-api/features/cat/interface.ts: -------------------------------------------------------------------------------- 1 | import { createModule } from 'typeless'; 2 | import { CatSymbol } from './symbol'; 3 | 4 | export const [useModule, CatActions, getCatState] = createModule(CatSymbol) 5 | .withActions({ 6 | loadCat: null, 7 | cancel: null, 8 | catLoaded: (cat: Cat) => ({ payload: { cat } }), 9 | errorOcurred: (error: string) => ({ payload: { error } }), 10 | }) 11 | .withState(); 12 | 13 | export interface CounterState { 14 | isLoading: boolean; 15 | count: number; 16 | } 17 | 18 | type ViewType = 'loading' | 'details' | 'error'; 19 | 20 | interface Cat { 21 | imageUrl: string; 22 | } 23 | 24 | export interface CatState { 25 | viewType: ViewType; 26 | cat: Cat | null; 27 | error: string; 28 | } 29 | -------------------------------------------------------------------------------- /examples/src/real-api/features/cat/module.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as Rx from 'typeless/rx'; 3 | import { useModule, CatState, CatActions } from './interface'; 4 | import { CatView } from './components/CatView'; 5 | 6 | function fetchCatData() { 7 | return Rx.of({ 8 | imageUrl: `https://cataas.com/cat/gif?_t=${Date.now()}`, 9 | }).pipe( 10 | Rx.delay(2000), 11 | Rx.map(cat => { 12 | if (Date.now() % 2 === 0) { 13 | throw new Error('Failed to load cat'); 14 | } 15 | return cat; 16 | }) 17 | ); 18 | } 19 | 20 | useModule.epic().on(CatActions.loadCat, (_, { action$ }) => 21 | fetchCatData().pipe( 22 | Rx.map(cat => CatActions.catLoaded(cat)), 23 | Rx.catchError(err => { 24 | console.error(err); 25 | return Rx.of(CatActions.errorOcurred(err.message)); 26 | }), 27 | Rx.takeUntil(action$.pipe(Rx.waitForType(CatActions.cancel))) 28 | ) 29 | ); 30 | 31 | const initialState: CatState = { 32 | viewType: 'details', 33 | cat: null, 34 | error: '', 35 | }; 36 | 37 | useModule 38 | .reducer(initialState) 39 | .on(CatActions.loadCat, state => { 40 | state.viewType = 'loading'; 41 | }) 42 | .on(CatActions.errorOcurred, (state, { error }) => { 43 | state.cat = null; 44 | state.viewType = 'error'; 45 | state.error = error; 46 | }) 47 | .on(CatActions.catLoaded, (state, { cat }) => { 48 | state.viewType = 'details'; 49 | state.cat = cat; 50 | }) 51 | .on(CatActions.cancel, state => { 52 | state.viewType = 'details'; 53 | }); 54 | 55 | export default function CatModule() { 56 | useModule(); 57 | 58 | return ; 59 | } 60 | -------------------------------------------------------------------------------- /examples/src/real-api/features/cat/symbol.ts: -------------------------------------------------------------------------------- 1 | export const CatSymbol = Symbol('cat'); 2 | -------------------------------------------------------------------------------- /examples/src/real-api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/src/real-api/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { DefaultTypelessProvider } from 'typeless'; 4 | import CatModule from './features/cat/module'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('app') 11 | ); 12 | -------------------------------------------------------------------------------- /examples/src/socket-hmr/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SocketModule from '../features/socket/module'; 3 | 4 | export function App() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /examples/src/socket-hmr/features/socket/components/SocketView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useActions } from 'typeless'; 3 | import { SocketActions, getSocketState } from '../interface'; 4 | 5 | export function SocketView() { 6 | const { stopC, startC } = useActions(SocketActions); 7 | const { a, b, c, isCRunning } = getSocketState.useState(); 8 | const textAreaStyles: React.CSSProperties = { width: '100%', height: 100 }; 9 | return ( 10 |
11 |

Module A

12 |

HMR reloads ignored

13 |