) => {
11 | const { filter, children } = props;
12 | const [{ visibilityFilter }, { setVisibilityFilter }] = useModel(todos);
13 |
14 | // won't re-render when only the `data` slice of state change,
15 | // because the state selector lets `FilterLink` only subscribe the `visibilityFilter` slice of the state
16 | // const [visibilityFilter, {setVisibilityFilter}] = useModel(todos, (state) => state.visibilityFilter)
17 |
18 | return (
19 | setVisibilityFilter(filter)}
22 | >
23 | {children}
24 |
25 | );
26 | };
27 |
28 | export default FilterLink;
29 |
--------------------------------------------------------------------------------
/packages/react/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 现代 Web 工程体系
7 |
8 |
9 | modernjs.dev
10 |
11 |
12 |
13 | The meta-framework suite designed from scratch for frontend-focused modern web development
14 |
15 |
16 | # Introduction
17 |
18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon.
19 |
20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646)
21 |
22 | ## Getting Started
23 |
24 | - [Quick Start](https://modernjs.dev/docs/start)
25 | - [Guides](https://modernjs.dev/docs/guides)
26 | - [API References](https://modernjs.dev/docs/api)
27 |
28 | ## Contributing
29 |
30 | - [Contributing Guide](/CONTRIBUTING.md)
31 |
--------------------------------------------------------------------------------
/packages/store/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 现代 Web 工程体系
7 |
8 |
9 | modernjs.dev
10 |
11 |
12 |
13 | The meta-framework suite designed from scratch for frontend-focused modern web development
14 |
15 |
16 | # Introduction
17 |
18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon.
19 |
20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646)
21 |
22 | ## Getting Started
23 |
24 | - [Quick Start](https://modernjs.dev/docs/start)
25 | - [Guides](https://modernjs.dev/docs/guides)
26 | - [API References](https://modernjs.dev/docs/api)
27 |
28 | ## Contributing
29 |
30 | - [Contributing Guide](/CONTRIBUTING.md)
31 |
--------------------------------------------------------------------------------
/packages/plugins/immer/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 现代 Web 工程体系
7 |
8 |
9 | modernjs.dev
10 |
11 |
12 |
13 | The meta-framework suite designed from scratch for frontend-focused modern web development
14 |
15 |
16 | # Introduction
17 |
18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon.
19 |
20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646)
21 |
22 | ## Getting Started
23 |
24 | - [Quick Start](https://modernjs.dev/docs/start)
25 | - [Guides](https://modernjs.dev/docs/guides)
26 | - [API References](https://modernjs.dev/docs/api)
27 |
28 | ## Contributing
29 |
30 | - [Contributing Guide](/CONTRIBUTING.md)
31 |
--------------------------------------------------------------------------------
/packages/plugins/xstate/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 现代 Web 工程体系
7 |
8 |
9 | modernjs.dev
10 |
11 |
12 |
13 | The meta-framework suite designed from scratch for frontend-focused modern web development
14 |
15 |
16 | # Introduction
17 |
18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon.
19 |
20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646)
21 |
22 | ## Getting Started
23 |
24 | - [Quick Start](https://modernjs.dev/docs/start)
25 | - [Guides](https://modernjs.dev/docs/guides)
26 | - [API References](https://modernjs.dev/docs/api)
27 |
28 | ## Contributing
29 |
30 | - [Contributing Guide](/CONTRIBUTING.md)
31 |
--------------------------------------------------------------------------------
/packages/plugins/devtools/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 现代 Web 工程体系
7 |
8 |
9 | modernjs.dev
10 |
11 |
12 |
13 | The meta-framework suite designed from scratch for frontend-focused modern web development
14 |
15 |
16 | # Introduction
17 |
18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon.
19 |
20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646)
21 |
22 | ## Getting Started
23 |
24 | - [Quick Start](https://modernjs.dev/docs/start)
25 | - [Guides](https://modernjs.dev/docs/guides)
26 | - [API References](https://modernjs.dev/docs/api)
27 |
28 | ## Contributing
29 |
30 | - [Contributing Guide](/CONTRIBUTING.md)
31 |
--------------------------------------------------------------------------------
/packages/plugins/effects/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 现代 Web 工程体系
7 |
8 |
9 | modernjs.dev
10 |
11 |
12 |
13 | The meta-framework suite designed from scratch for frontend-focused modern web development
14 |
15 |
16 | # Introduction
17 |
18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon.
19 |
20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646)
21 |
22 | ## Getting Started
23 |
24 | - [Quick Start](https://modernjs.dev/docs/start)
25 | - [Guides](https://modernjs.dev/docs/guides)
26 | - [API References](https://modernjs.dev/docs/api)
27 |
28 | ## Contributing
29 |
30 | - [Contributing Guide](/CONTRIBUTING.md)
31 |
--------------------------------------------------------------------------------
/packages/plugins/auto-actions/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 现代 Web 工程体系
7 |
8 |
9 | modernjs.dev
10 |
11 |
12 |
13 | The meta-framework suite designed from scratch for frontend-focused modern web development
14 |
15 |
16 | # Introduction
17 |
18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon.
19 |
20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646)
21 |
22 | ## Getting Started
23 |
24 | - [Quick Start](https://modernjs.dev/docs/start)
25 | - [Guides](https://modernjs.dev/docs/guides)
26 | - [API References](https://modernjs.dev/docs/api)
27 |
28 | ## Contributing
29 |
30 | - [Contributing Guide](/CONTRIBUTING.md)
31 |
--------------------------------------------------------------------------------
/packages/plugins/xstate-immer/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 现代 Web 工程体系
7 |
8 |
9 | modernjs.dev
10 |
11 |
12 |
13 | The meta-framework suite designed from scratch for frontend-focused modern web development
14 |
15 |
16 | # Introduction
17 |
18 | > The doc site ([modernjs.dev](https://modernjs.dev)) and articles are only available in Chinese for now, we are planning to add English versions soon.
19 |
20 | - [Modern.js: Hello, World!](https://zhuanlan.zhihu.com/p/426707646)
21 |
22 | ## Getting Started
23 |
24 | - [Quick Start](https://modernjs.dev/docs/start)
25 | - [Guides](https://modernjs.dev/docs/guides)
26 | - [API References](https://modernjs.dev/docs/api)
27 |
28 | ## Contributing
29 |
30 | - [Contributing Guide](/CONTRIBUTING.md)
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Modern.js
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/store/src/__tsd__/computed.tsd.ts:
--------------------------------------------------------------------------------
1 | import { expectType } from 'tsd';
2 | import { createStore, model } from '..';
3 |
4 | type StateA = {
5 | a: number;
6 | };
7 | const modelA = model('modelA').define({
8 | state: {
9 | a: 1,
10 | },
11 | computed: {
12 | double(s) {
13 | return s.a + 1;
14 | },
15 | },
16 | });
17 |
18 | type StateB = {
19 | b: string;
20 | };
21 |
22 | const modelB = model('modelB').define({
23 | state: {
24 | b: '10',
25 | },
26 | computed: {
27 | str: [
28 | modelA,
29 | (s, other: StateA) => {
30 | return s.b.repeat(other.a);
31 | },
32 | ],
33 | },
34 | });
35 |
36 | describe('test computed', () => {
37 | const store = createStore();
38 |
39 | test('basic usage', () => {
40 | const [state] = store.use(modelA);
41 | expectType(state);
42 | });
43 |
44 | test('depend on other models', () => {
45 | const use = () => store.use(modelA, modelB);
46 | const [state] = use();
47 | expectType(state);
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/packages/react/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Modern.js
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/store/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Modern.js
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/plugins/immer/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Modern.js
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/plugins/xstate/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Modern.js
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/plugins/devtools/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Modern.js
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/plugins/effects/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Modern.js
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/plugins/auto-actions/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Modern.js
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/plugins/xstate-immer/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Modern.js
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/todos/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todos",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@modern-js-reduck/react": "latest",
7 | "@testing-library/jest-dom": "^5.16.4",
8 | "@testing-library/react": "^13.3.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "@types/jest": "^27.5.1",
11 | "@types/node": "^14",
12 | "@types/react": "^18",
13 | "@types/react-dom": "^18",
14 | "react": "^18.0.2",
15 | "react-dom": "^18.0.2",
16 | "react-scripts": "5.0.1",
17 | "typescript": "^4.7.4",
18 | "web-vitals": "^2.1.4"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the main branch
8 | push:
9 | branches: [ main ]
10 | pull_request:
11 | branches: [ main ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | # This workflow contains a single job called "build"
19 | test:
20 | # The type of runner that the job will run on
21 | runs-on: ubuntu-latest
22 |
23 | # Steps represent a sequence of tasks that will be executed as part of the job
24 | steps:
25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
26 | - uses: actions/checkout@v2
27 |
28 | # Runs a single command using the runners shell
29 | - name: install pnpm
30 | run: npm i -g pnpm@7
31 |
32 | - name: build
33 | run: pnpm i --no-frozen-lockfile
34 |
35 | - name: test
36 | run: pnpm -r --filter "./packages/**" test && pnpm -r --filter "./packages/**" test-type
37 |
--------------------------------------------------------------------------------
/packages/react/modern.config.js:
--------------------------------------------------------------------------------
1 | const {
2 | default: moduleTools,
3 | defineConfig,
4 | } = require('@modern-js/module-tools');
5 | const test = require('@modern-js/plugin-testing').default;
6 | const config = require('../../common/config');
7 |
8 | const react17Config = {
9 | displayName: 'ReactDOM 17',
10 | moduleNameMapper: {
11 | '^react$': 'react-17',
12 | '^react-dom$': 'react-dom-17',
13 | '^@testing-library/react$': '@testing-library/react-12',
14 | },
15 | };
16 |
17 | const react18Config = {
18 | displayName: 'ReactDOM 18',
19 | };
20 |
21 | module.exports = defineConfig({
22 | ...config,
23 | plugins: [moduleTools(), test()],
24 | testing: {
25 | jest: options => {
26 | const { moduleNameMapper } = options;
27 | delete options.moduleNameMapper;
28 | return {
29 | ...options,
30 | collectCoverage: true,
31 | projects: [
32 | {
33 | ...react17Config,
34 | moduleNameMapper: {
35 | ...moduleNameMapper,
36 | ...react17Config.moduleNameMapper,
37 | },
38 | },
39 | {
40 | ...react18Config,
41 | moduleNameMapper,
42 | },
43 | ],
44 | };
45 | },
46 | },
47 | });
48 |
--------------------------------------------------------------------------------
/packages/store/src/plugin/core.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, PluginContext, PluginLifeCycle } from '@/types/plugin';
2 |
3 | type Stage = keyof PluginLifeCycle;
4 |
5 | export const createPluginCore = (pluginContext: PluginContext) => {
6 | const lifeCycleList: PluginLifeCycle[] = [];
7 | const findHandlers = (stage: S) =>
8 | lifeCycleList.map(liftCycle => liftCycle[stage]).filter(Boolean);
9 |
10 | return {
11 | usePlugin: (plugin: Plugin) => {
12 | lifeCycleList.push(plugin(pluginContext));
13 | },
14 | invokePipeline: (
15 | stage: S,
16 | bypassParams: Parameters[0],
17 | ...args: Parameters extends [any, ...infer T] ? T : []
18 | ) => {
19 | const handlers = findHandlers(stage);
20 |
21 | let params = bypassParams;
22 |
23 | for (const handler of handlers) {
24 | params = (handler as any)(params, ...args);
25 | }
26 |
27 | return params;
28 | },
29 | invokeWaterFall: (
30 | stage: S,
31 | ...args: Parameters
32 | ) => {
33 | const handlers = findHandlers(stage);
34 |
35 | return handlers.forEach(handler => (handler as any)(...args));
36 | },
37 | };
38 | };
39 |
--------------------------------------------------------------------------------
/examples/with-middleware/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-middleware",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@modern-js-reduck/react": "latest",
7 | "react": "^18.0.2",
8 | "react-dom": "^18.0.2",
9 | "react-scripts": "5.0.1",
10 | "redux-logger": "^3.0.6",
11 | "typescript": "^4.7.4",
12 | "web-vitals": "^2.1.4"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | },
38 | "devDependencies": {
39 | "@testing-library/jest-dom": "^5.16.4",
40 | "@testing-library/react": "^13.3.0",
41 | "@testing-library/user-event": "^13.5.0",
42 | "@types/jest": "^27.5.1",
43 | "@types/node": "^14",
44 | "@types/react": "^18",
45 | "@types/react-dom": "^18",
46 | "@types/redux-logger": "^3.0.9"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/react/src/utils/useIsomorphicLayoutEffect.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * copied from https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.ts
3 | * license at https://github.com/reduxjs/react-redux/blob/master/LICENSE.md
4 | */
5 |
6 | import { useEffect, useLayoutEffect } from 'react';
7 |
8 | // React currently throws a warning when using useLayoutEffect on the server.
9 | // To get around it, we can conditionally useEffect on the server (no-op) and
10 | // useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
11 | // subscription callback always has the selector from the latest render commit
12 | // available, otherwise a store update may happen between render and the effect,
13 | // which may cause missed updates; we also must ensure the store subscription
14 | // is created synchronously, otherwise a store update may occur before the
15 | // subscription is created and an inconsistent state may be observed
16 |
17 | // Matches logic in React's `shared/ExecutionEnvironment` file
18 | export const canUseDOM = Boolean(
19 | typeof window !== 'undefined' &&
20 | typeof window.document !== 'undefined' &&
21 | typeof window.document.createElement !== 'undefined',
22 | );
23 |
24 | export const useIsomorphicLayoutEffect = canUseDOM
25 | ? useLayoutEffect
26 | : useEffect;
27 |
--------------------------------------------------------------------------------
/packages/plugins/auto-actions/tests/array.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore, model } from '@modern-js-reduck/store';
2 | import plugin from '../src';
3 |
4 | const testModel = model('name').define({
5 | state: [1, 2, 3],
6 | });
7 |
8 | const store = createStore({
9 | plugins: [plugin],
10 | });
11 |
12 | const [, actions] = store.use(testModel);
13 |
14 | const expectState = (state: any) => {
15 | expect(store.use(testModel)[0]).toEqual(state);
16 | };
17 |
18 | describe('test array auto actions', () => {
19 | test('push', () => {
20 | actions.push(4);
21 |
22 | expectState([1, 2, 3, 4]);
23 | });
24 |
25 | test('pop', () => {
26 | actions.pop();
27 | expectState([1, 2, 3]);
28 | });
29 |
30 | test('shift', () => {
31 | actions.shift();
32 | expectState([2, 3]);
33 | });
34 |
35 | test('unshift', () => {
36 | actions.unshift(1);
37 | expectState([1, 2, 3]);
38 | });
39 |
40 | test('concat', () => {
41 | actions.concat([4, 5]);
42 | expectState([1, 2, 3, 4, 5]);
43 | });
44 |
45 | test('splice', () => {
46 | actions.splice(0, 2);
47 | expectState([3, 4, 5]);
48 |
49 | actions.splice(0, 0, 1, 2);
50 | expectState([1, 2, 3, 4, 5]);
51 | });
52 |
53 | test('filter', () => {
54 | actions.filter(value => value <= 2);
55 | expectState([1, 2]);
56 |
57 | actions.push(3);
58 | expectState([1, 2, 3]);
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/packages/plugins/xstate-immer/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import { createPlugin } from '@modern-js-reduck/store';
2 | import { isMachineModel } from '@modern-js-reduck/plugin-xstate';
3 | import { assign } from '@xstate/immer';
4 |
5 | export const plugin = createPlugin(() => ({
6 | prepareModelDesc(modelDesc) {
7 | if (!isMachineModel(modelDesc)) {
8 | return modelDesc;
9 | }
10 |
11 | const path: string[] = [];
12 |
13 | // FIXME:
14 | traverse((modelDesc.machine as any).config.states);
15 |
16 | return modelDesc;
17 |
18 | /**
19 | * traverse for immerifing actions
20 | */
21 | function traverse(obj: any) {
22 | Object.keys(obj).forEach(key => {
23 | path.push(key);
24 | const realPath = path.join('/');
25 |
26 | const isOnActions = /\/on\/\S+\/actions$/.exec(realPath);
27 |
28 | if (typeof obj[key] === 'string') {
29 | path.pop();
30 | return;
31 | }
32 |
33 | const isFunction = typeof obj[key] === 'function';
34 | const isAssignObj =
35 | Object.keys(obj[key]) === ['type', 'assignment'] &&
36 | obj[key].type === 'xstate.assign';
37 | if (isOnActions && isFunction) {
38 | obj[key] = assign(obj[key]);
39 | } else if (isAssignObj) {
40 | obj[key] = assign(obj[key].assignment);
41 | } else {
42 | traverse(obj[key]);
43 | }
44 | path.pop();
45 | });
46 | }
47 | },
48 | }));
49 |
--------------------------------------------------------------------------------
/packages/store/src/utils/misc.ts:
--------------------------------------------------------------------------------
1 | import { Model } from '@/types';
2 |
3 | export const initializerSymbol = Symbol.for('__reduck_model_initializer__');
4 |
5 | export const getModelInitializer = (_model: Model) => _model[initializerSymbol];
6 |
7 | export const isModel = (_model: any): _model is Model =>
8 | _model && Boolean(getModelInitializer(_model));
9 |
10 | export const getComputedDepModels = (computed: any) => {
11 | const depModels: Model[] = [];
12 | const computedArr = Array.isArray(computed) ? computed : [computed];
13 |
14 | computedArr.forEach(_computed => {
15 | computed &&
16 | Object.keys(computed).forEach(key => {
17 | const selector = computed[key];
18 | if (Array.isArray(selector)) {
19 | selector.forEach(s => {
20 | if (!depModels.includes(s) && isModel(s)) {
21 | depModels.push(s);
22 | }
23 | });
24 | }
25 | });
26 | });
27 |
28 | return depModels;
29 | };
30 |
31 | export enum StateType {
32 | Primitive = 'primitive',
33 | Array = 'array',
34 | Object = 'object',
35 | }
36 |
37 | export const getStateType = (value: any): StateType => {
38 | if (Array.isArray(value)) {
39 | return StateType.Array;
40 | // eslint-disable-next-line eqeqeq
41 | } else if (typeof value === 'object' && value != 'undefined') {
42 | return StateType.Object;
43 | } else {
44 | // ignore other types of checking which are not supported by Redux
45 | return StateType.Primitive;
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/examples/vanilla-counter/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | reduck-vanilla-counter
5 |
6 |
7 |
10 |
11 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/packages/store/tests/store.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore, model } from '../src';
2 |
3 | const countModel = model<{ value: number }>('counter').define({
4 | name: 'counter',
5 | state: {
6 | value: 1,
7 | },
8 | actions: {
9 | add(state) {
10 | return {
11 | ...state,
12 | value: state.value + 1,
13 | };
14 | },
15 | },
16 | });
17 |
18 | describe('createStore', () => {
19 | test('create store should work', () => {
20 | const store = createStore();
21 |
22 | expect(store.getState()).toEqual({});
23 | });
24 |
25 | test('store use model should work', () => {
26 | const store = createStore();
27 | const [state] = store.use(countModel);
28 |
29 | expect(state).toEqual({ value: 1 });
30 | expect(store.getState()).toEqual({
31 | counter: { value: 1 },
32 | });
33 | });
34 |
35 | test('model actions should work', () => {
36 | const store = createStore();
37 | const [state, actions] = store.use(countModel);
38 |
39 | expect(state).toEqual({ value: 1 });
40 | expect(store.getState()).toEqual({
41 | counter: { value: 1 },
42 | });
43 |
44 | actions.add();
45 | expect(store.getState()).toEqual({
46 | counter: { value: 2 },
47 | });
48 | });
49 |
50 | test('mount models in createStore', () => {
51 | const store = createStore({ models: [countModel] });
52 |
53 | expect(store.getState()).toEqual({ counter: { value: 1 } });
54 |
55 | const [, actions] = store.use(countModel);
56 | actions.add();
57 |
58 | expect(store.getState()).toEqual({
59 | counter: { value: 2 },
60 | });
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/packages/store/tests/selector.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore, model } from '../src';
2 |
3 | const count1Model = model('count1').define({
4 | state: {
5 | value: 1,
6 | },
7 | actions: {
8 | add(state) {
9 | return {
10 | ...state,
11 | value: state.value + 1,
12 | };
13 | },
14 | },
15 | });
16 |
17 | const count2Model = model('count2').define({
18 | state: {
19 | value: 10,
20 | },
21 | actions: {
22 | add1(state) {
23 | return {
24 | ...state,
25 | value: state.value + 1,
26 | };
27 | },
28 | },
29 | });
30 |
31 | describe('test selector', () => {
32 | const store = createStore();
33 |
34 | test('select state should work', () => {
35 | const [state] = store.use(count1Model, count2Model, (state1, state2) => ({
36 | one: state1.value,
37 | two: state2.value,
38 | }));
39 |
40 | expect(state).toEqual({ one: 1, two: 10 });
41 | });
42 |
43 | test('select actions should work', () => {
44 | const use = () =>
45 | store.use(
46 | count1Model,
47 | count2Model,
48 | (state1, state2) => ({
49 | one: state1.value,
50 | two: state2.value,
51 | }),
52 | (actions1, actions2) => ({
53 | oneAdd: actions1.add,
54 | twoAdd: actions2.add1,
55 | }),
56 | );
57 |
58 | const [state, actions] = use();
59 |
60 | expect(state).toEqual({ one: 1, two: 10 });
61 |
62 | actions.oneAdd();
63 |
64 | expect(use()[0]).toEqual({ one: 2, two: 10 });
65 |
66 | actions.twoAdd();
67 |
68 | expect(use()[0]).toEqual({ one: 2, two: 11 });
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/.github/workflows/release-pull-request.yml:
--------------------------------------------------------------------------------
1 | name: Release Pull Request
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | type: choice
8 | description: 'Release Type(next, beta, alpha, latest)'
9 | required: true
10 | default: 'latest'
11 | options:
12 | - next
13 | - beta
14 | - alpha
15 | - latest
16 |
17 | jobs:
18 | release:
19 | name: Create Release Pull Request
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: Checkout Repo
23 | uses: actions/checkout@master
24 | with:
25 | fetch-depth: 100
26 |
27 | - name: Install Pnpm
28 | run: corepack enable
29 |
30 | - name: Setup Node.js 16
31 | uses: actions/setup-node@v3
32 | with:
33 | node-version: "16"
34 | cache: 'pnpm'
35 |
36 | - name: Install Dependencies
37 | run: pnpm install --ignore-scripts
38 |
39 | - name: Prepare Monorepo-Tools
40 | run: pnpm run --filter @modern-js/monorepo-tools... build
41 |
42 | - name: Create Release Pull Request
43 | uses: web-infra-dev/actions@v2
44 | with:
45 | # this expects you to have a script called release which does a build for your packages and calls changeset publish
46 | version: ${{ github.event.inputs.version }}
47 | versionNumber: 'auto'
48 | type: 'pull request'
49 | tools: 'modern'
50 | env:
51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
53 | REPOSITORY: ${{ github.repository }}
54 | REF: ${{ github.ref }}
55 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | type: choice
8 | description: 'Release Type(next, beta, alpha, latest)'
9 | required: true
10 | default: 'latest'
11 | options:
12 | - next
13 | - beta
14 | - alpha
15 | - latest
16 | branch:
17 | description: 'Release Branch(confirm release branch)'
18 | required: true
19 | default: 'main'
20 |
21 | jobs:
22 | release:
23 | name: Release
24 | if: ${{ github.event_name == 'workflow_dispatch' }}
25 | runs-on: ubuntu-latest
26 | steps:
27 | - name: Checkout Repo
28 | uses: actions/checkout@v3
29 | with:
30 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
31 | fetch-depth: 1
32 |
33 | - name: Install Pnpm
34 | run: corepack enable
35 |
36 | - name: Setup Node.js 16
37 | uses: actions/setup-node@v3
38 | with:
39 | node-version: "16"
40 | cache: 'pnpm'
41 |
42 | - name: Install Dependencies && Build
43 | run: pnpm install
44 |
45 | - name: Release
46 | uses: web-infra-dev/actions@v2
47 | with:
48 | version: ${{ github.event.inputs.version }}
49 | branch: ${{ github.event.inputs.branch }}
50 | type: 'release'
51 | tools: 'modern'
52 | env:
53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
55 | REPOSITORY: ${{ github.repository }}
56 | REF: ${{ github.ref }}
57 |
58 |
59 |
--------------------------------------------------------------------------------
/packages/store/src/types/plugin.ts:
--------------------------------------------------------------------------------
1 | import type { Store } from 'redux';
2 | import type { Context, StoreConfig } from './app';
3 | import type { Action, Model, ModelDesc, MountedModel } from './model';
4 |
5 | export interface PluginContext {
6 | store: Store;
7 | }
8 |
9 | export interface PluginLifeCycle {
10 | /**
11 | * Before createStore, this hook will be invoked. Use to change config.
12 | */
13 | config?: (config: T) => T;
14 |
15 | /**
16 | * Runs after store created
17 | */
18 | afterCreateStore?: (
19 | store: T,
20 | ) => T;
21 |
22 | /**
23 | * Runs when a model mounted for first time.
24 | */
25 | modelMount?: (
26 | params: T,
27 | api: {
28 | /**
29 | * path: ['todo', 'load']
30 | */
31 | setDispatchAction: (path: string[], dispatchAction: any) => void;
32 | },
33 | ) => T;
34 |
35 | /**
36 | * invoke before useModel value return.
37 | * You can custom returned value in this hook.
38 | */
39 | useModel?: (
40 | bypassParams: T,
41 | {
42 | models,
43 | mountedModels,
44 | }: {
45 | models: Model[];
46 | mountedModels: MountedModel[];
47 | },
48 | ) => T;
49 |
50 | prepareModelDesc?: (modelDesc: ModelDesc) => ModelDesc;
51 |
52 | /**
53 | * invoke before reducer execute. You can wrap and return your reducer.
54 | */
55 | beforeReducer?: (
56 | reducer: Action,
57 | options: { name: string; computedDescriptors: any },
58 | ) => Action;
59 | }
60 |
61 | export type Plugin = (context: PluginContext) => PluginLifeCycle;
62 |
--------------------------------------------------------------------------------
/packages/plugins/auto-actions/src/array.ts:
--------------------------------------------------------------------------------
1 | const push = (state: T, payload: T[0]) =>
2 | state.concat(payload);
3 |
4 | const pop = (state: T) => {
5 | const newState = [];
6 |
7 | for (let i = 0; i < state.length - 1; i++) {
8 | newState.push(state[i]);
9 | }
10 |
11 | return newState;
12 | };
13 |
14 | const shift = (state: T) => {
15 | const newState = [];
16 |
17 | for (let i = 1; i < state.length; i++) {
18 | newState.push(state[i]);
19 | }
20 |
21 | return newState;
22 | };
23 |
24 | const unshift = (state: T, payload: T[0]) => [
25 | payload,
26 | ...state,
27 | ];
28 |
29 | const concat = (state: T, payload: T) => [
30 | ...state,
31 | ...payload,
32 | ];
33 |
34 | const splice = (
35 | state: T,
36 | start: number,
37 | deleteCount: number,
38 | ...items: T
39 | ) => {
40 | const newState = state.slice();
41 | newState.splice(start, deleteCount, ...items);
42 |
43 | return newState;
44 | };
45 |
46 | const filter = (
47 | state: T,
48 | filterFn: (value: T[0], index: number, array: T[0][]) => boolean,
49 | ) => {
50 | const newState = state.filter(filterFn);
51 |
52 | return newState;
53 | };
54 |
55 | type ArrayDispatchActions = {
56 | push: (payload: T[0]) => void;
57 | pop: () => void;
58 | shift: () => void;
59 | unshift: (payload: T[0]) => void;
60 | concat: (payload: T) => void;
61 | splice: (start: number, deleteCount: number, ...items: T) => void;
62 | filter: (
63 | filterFn: (value: T[0], index: number, array: T[0][]) => boolean,
64 | ) => void;
65 | };
66 |
67 | export { push, pop, shift, unshift, concat, splice, filter };
68 | export type { ArrayDispatchActions };
69 |
--------------------------------------------------------------------------------
/packages/store/src/types/app.ts:
--------------------------------------------------------------------------------
1 | import { Reducer, Store as ReduxStore, Middleware, StoreEnhancer } from 'redux';
2 | import { Model, MountedModel } from './model';
3 | import { Plugin } from './plugin';
4 | import { createUseModel } from '@/model/useModel';
5 | import { createPluginCore } from '@/plugin';
6 | import { createSubscribe } from '@/model/subscribe';
7 |
8 | export interface ReduckContext {
9 | store: Context['store'];
10 | }
11 |
12 | /**
13 | * Context of reduck app
14 | */
15 | export interface Context {
16 | /**
17 | * Store instance
18 | */
19 | store: ReduxStore & {
20 | use: ReturnType;
21 | unmount: (model: Model) => void;
22 | };
23 | apis: {
24 | addReducers: (reducers: Record) => void;
25 | addModel: (model: M, mountModel: MountedModel) => void;
26 |
27 | getModel: (model: M) => MountedModel | null;
28 |
29 | useModel: ReturnType;
30 |
31 | getModelSubscribe: (model: Model) => ReturnType;
32 |
33 | /**
34 | * Get mountedModel instance by modelname
35 | */
36 | getModelByName: (name: string) => MountedModel | null;
37 |
38 | /**
39 | * Tag that model with name is `param name` is in mounting.
40 | */
41 | mountingModel: (modelname: string) => void;
42 |
43 | /**
44 | * Unmount model
45 | */
46 | unmountModel: (model: Model) => void;
47 | };
48 | pluginCore: ReturnType;
49 | }
50 |
51 | export interface StoreConfig {
52 | initialState?: Record;
53 | middlewares?: Middleware[];
54 | models?: Model[];
55 | plugins?: Plugin[];
56 | enhancers?: StoreEnhancer[];
57 | }
58 |
59 | export type Store = Context['store'];
60 |
--------------------------------------------------------------------------------
/examples/todos/src/models/todos.ts:
--------------------------------------------------------------------------------
1 | import { model } from '@modern-js-reduck/react';
2 |
3 | export enum VisibilityFilters {
4 | SHOW_ALL = 'SHOW_ALL',
5 | SHOW_COMPLETED = 'SHOW_COMPLETED',
6 | SHOW_ACTIVE = 'SHOW_ACTIVE',
7 | }
8 |
9 | let nextTodoId = 0;
10 |
11 | export interface Todo {
12 | id: number;
13 | text: string;
14 | completed: boolean;
15 | }
16 |
17 | interface State {
18 | data: Todo[];
19 | visibilityFilter: VisibilityFilters;
20 | }
21 |
22 | const todos = model('todos').define({
23 | state: {
24 | data: [],
25 | visibilityFilter: VisibilityFilters.SHOW_ALL,
26 | },
27 | computed: {
28 | visibleTodos: state => {
29 | const { data, visibilityFilter } = state;
30 | switch (visibilityFilter) {
31 | case VisibilityFilters.SHOW_ALL:
32 | return data;
33 | case VisibilityFilters.SHOW_COMPLETED:
34 | return data.filter(t => t.completed);
35 | case VisibilityFilters.SHOW_ACTIVE:
36 | return data.filter(t => !t.completed);
37 | default:
38 | throw new Error('Unknown filter: ' + visibilityFilter);
39 | }
40 | },
41 | completedTodoCount: state =>
42 | state.data.reduce(
43 | (count, todo) => (todo.completed ? count + 1 : count),
44 | 0,
45 | ),
46 | },
47 | actions: {
48 | addTodo: (state, text: string) => {
49 | state.data.push({ id: nextTodoId++, text, completed: false });
50 | },
51 | toggleTodo: (state, id: number) => {
52 | state.data.forEach(todo =>
53 | todo.id === id ? (todo.completed = !todo.completed) : todo,
54 | );
55 | },
56 | setVisibilityFilter: (state, filter: VisibilityFilters) => {
57 | state.visibilityFilter = filter;
58 | },
59 | },
60 | });
61 |
62 | export default todos;
63 |
--------------------------------------------------------------------------------
/packages/plugins/auto-actions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modern-js-reduck/plugin-auto-actions",
3 | "version": "1.1.13",
4 | "files": [
5 | "dist"
6 | ],
7 | "jsnext:source": "./src/index.ts",
8 | "types": "./dist/types/index.d.ts",
9 | "main": "./dist/cjs/index.js",
10 | "module": "./dist/esm/index.js",
11 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.",
12 | "homepage": "https://modernjs.dev",
13 | "bugs": "https://github.com/modern-js-dev/reduck/issues",
14 | "license": "MIT",
15 | "keywords": [
16 | "react",
17 | "framework",
18 | "modern",
19 | "modern.js",
20 | "state",
21 | "reduck"
22 | ],
23 | "exports": {
24 | ".": {
25 | "types": "./dist/types/index.d.ts",
26 | "require": "./dist/cjs/index.js",
27 | "default": "./dist/esm/index.js"
28 | }
29 | },
30 | "scripts": {
31 | "prepare": "pnpm build",
32 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform",
33 | "new": "modern new",
34 | "build": "modern build",
35 | "test": "modern test"
36 | },
37 | "dependencies": {
38 | "@swc/helpers": "0.5.1"
39 | },
40 | "devDependencies": {
41 | "@modern-js-reduck/store": "workspace:*",
42 | "@modern-js/module-tools": "2.21.1",
43 | "@modern-js/plugin-testing": "2.21.1",
44 | "@types/jest": "^27.5.1",
45 | "@types/node": "^14",
46 | "typescript": "^4",
47 | "@modern-js-reduck/scripts": "workspace:*"
48 | },
49 | "modernSettings": {},
50 | "sideEffects": false,
51 | "peerDependencies": {
52 | "@modern-js-reduck/store": "workspace:^1.1.13"
53 | },
54 | "publishConfig": {
55 | "registry": "https://registry.npmjs.org/",
56 | "access": "public"
57 | },
58 | "repository": "modern-js-dev/reduck"
59 | }
60 |
--------------------------------------------------------------------------------
/packages/plugins/immer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modern-js-reduck/plugin-immutable",
3 | "version": "1.1.13",
4 | "files": [
5 | "dist"
6 | ],
7 | "jsnext:source": "./src/index.ts",
8 | "types": "./dist/types/index.d.ts",
9 | "main": "./dist/cjs/index.js",
10 | "module": "./dist/esm/index.js",
11 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.",
12 | "homepage": "https://modernjs.dev",
13 | "bugs": "https://github.com/modern-js-dev/reduck/issues",
14 | "license": "MIT",
15 | "keywords": [
16 | "react",
17 | "framework",
18 | "modern",
19 | "modern.js",
20 | "state",
21 | "reduck"
22 | ],
23 | "exports": {
24 | ".": {
25 | "types": "./dist/types/index.d.ts",
26 | "require": "./dist/cjs/index.js",
27 | "default": "./dist/esm/index.js"
28 | }
29 | },
30 | "scripts": {
31 | "prepare": "pnpm build",
32 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform",
33 | "new": "modern new",
34 | "build": "modern build",
35 | "test": "modern test"
36 | },
37 | "dependencies": {
38 | "@swc/helpers": "0.5.1",
39 | "immer": "^9.0.5"
40 | },
41 | "devDependencies": {
42 | "@modern-js-reduck/store": "workspace:*",
43 | "@modern-js/module-tools": "2.21.1",
44 | "@modern-js/plugin-testing": "2.21.1",
45 | "@types/jest": "^27.5.1",
46 | "@types/node": "^14",
47 | "typescript": "^4",
48 | "@modern-js-reduck/scripts": "workspace:*"
49 | },
50 | "modernSettings": {},
51 | "sideEffects": false,
52 | "peerDependencies": {
53 | "@modern-js-reduck/store": "workspace:^1.1.13"
54 | },
55 | "publishConfig": {
56 | "registry": "https://registry.npmjs.org/",
57 | "access": "public"
58 | },
59 | "repository": "modern-js-dev/reduck"
60 | }
61 |
--------------------------------------------------------------------------------
/packages/plugins/devtools/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modern-js-reduck/plugin-devtools",
3 | "version": "1.1.13",
4 | "files": [
5 | "dist"
6 | ],
7 | "jsnext:source": "./src/index.ts",
8 | "types": "./dist/types/index.d.ts",
9 | "main": "./dist/cjs/index.js",
10 | "module": "./dist/esm/index.js",
11 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.",
12 | "homepage": "https://modernjs.dev",
13 | "bugs": "https://github.com/modern-js-dev/reduck/issues",
14 | "license": "MIT",
15 | "keywords": [
16 | "react",
17 | "framework",
18 | "modern",
19 | "modern.js",
20 | "state",
21 | "reduck"
22 | ],
23 | "exports": {
24 | ".": {
25 | "types": "./dist/types/index.d.ts",
26 | "require": "./dist/cjs/index.js",
27 | "default": "./dist/esm/index.js"
28 | }
29 | },
30 | "scripts": {
31 | "prepare": "pnpm build",
32 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform",
33 | "new": "modern new",
34 | "build": "modern build",
35 | "test": "modern test --passWithNoTests"
36 | },
37 | "dependencies": {
38 | "@swc/helpers": "0.5.1",
39 | "@redux-devtools/extension": "^3.2.2",
40 | "redux": "^4.1.1"
41 | },
42 | "devDependencies": {
43 | "@modern-js-reduck/store": "workspace:*",
44 | "@modern-js/module-tools": "2.21.1",
45 | "@modern-js/plugin-testing": "2.21.1",
46 | "@types/jest": "^27.5.1",
47 | "@types/node": "^14",
48 | "typescript": "^4",
49 | "@modern-js-reduck/scripts": "workspace:*"
50 | },
51 | "modernSettings": {},
52 | "sideEffects": false,
53 | "peerDependencies": {
54 | "@modern-js-reduck/store": "workspace:^1.1.13"
55 | },
56 | "publishConfig": {
57 | "registry": "https://registry.npmjs.org/",
58 | "access": "public"
59 | },
60 | "repository": "modern-js-dev/reduck"
61 | }
62 |
--------------------------------------------------------------------------------
/packages/plugins/auto-actions/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createPlugin, utils } from '@modern-js-reduck/store';
2 | import { Model } from '@modern-js-reduck/store/types';
3 | import { mergeActions } from './utils';
4 | import * as primitiveActions from './primitive';
5 | import { ArrayDispatchActions } from './array';
6 | import * as arrayActions from './array';
7 | import { ObjectDispatchActions, createObjectActions } from './object';
8 |
9 | type ExtractDispatchAction = {
10 | [key in keyof T]: T[key] extends (state: any) => any
11 | ? () => void
12 | : T[key] extends (state: any, payload: any) => any
13 | ? (payload: State) => void
14 | : never;
15 | };
16 |
17 | declare module '@modern-js-reduck/store' {
18 | // Overload GetActions interface to add actions type to useModel's return
19 | interface GetActions {
20 | autoActions: M['_']['state'] extends
21 | | string
22 | | number
23 | | null
24 | | undefined
25 | | ((...args: any[]) => any)
26 | | RegExp
27 | | symbol
28 | ? ExtractDispatchAction
29 | : M['_']['state'] extends any[]
30 | ? ArrayDispatchActions
31 | : M['_']['state'] extends Record
32 | ? ObjectDispatchActions
33 | : Record;
34 | }
35 | }
36 |
37 | export default createPlugin(() => ({
38 | prepareModelDesc(modelDesc) {
39 | const initialState = modelDesc.state;
40 | const type = utils.getStateType(initialState);
41 |
42 | if (type === 'primitive') {
43 | return mergeActions(modelDesc, primitiveActions);
44 | }
45 |
46 | if (type === 'array') {
47 | return mergeActions(modelDesc, arrayActions);
48 | }
49 |
50 | if (type === 'object') {
51 | return mergeActions(modelDesc, createObjectActions(modelDesc.state));
52 | }
53 |
54 | return modelDesc;
55 | },
56 | }));
57 |
--------------------------------------------------------------------------------
/packages/plugins/effects/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modern-js-reduck/plugin-effects",
3 | "version": "1.1.13",
4 | "files": [
5 | "dist"
6 | ],
7 | "jsnext:source": "./src/index.ts",
8 | "types": "./dist/types/index.d.ts",
9 | "main": "./dist/cjs/index.js",
10 | "module": "./dist/esm/index.js",
11 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.",
12 | "homepage": "https://modernjs.dev",
13 | "bugs": "https://github.com/modern-js-dev/reduck/issues",
14 | "license": "MIT",
15 | "keywords": [
16 | "react",
17 | "framework",
18 | "modern",
19 | "modern.js",
20 | "state",
21 | "reduck"
22 | ],
23 | "exports": {
24 | ".": {
25 | "types": "./dist/types/index.d.ts",
26 | "require": "./dist/cjs/index.js",
27 | "default": "./dist/esm/index.js"
28 | }
29 | },
30 | "scripts": {
31 | "prepare": "pnpm build",
32 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform",
33 | "new": "modern new",
34 | "build": "modern build",
35 | "test": "modern test"
36 | },
37 | "dependencies": {
38 | "@swc/helpers": "0.5.1",
39 | "redux": "^4.1.1",
40 | "redux-promise-middleware": "^6.1.2"
41 | },
42 | "devDependencies": {
43 | "@modern-js-reduck/store": "workspace:*",
44 | "@modern-js/module-tools": "2.21.1",
45 | "@modern-js/plugin-testing": "2.21.1",
46 | "@types/jest": "^27.5.1",
47 | "@types/node": "^14",
48 | "@types/redux-logger": "^3.0.9",
49 | "redux-logger": "^3.0.6",
50 | "typescript": "^4",
51 | "@modern-js-reduck/scripts": "workspace:*"
52 | },
53 | "modernSettings": {},
54 | "sideEffects": false,
55 | "peerDependencies": {
56 | "@modern-js-reduck/store": "workspace:^1.1.13"
57 | },
58 | "publishConfig": {
59 | "registry": "https://registry.npmjs.org/",
60 | "access": "public"
61 | },
62 | "repository": "modern-js-dev/reduck"
63 | }
64 |
--------------------------------------------------------------------------------
/packages/plugins/xstate/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modern-js-reduck/plugin-xstate",
3 | "version": "1.1.0",
4 | "private": true,
5 | "jsnext:source": "./src/index.ts",
6 | "types": "./dist/types/index.d.ts",
7 | "main": "./dist/cjs/index.js",
8 | "module": "./dist/esm/index.js",
9 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.",
10 | "homepage": "https://modernjs.dev",
11 | "bugs": "https://github.com/modern-js-dev/reduck/issues",
12 | "license": "MIT",
13 | "keywords": [
14 | "react",
15 | "framework",
16 | "modern",
17 | "modern.js",
18 | "state",
19 | "reduck"
20 | ],
21 | "exports": {
22 | ".": {
23 | "types": "./dist/types/index.d.ts",
24 | "require": "./dist/cjs/index.js",
25 | "default": "./dist/esm/index.js"
26 | }
27 | },
28 | "scripts": {
29 | "prepare": "pnpm build",
30 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform",
31 | "new": "modern new",
32 | "build": "modern build",
33 | "test": "modern test"
34 | },
35 | "dependencies": {
36 | "@swc/helpers": "0.5.1",
37 | "xstate": "^4.23.1"
38 | },
39 | "devDependencies": {
40 | "@modern-js-reduck/store": "workspace:*",
41 | "@modern-js/module-tools": "2.21.1",
42 | "@modern-js/plugin-testing": "2.21.1",
43 | "@testing-library/jest-dom": "^5.14.1",
44 | "@testing-library/react": "^12.0.0",
45 | "@types/jest": "^27.5.1",
46 | "@types/node": "^14",
47 | "@types/testing-library__jest-dom": "^5.14.1",
48 | "typescript": "^4",
49 | "@modern-js-reduck/scripts": "workspace:*"
50 | },
51 | "modernSettings": {},
52 | "sideEffects": false,
53 | "peerDependencies": {
54 | "@modern-js-reduck/store": "workspace:^1.1.13"
55 | },
56 | "publishConfig": {
57 | "registry": "https://registry.npmjs.org/",
58 | "access": "public"
59 | },
60 | "repository": "modern-js-dev/reduck"
61 | }
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reduck
6 |
7 |
8 | A Redux-based state management library.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ## Introduction
18 |
19 | Reduck is a Redux-based state management library that offers simple APIs and less boilerplate codes.
20 | It can be used out of the box within Modern.js, which is also recommended, or it can be used directly.
21 |
22 | ## Documentation
23 |
24 | The Reduck docs are located at : https://modernjs.dev/en/guides/topic-detail/model/quick-start.
25 |
26 | # Examples
27 | - [todos](https://stackblitz.com/github/modern-js-dev/reduck/tree/main/examples/todos?file=src%2Findex.tsx&terminal=start&title=todos)
28 | - [vanilla-counter](https://stackblitz.com/github/modern-js-dev/reduck/tree/main/examples/vanilla-counter?file=index.html&terminal=dev&title=vanilla-counter)
29 | - [vite-counter](https://stackblitz.com/github/modern-js-dev/reduck/tree/main/examples/vite-counter?file=src%2FApp.tsx&terminal=dev&title=vite-counter)
30 |
31 |
32 | ## Contributing
33 |
34 | > New contributors welcome!
35 |
36 | Please read the [Contributing Guide](https://github.com/web-infra-dev/modern.js/blob/main/CONTRIBUTING.md).
37 |
38 | ### Code of Conduct
39 |
40 | This repo has adopted the Bytedance Open Source Code of Conduct. Please check [Code of Conduct](./CODE_OF_CONDUCT.md) for more details.
41 |
42 |
43 | ## License
44 |
45 | Reduck is [MIT licensed](https://github.com/web-infra-dev/reduck/blob/main/LICENSE).
46 |
--------------------------------------------------------------------------------
/packages/store/src/__tsd__/selector.tsd.ts:
--------------------------------------------------------------------------------
1 | import { expectType } from 'tsd';
2 | import { createStore, model } from '..';
3 | import { ReduxAction } from '@/types';
4 |
5 | const count1Model = model('count1').define({
6 | state: {
7 | value: 1,
8 | },
9 | actions: {
10 | add(state) {
11 | return {
12 | ...state,
13 | value: state.value + 1,
14 | };
15 | },
16 | sub(state, n: number) {
17 | return {
18 | ...state,
19 | value: state.value - n,
20 | };
21 | },
22 | },
23 | });
24 |
25 | const count2Model = model('count2').define({
26 | state: {
27 | value: 10,
28 | },
29 | actions: {
30 | add1(state) {
31 | return {
32 | ...state,
33 | value: state.value + 1,
34 | };
35 | },
36 | sub1(state, n: number) {
37 | return {
38 | ...state,
39 | value: state.value - n,
40 | };
41 | },
42 | },
43 | });
44 | type State = {
45 | one: number;
46 | two: number;
47 | };
48 | describe('test selector', () => {
49 | const store = createStore();
50 |
51 | test('select state should work', () => {
52 | const [state] = store.use(count1Model, count2Model, (state1, state2) => ({
53 | one: state1.value,
54 | two: state2.value,
55 | }));
56 | expectType(state);
57 | });
58 |
59 | test('select actions should work', () => {
60 | const use = () =>
61 | store.use(
62 | count1Model,
63 | count2Model,
64 | (state1, state2) => ({
65 | one: state1.value,
66 | two: state2.value,
67 | }),
68 | (actions1, actions2) => ({
69 | oneAdd: actions1.add,
70 | twoAdd: actions2.add1,
71 | oneSub: actions1.sub,
72 | twoSub: actions2.sub1,
73 | }),
74 | );
75 |
76 | const [state, actions] = use();
77 | expectType(state);
78 | expectType<{
79 | oneAdd: () => ReduxAction;
80 | twoAdd: () => ReduxAction;
81 | oneSub: (n: number) => ReduxAction;
82 | twoSub: (n: number) => ReduxAction;
83 | }>(actions);
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/packages/plugins/xstate-immer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modern-js-reduck/plugin-xstate-immer",
3 | "version": "1.1.0",
4 | "private": true,
5 | "files": [
6 | "dist"
7 | ],
8 | "jsnext:source": "./src/index.ts",
9 | "types": "./dist/types/index.d.ts",
10 | "main": "./dist/cjs/index.js",
11 | "module": "./dist/esm/index.js",
12 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.",
13 | "homepage": "https://modernjs.dev",
14 | "bugs": "https://github.com/modern-js-dev/reduck/issues",
15 | "license": "MIT",
16 | "keywords": [
17 | "react",
18 | "framework",
19 | "modern",
20 | "modern.js",
21 | "state",
22 | "reduck"
23 | ],
24 | "exports": {
25 | ".": {
26 | "types": "./dist/types/index.d.ts",
27 | "require": "./dist/cjs/index.js",
28 | "default": "./dist/esm/index.js"
29 | }
30 | },
31 | "scripts": {
32 | "prepare": "pnpm build",
33 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform",
34 | "new": "modern new",
35 | "build": "modern build",
36 | "test": "modern test"
37 | },
38 | "dependencies": {
39 | "@swc/helpers": "0.5.1",
40 | "@xstate/immer": "^0.3.1",
41 | "immer": "^9.0.5"
42 | },
43 | "devDependencies": {
44 | "@modern-js-reduck/plugin-xstate": "workspace:*",
45 | "@modern-js-reduck/store": "workspace:*",
46 | "@modern-js/module-tools": "2.21.1",
47 | "@modern-js/plugin-testing": "2.21.1",
48 | "@testing-library/jest-dom": "^5.14.1",
49 | "@testing-library/react": "^12.0.0",
50 | "@types/jest": "^27.5.1",
51 | "@types/node": "^14",
52 | "@types/testing-library__jest-dom": "^5.14.1",
53 | "typescript": "^4",
54 | "xstate": "*",
55 | "@modern-js-reduck/scripts": "workspace:*"
56 | },
57 | "modernSettings": {},
58 | "sideEffects": false,
59 | "peerDependencies": {
60 | "@modern-js-reduck/store": "workspace:^1.1.13",
61 | "@modern-js-reduck/plugin-xstate": "workspace:^1.1.0"
62 | },
63 | "publishConfig": {
64 | "registry": "https://registry.npmjs.org/",
65 | "access": "public"
66 | },
67 | "repository": "modern-js-dev/reduck"
68 | }
69 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Reduck Contributing Guide
2 |
3 | Thanks for that you are interested in contributing to Reduck.
4 |
5 | ## Developing
6 |
7 | To develop locally:
8 |
9 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
10 | own GitHub account and then
11 | [clone](https://help.github.com/articles/cloning-a-repository/) it to your
12 | local.
13 | 2. Create a new branch:
14 |
15 | ```zsh
16 | git checkout -b MY_BRANCH_NAME
17 | ```
18 |
19 | 3. Install pnpm:
20 |
21 | ```zsh
22 | npm install -g pnpm
23 | ```
24 |
25 | 4. Install the dependencies with:
26 |
27 | ```zsh
28 | pnpm run setup
29 | ```
30 |
31 | 5. Go into package which you want to contribute.
32 |
33 | ```zsh
34 | cd ./packages/
35 | ```
36 |
37 | 6. Start developing.
38 |
39 |
40 | ## Building
41 |
42 | You can build single package, with:
43 |
44 | ```zsh
45 | cd ./packages/*
46 | pnpm build
47 | ```
48 |
49 | build all packages, with:
50 |
51 | ```zsh
52 | pnpm -r prepare
53 | ```
54 |
55 | If you need to clean all `node_modules/*` the project for any reason, with
56 |
57 | ```zsh
58 | pnpm reset
59 | ```
60 |
61 | ## Testing
62 |
63 | You need write new test cases for new feature or modify existing test cases for changes.
64 |
65 | We wish you write unit tests at `PACKAGE_DIR/__test__`. Test syntax is based on [jest](https://jestjs.io/).
66 |
67 | ### Run Testing
68 |
69 | ```sh
70 | pnpm -r test
71 | ```
72 |
73 | ## Linting
74 |
75 | To check the formatting of your code:
76 |
77 | ```zsh
78 | pnpm lint
79 | ```
80 |
81 | ## Publishing
82 |
83 | We use **Modern.js Monorepo Solution** to manage version and changelog.
84 |
85 | Repository maintainers can publish a new version of all packages to npm.
86 |
87 | 1. Fetch newest code at branch `main`.
88 | 2. Install
89 |
90 | ```zsh
91 | pnpm run setup
92 | ```
93 |
94 | 3. Add changeset
95 |
96 | ```zsh
97 | pnpm change
98 | ```
99 |
100 | 4. Bump version
101 |
102 | ```zsh
103 | pnpm bump
104 | ```
105 |
106 | 5. Commit version change. The format of commit message should be `chore: va.b.c` which is the main version of current release.
107 |
108 | ```zsh
109 | git add .
110 | git commit -m "chore: va.b.c"
111 | ```
112 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "reduck-monorepo",
4 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.",
5 | "homepage": "https://modernjs.dev",
6 | "bugs": "https://github.com/modern-js-dev/reduck/issues",
7 | "repository": "modern-js-dev/reduck",
8 | "license": "MIT",
9 | "keywords": [
10 | "react",
11 | "framework",
12 | "modern",
13 | "modern.js",
14 | "state",
15 | "reduck"
16 | ],
17 | "scripts": {
18 | "new": "modern new",
19 | "setup": "npm run reset && pnpm install",
20 | "reset": "pnpm -r exec rm -rf node_modules",
21 | "lint": "modern lint",
22 | "change": "modern change",
23 | "bump": "modern bump",
24 | "pre": "modern pre",
25 | "release": "modern release",
26 | "prepare": "husky install",
27 | "build": "pnpm run --filter './packages/**' prepare",
28 | "preinstall": "only-allow-pnpm",
29 | "gen-release-note": "modern gen-release-note",
30 | "get-release-version": "cd scripts && pnpm run get-release-version"
31 | },
32 | "engines": {
33 | "node": ">=12.13.0"
34 | },
35 | "packageManager": "pnpm@8.6.1",
36 | "commitlint": {
37 | "extends": [
38 | "@commitlint/config-conventional"
39 | ]
40 | },
41 | "lint-staged": {
42 | "*.{ts,tsx}": [
43 | "pnpm exec eslint --fix --color --cache --quiet"
44 | ],
45 | "*.{js,jsx,mjs,mjsx,cjs,cjsx}": [
46 | "pnpm exec eslint --fix --color --cache --quiet"
47 | ]
48 | },
49 | "eslintConfig": {
50 | "extends": [
51 | "@modern-js"
52 | ],
53 | "ignorePatterns": [
54 | "dist",
55 | "lcov-report",
56 | "examples",
57 | "common"
58 | ]
59 | },
60 | "eslintIgnore": [
61 | "node_modules/",
62 | "dist/",
63 | "lib/",
64 | ".rpt2_cache/"
65 | ],
66 | "workspaces": {
67 | "packages": [
68 | "packages/*",
69 | "packages/**/*"
70 | ]
71 | },
72 | "devDependencies": {
73 | "@commitlint/cli": "^17.0.0",
74 | "@commitlint/config-conventional": "^17.0.0",
75 | "@modern-js-reduck/scripts": "workspace:*",
76 | "@modern-js/monorepo-tools": "2.21.1",
77 | "@modern-js/tsconfig": "2.21.1",
78 | "@modern-js/eslint-config": "2.21.1",
79 | "husky": "^8.0.0",
80 | "lint-staged": "^11.2.6",
81 | "webpack": "^5.54.0"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/packages/store/src/model/subscribe.ts:
--------------------------------------------------------------------------------
1 | import { Context, Model } from '@/types';
2 |
3 | export const GetUnsubscribe = Symbol('getUnsubscribe');
4 |
5 | const createSubscribe = (context: Context, model: Model) => {
6 | const mountedModel = context.apis.getModel(model);
7 |
8 | if (!mountedModel) {
9 | return null;
10 | }
11 |
12 | const { name } = mountedModel;
13 | let lastState = context.store.getState()[name];
14 | let unsubscribeStore: ReturnType;
15 | const handlers = new Set();
16 |
17 | const setupSubscribeStore = () => {
18 | // Already subscribed store
19 | if (unsubscribeStore) {
20 | return unsubscribeStore;
21 | }
22 |
23 | unsubscribeStore = context.store.subscribe(() => {
24 | const curState = context.store.getState()[name];
25 |
26 | if (lastState !== curState) {
27 | lastState = curState;
28 |
29 | handlers.forEach(handler => handler());
30 | }
31 | });
32 |
33 | return unsubscribeStore;
34 | };
35 |
36 | const ret = (handler: () => void) => {
37 | unsubscribeStore = setupSubscribeStore();
38 | handlers.add(handler);
39 |
40 | return () => {
41 | handlers.delete(handler);
42 | if (handlers.size === 0) {
43 | unsubscribeStore?.();
44 | unsubscribeStore = null;
45 | }
46 | };
47 | };
48 |
49 | // manually unsubscribe when model is unmounted
50 | ret[GetUnsubscribe] = () => unsubscribeStore;
51 | return ret;
52 | };
53 |
54 | const combineSubscribe = (
55 | context: Context,
56 | ...subscribes: ReturnType[]
57 | ) => {
58 | const { store } = context;
59 | let changed = false;
60 | const handlers = new Set();
61 |
62 | return (handler: () => void) => {
63 | handlers.add(handler);
64 |
65 | const disposer = [];
66 |
67 | subscribes.forEach(subscribe => {
68 | disposer.push(
69 | subscribe(() => {
70 | changed = true;
71 | }),
72 | );
73 | });
74 |
75 | const unsubscribeStore = store.subscribe(() => {
76 | if (changed) {
77 | changed = false;
78 | handlers.forEach(h => h());
79 | }
80 | });
81 |
82 | return () => {
83 | unsubscribeStore();
84 | disposer.forEach(dispose => dispose());
85 | };
86 | };
87 | };
88 |
89 | export { createSubscribe, combineSubscribe };
90 |
--------------------------------------------------------------------------------
/packages/store/src/store/createStore.ts:
--------------------------------------------------------------------------------
1 | import {
2 | applyMiddleware,
3 | compose,
4 | createStore as createReduxStore,
5 | type Action,
6 | type Reducer,
7 | type StoreEnhancer,
8 | type StoreEnhancerStoreCreator,
9 | } from 'redux';
10 | import { createContext } from './context';
11 | import type { Context, StoreConfig } from '@/types';
12 |
13 | const createStore = (props: StoreConfig = {}): Context['store'] => {
14 | const store: any = {};
15 | const context = createContext(store);
16 |
17 | // Load all available plugins
18 | props?.plugins?.forEach(plugin => context.pluginCore.usePlugin(plugin));
19 |
20 | const finalProps = context.pluginCore.invokePipeline('config', props);
21 |
22 | const {
23 | initialState = {},
24 | middlewares,
25 | enhancers = [],
26 | models = [],
27 | } = finalProps;
28 |
29 | Object.assign(
30 | store,
31 | createReduxStore(
32 | (state => state) as Reducer,
33 | initialState,
34 | compose>(
35 | ...[
36 | mergeInitialState(),
37 | middlewares ? applyMiddleware(...middlewares) : undefined,
38 | ...(enhancers || []),
39 | ].filter(Boolean),
40 | ),
41 | ),
42 | );
43 |
44 | store.use = context.apis.useModel;
45 | store.unmount = context.apis.unmountModel;
46 |
47 | if (models.length > 0) {
48 | store.use(models);
49 | }
50 |
51 | context.pluginCore.invokeWaterFall('afterCreateStore', store);
52 |
53 | return store;
54 | };
55 |
56 | /**
57 | * Merge prev global state when mounting new models
58 | * to avoid to miss the initial state of the mounting models
59 | */
60 | function mergeInitialState(): StoreEnhancer {
61 | return createStore => (reducer, initialState) => {
62 | const liftReducer = (r: Reducer) => {
63 | if (typeof r !== 'function') {
64 | throw new Error('Expected the reducer to be a function.');
65 | }
66 |
67 | return (state = initialState, action: Action) => {
68 | const nextState = r(state, action);
69 | if (/^@@redux\/REPLACE/.test(action.type)) {
70 | return { ...state, ...nextState };
71 | } else {
72 | return nextState;
73 | }
74 | };
75 | };
76 |
77 | const store = createStore(liftReducer(reducer));
78 |
79 | return {
80 | ...store,
81 | replaceReducer: reducer => {
82 | return store.replaceReducer(liftReducer(reducer));
83 | },
84 | };
85 | };
86 | }
87 |
88 | export default createStore;
89 |
--------------------------------------------------------------------------------
/packages/store/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modern-js-reduck/store",
3 | "version": "1.1.13",
4 | "files": [
5 | "dist"
6 | ],
7 | "jsnext:source": "./src/index.ts",
8 | "types": "./dist/types/index.d.ts",
9 | "main": "./dist/cjs/index.js",
10 | "module": "./dist/esm/index.js",
11 | "jsnext:modern": "./dist/js/modern/index.js",
12 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.",
13 | "homepage": "https://modernjs.dev",
14 | "bugs": "https://github.com/modern-js-dev/reduck/issues",
15 | "license": "MIT",
16 | "keywords": [
17 | "react",
18 | "framework",
19 | "modern",
20 | "modern.js",
21 | "state",
22 | "reduck",
23 | "store",
24 | "redux"
25 | ],
26 | "exports": {
27 | ".": {
28 | "types": "./dist/types/index.d.ts",
29 | "require": "./dist/cjs/index.js",
30 | "default": "./dist/esm/index.js"
31 | },
32 | "./types": {
33 | "types": "./dist/types/types/index.d.ts",
34 | "require": "./dist/cjs/types/index.js",
35 | "default": "./dist/esm/types/index.js"
36 | },
37 | "./utils": {
38 | "types": "./dist/types/utils/index.d.ts",
39 | "require": "./dist/cjs/utils/index.js",
40 | "default": "./dist/esm/utils/index.js"
41 | }
42 | },
43 | "typesVersions": {
44 | "*": {
45 | ".": [
46 | "./dist/types/index.d.ts"
47 | ],
48 | "types": [
49 | "./dist/types/types/index.d.ts"
50 | ],
51 | "utils": [
52 | "./dist/types/utils/index.d.ts"
53 | ]
54 | }
55 | },
56 | "scripts": {
57 | "prepare": "pnpm build",
58 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform",
59 | "new": "modern new",
60 | "build": "modern build",
61 | "test": "modern test",
62 | "test-type": "tsd"
63 | },
64 | "tsd": {
65 | "directory": "./src/__tsd__"
66 | },
67 | "dependencies": {
68 | "@swc/helpers": "0.5.1",
69 | "redux": "^4.1.1"
70 | },
71 | "devDependencies": {
72 | "@modern-js/module-tools": "2.21.1",
73 | "@modern-js/plugin-testing": "2.21.1",
74 | "@modern-js/runtime": "2.21.1",
75 | "@types/jest": "^27.5.1",
76 | "@types/node": "^14",
77 | "tsd": "latest",
78 | "typescript": "latest",
79 | "@modern-js-reduck/scripts": "workspace:*"
80 | },
81 | "modernSettings": {},
82 | "sideEffects": false,
83 | "publishConfig": {
84 | "registry": "https://registry.npmjs.org/",
85 | "access": "public"
86 | },
87 | "repository": "modern-js-dev/reduck"
88 | }
89 |
--------------------------------------------------------------------------------
/packages/store/tests/onMount.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore, model } from '../src';
2 |
3 | const createCountModel = (onMountCreator: (onMount: any, use: any) => void) =>
4 | model<{ value: number }>('count').define((_, { onMount, use }) => {
5 | onMountCreator(onMount, use);
6 |
7 | return {
8 | state: {
9 | value: 1,
10 | },
11 | actions: {
12 | addValue(state, value) {
13 | return {
14 | ...state,
15 | // FIXME: ESlint 校验时,无法正确获取参数 state 的类型信息,识别为 any
16 | // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
17 | value: state.value + value,
18 | };
19 | },
20 | },
21 | };
22 | });
23 |
24 | describe('test onMount hook', () => {
25 | test('onMount hook should invoked when model mounted', () => {
26 | const store = createStore();
27 | const fn = jest.fn();
28 | const onMountCreator = onMount => {
29 | onMount(() => {
30 | fn();
31 | });
32 | };
33 | const count = createCountModel(onMountCreator);
34 |
35 | store.use(count);
36 |
37 | expect(fn).toBeCalledTimes(1);
38 | });
39 |
40 | test('onMount hook should invoked only once when store.use model multiple times', () => {
41 | const store = createStore();
42 | const fn = jest.fn();
43 | const onMountCreator = onMount => {
44 | onMount(() => {
45 | fn();
46 | });
47 | };
48 | const count = createCountModel(onMountCreator);
49 |
50 | store.use(count);
51 | store.use(count);
52 | store.use(count);
53 | store.use(count);
54 |
55 | expect(fn).toBeCalledTimes(1);
56 | });
57 |
58 | test('through `use` to get newest state in onMount', () => {
59 | const store = createStore();
60 | const onMountCreator = (onMount, use) => {
61 | onMount(() => {
62 | const [state, actions] = use(count);
63 |
64 | expect(state).toEqual({ value: 1 });
65 |
66 | actions.addValue(1);
67 |
68 | expect(use(count)[0]).toEqual({ value: 2 });
69 | });
70 | };
71 | const count = createCountModel(onMountCreator);
72 | store.use(count);
73 | });
74 |
75 | test('actions should return correct value', () => {
76 | const store = createStore();
77 | const onMountCreator = (onMount, use) => {
78 | onMount(() => {
79 | const [, actions] = use(count);
80 |
81 | const result = actions.addValue(1);
82 |
83 | expect(result).toEqual({
84 | type: 'COUNT/ADDVALUE',
85 | payload: 1,
86 | extraArgs: [],
87 | });
88 | });
89 | };
90 | const count = createCountModel(onMountCreator);
91 | store.use(count);
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/packages/plugins/immer/tests/index.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/restrict-plus-operands */
2 | import { model, createStore } from '@modern-js-reduck/store';
3 | import immerPlugin from '../src';
4 |
5 | const count = model('count').define({
6 | state: {
7 | value: 1,
8 | },
9 | actions: {
10 | add(state) {
11 | state.value += 1;
12 | },
13 | pureAdd(state) {
14 | return {
15 | ...state,
16 | value: state.value + 1,
17 | };
18 | },
19 | },
20 | });
21 |
22 | const count2 = model('count2').define({
23 | state: {
24 | value: 1,
25 | },
26 | computed: {
27 | addOne: (state: any) => state.value + 1,
28 | sum: [count, (state: any, state2: any) => state.value + state2.value],
29 | },
30 | actions: {
31 | add(state) {
32 | state.value += 1;
33 | },
34 | pureAdd(state) {
35 | return {
36 | ...state,
37 | value: state.value + 1,
38 | };
39 | },
40 | },
41 | });
42 |
43 | describe('test immer', () => {
44 | let store: any;
45 | beforeEach(() => {
46 | store = createStore({
47 | plugins: [immerPlugin],
48 | });
49 | });
50 |
51 | test('mutable state state in action should work', () => {
52 | const [, actions, subscribe] = store.use(count);
53 | let stateUpdated = false;
54 |
55 | const unsubscribe = subscribe(() => {
56 | expect(store.use(count)[0]).toEqual({ value: 2 });
57 | stateUpdated = true;
58 | });
59 |
60 | actions.add();
61 | unsubscribe();
62 |
63 | expect(stateUpdated).toBe(true);
64 | });
65 |
66 | test('pure action should work', () => {
67 | const [, actions, subscribe] = store.use(count);
68 | let stateUpdated = false;
69 |
70 | const unsubscribe = subscribe(() => {
71 | expect(store.use(count)[0]).toEqual({ value: 2 });
72 | stateUpdated = true;
73 | });
74 |
75 | actions.pureAdd();
76 | unsubscribe();
77 |
78 | expect(stateUpdated).toBe(true);
79 | });
80 |
81 | test('computed properties should work', () => {
82 | const [, count1Actions] = store.use(count);
83 | const [, count2Actions, subscribe] = store.use(count2);
84 |
85 | count2Actions.pureAdd();
86 | const [count2State] = store.use(count2);
87 | expect(count2State.addOne).toEqual(3);
88 | expect(count2State.sum).toEqual(3);
89 |
90 | let stateUpdated = false;
91 | const unsubscribe = subscribe(() => {
92 | stateUpdated = true;
93 | });
94 |
95 | count1Actions.add();
96 | const [updateCount2State] = store.use(count2);
97 | expect(stateUpdated).toBe(true);
98 | expect(updateCount2State.addOne).toEqual(3);
99 | expect(updateCount2State.sum).toEqual(4);
100 |
101 | unsubscribe();
102 | });
103 | });
104 | /* eslint-enable @typescript-eslint/restrict-plus-operands */
105 |
--------------------------------------------------------------------------------
/packages/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@modern-js-reduck/react",
3 | "version": "1.1.13",
4 | "files": [
5 | "dist",
6 | "type.d.ts"
7 | ],
8 | "jsnext:source": "./src/index.ts",
9 | "types": "./type.d.ts",
10 | "main": "./dist/cjs/index.js",
11 | "module": "./dist/esm/index.js",
12 | "jsnext:modern": "./dist/js/modern/index.js",
13 | "description": "The meta-framework suite designed from scratch for frontend-focused modern web development.",
14 | "homepage": "https://modernjs.dev",
15 | "bugs": "https://github.com/modern-js-dev/reduck/issues",
16 | "license": "MIT",
17 | "keywords": [
18 | "react",
19 | "framework",
20 | "modern",
21 | "modern.js",
22 | "state",
23 | "reduck"
24 | ],
25 | "exports": {
26 | ".": {
27 | "types": "./type.d.ts",
28 | "require": "./dist/cjs/index.js",
29 | "default": "./dist/esm/index.js"
30 | }
31 | },
32 | "scripts": {
33 | "prepare": "pnpm build",
34 | "prepublishOnly": "only-allow-pnpm && pnpm build --platform",
35 | "new": "modern new",
36 | "build": "modern build",
37 | "test": "modern test"
38 | },
39 | "dependencies": {
40 | "@swc/helpers": "0.5.1",
41 | "@modern-js-reduck/store": "workspace:*",
42 | "@modern-js-reduck/plugin-effects": "workspace:*",
43 | "@modern-js-reduck/plugin-immutable": "workspace:*",
44 | "@modern-js-reduck/plugin-devtools": "workspace:*",
45 | "@modern-js-reduck/plugin-auto-actions": "workspace:*",
46 | "hoist-non-react-statics": "3.3.2",
47 | "invariant": "^2.2.4"
48 | },
49 | "devDependencies": {
50 | "@modern-js/module-tools": "2.21.1",
51 | "@modern-js/plugin-testing": "2.21.1",
52 | "@testing-library/jest-dom": "^5.16.5",
53 | "@testing-library/react": "^13.3.0",
54 | "@testing-library/react-12": "npm:@testing-library/react@^12",
55 | "@types/hoist-non-react-statics": "3.3.1",
56 | "@types/invariant": "^2.2.34",
57 | "@types/jest": "^27.5.1",
58 | "@types/node": "^14",
59 | "@types/react": "^18",
60 | "@types/react-dom": "^18",
61 | "@types/testing-library__jest-dom": "^5.14.1",
62 | "react": "^18.0.0",
63 | "react-dom": "^18.0.0",
64 | "react-17": "npm:react@^17",
65 | "react-dom-17": "npm:react-dom@^17",
66 | "redux": "^4.1.1",
67 | "typescript": "^4",
68 | "@modern-js-reduck/scripts": "workspace:*"
69 | },
70 | "modernSettings": {},
71 | "sideEffects": false,
72 | "peerDependencies": {
73 | "@types/react": "^16.8 || ^17.0 || ^18.0",
74 | "@types/react-dom": "^16.8 || ^17.0 || ^18.0",
75 | "react": "^16.8 || ^17.0 || ^18.0",
76 | "react-dom": "^16.8 || ^17.0 || ^18.0"
77 | },
78 | "peerDependenciesMeta": {
79 | "@types/react": {
80 | "optional": true
81 | },
82 | "@types/react-dom": {
83 | "optional": true
84 | }
85 | },
86 | "publishConfig": {
87 | "registry": "https://registry.npmjs.org/",
88 | "access": "public"
89 | },
90 | "repository": "modern-js-dev/reduck"
91 | }
92 |
--------------------------------------------------------------------------------
/packages/store/tests/subscribe.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore, model } from '../src';
2 |
3 | interface State {
4 | value: number;
5 | }
6 |
7 | const count1Model = model('count1').define({
8 | state: {
9 | value: 1,
10 | },
11 | actions: {
12 | add(state) {
13 | return {
14 | ...state,
15 | value: state.value + 1,
16 | };
17 | },
18 | },
19 | });
20 |
21 | const count2Model = model('count2').define({
22 | state: {
23 | value: 1,
24 | },
25 | computed: {
26 | sum: [
27 | count1Model,
28 | (state, state2) => {
29 | // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
30 | return state.value + state2.value;
31 | },
32 | ],
33 | },
34 | actions: {
35 | add1(state) {
36 | return {
37 | ...state,
38 | value: state.value + 1,
39 | };
40 | },
41 | },
42 | });
43 |
44 | describe('test subscribe', () => {
45 | test('subsribe should work for simple model', () => {
46 | const store = createStore();
47 |
48 | const [, actions, subscribe] = store.use(count1Model);
49 | const fn = jest.fn();
50 |
51 | subscribe(() => {
52 | fn();
53 | });
54 |
55 | actions.add();
56 |
57 | expect(fn).toBeCalledTimes(1);
58 | });
59 |
60 | test('subscribe should work for multiple model', () => {
61 | const store = createStore();
62 |
63 | const [, actions, subscribe] = store.use([count1Model, count2Model]);
64 |
65 | const fn = jest.fn();
66 |
67 | subscribe(() => {
68 | fn();
69 | });
70 |
71 | actions.add();
72 |
73 | expect(fn).toBeCalledTimes(1);
74 | expect(store.getState()).toEqual({
75 | count1: {
76 | value: 2,
77 | },
78 | count2: {
79 | value: 1,
80 | },
81 | });
82 |
83 | actions.add1();
84 |
85 | expect(fn).toBeCalledTimes(2);
86 |
87 | expect(store.getState()).toEqual({
88 | count1: {
89 | value: 2,
90 | },
91 | count2: {
92 | value: 2,
93 | },
94 | });
95 | });
96 |
97 | test('subscribe should work for computed property depending on other models', () => {
98 | const store = createStore();
99 |
100 | const [, action] = store.use(count1Model);
101 | const [state2, , subscribe] = store.use(count2Model);
102 |
103 | const fn = jest.fn();
104 | subscribe(() => {
105 | fn();
106 | });
107 |
108 | expect(fn).toBeCalledTimes(0);
109 | action.add();
110 | expect(fn).toBeCalledTimes(1);
111 | const [updateState2] = store.use(count2Model);
112 | // state from use is immutable
113 | expect(state2.sum).toBe(2);
114 | expect(updateState2.sum).toBe(3);
115 | });
116 |
117 | test('unsubscribe should work', () => {
118 | const store = createStore();
119 |
120 | const [, actions, subscribe] = store.use(count1Model);
121 | const fn = jest.fn();
122 |
123 | const unsubscribe = subscribe(() => {
124 | fn();
125 | });
126 |
127 | actions.add();
128 |
129 | expect(fn).toBeCalledTimes(1);
130 |
131 | unsubscribe();
132 |
133 | expect(fn).toBeCalledTimes(1);
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/packages/plugins/effects/tests/index.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore, model } from '@modern-js-reduck/store';
2 | import logger from 'redux-logger';
3 | import { plugin } from '../src';
4 |
5 | const todoModel = model('todo').define((_, { use }) => ({
6 | state: {
7 | items: [],
8 | },
9 | actions: {
10 | load: {
11 | fulfilled: (state: any, payload: any) => ({
12 | ...state,
13 | items: payload,
14 | }),
15 | },
16 | loadWithParams: {
17 | fulfilled: (state: any, payload: any) => ({
18 | ...state,
19 | items: payload,
20 | }),
21 | },
22 | },
23 | effects: {
24 | async load() {
25 | return Promise.resolve(['1']);
26 | },
27 |
28 | async loadWithParams(a: string) {
29 | return Promise.resolve([a]);
30 | },
31 |
32 | async boolRetEffect() {
33 | return Promise.resolve(false);
34 | },
35 |
36 | loadThunk() {
37 | const actions = use(todoModel)[1];
38 |
39 | // cannot get `dispatch` and `getState` params, thunk effect not work correctlly
40 | // maybe we could only support promise effect?
41 | return () => {
42 | actions.load.fulfilled(['2']);
43 | };
44 | },
45 |
46 | voidEffect() {
47 | // do some effect thing, for example: localStorage.setItem('hello', 'reduck');
48 | return 'success';
49 | },
50 | },
51 | }));
52 |
53 | describe('reduck effects plugin', () => {
54 | test('promise middleware', async () => {
55 | const store = createStore({
56 | plugins: [plugin],
57 | middlewares: [logger],
58 | });
59 |
60 | const [, actions] = store.use(todoModel);
61 |
62 | const res = await actions.load();
63 |
64 | expect(res).toEqual(['1']);
65 | expect(store.use(todoModel)[0]).toEqual({ items: ['1'] });
66 | });
67 |
68 | test('promise middleware params', async () => {
69 | const store = createStore({
70 | plugins: [plugin],
71 | middlewares: [logger],
72 | });
73 |
74 | const [, actions] = store.use(todoModel);
75 |
76 | await actions.loadWithParams('dddd');
77 |
78 | expect(store.use(todoModel)[0]).toEqual({ items: ['dddd'] });
79 | });
80 |
81 | test('thunk middleware', () => {
82 | const store = createStore({
83 | plugins: [plugin],
84 | middlewares: [logger],
85 | });
86 |
87 | const [, actions] = store.use(todoModel);
88 |
89 | actions.loadThunk();
90 |
91 | expect(store.use(todoModel)[0]).toEqual({ items: ['2'] });
92 | });
93 |
94 | test('void effect', () => {
95 | const store = createStore({
96 | plugins: [plugin],
97 | middlewares: [logger],
98 | });
99 |
100 | const [, actions] = store.use(todoModel);
101 |
102 | const res = actions.voidEffect();
103 |
104 | expect(res).toEqual('success');
105 | });
106 |
107 | test('promise effect return bool', async () => {
108 | const store = createStore({
109 | plugins: [plugin],
110 | middlewares: [logger],
111 | });
112 |
113 | const [, actions] = store.use(todoModel);
114 |
115 | const res = await actions.boolRetEffect();
116 |
117 | expect(res).toEqual(false);
118 | });
119 | });
120 |
--------------------------------------------------------------------------------
/packages/plugins/xstate/src/types/override.ts:
--------------------------------------------------------------------------------
1 | import { Model } from '@modern-js-reduck/store/types';
2 | import { Interpreter } from 'xstate/lib/interpreter';
3 | import {
4 | StateMachine,
5 | EventObject,
6 | MachineConfig,
7 | MachineOptions,
8 | } from 'xstate/lib/types';
9 |
10 | /**
11 | * override types of `reduck/core`'s states, actions
12 | */
13 | declare module '@modern-js-reduck/store' {
14 | // Add `machine` type when use model({machine}).
15 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
16 | interface ModelDesc {
17 | machine?:
18 | | StateMachine<
19 | MDO['machine'] extends { context: any }
20 | ? MDO['machine']['context']
21 | : any,
22 | any,
23 | MDO['machine'] extends { event: any } ? MDO['machine']['event'] : any
24 | >
25 | | MachineConfig<
26 | MDO['machine'] extends { context: any }
27 | ? MDO['machine']['context']
28 | : any,
29 | any,
30 | MDO['machine'] extends { event: any } ? MDO['machine']['event'] : any
31 | >;
32 | machineOptions?: Partial<
33 | MachineOptions<
34 | MDO['machine'] extends { context: any }
35 | ? MDO['machine']['context']
36 | : any,
37 | MDO['machine'] extends { event: any } ? MDO['machine']['event'] : any
38 | >
39 | >;
40 | }
41 |
42 | interface ModelDescOptions {
43 | machine?: {
44 | context?: any;
45 | event?: EventObject;
46 | };
47 | }
48 |
49 | interface GetState {
50 | machineState: {
51 | /**
52 | * choose context, meta, value of Interpreter State and whole State
53 | */
54 | machine: M['_']['machine'] extends StateMachine<
55 | infer TContext,
56 | infer TStateSchema,
57 | infer TEvent
58 | > // analyse StateMachine and MachineConfig
59 | ? Pick<
60 | Interpreter['state'],
61 | 'context' | 'meta' | 'value'
62 | > & {
63 | state: Interpreter['state'];
64 | }
65 | : M['_']['machine'] extends MachineConfig<
66 | infer TContext,
67 | any,
68 | infer TEvent
69 | >
70 | ? Pick<
71 | Interpreter['state'],
72 | 'context' | 'meta' | 'value'
73 | > & {
74 | state: Interpreter['state'];
75 | }
76 | : StateMachine;
77 | };
78 | }
79 |
80 | interface GetActions {
81 | machineActions: {
82 | /** machine send function */
83 | send: M['_']['machine'] extends StateMachine<
84 | infer TContext,
85 | infer TStateSchema,
86 | infer TEvent
87 | > // analyse StateMachine and MachineConfig
88 | ? Interpreter['send']
89 | : M['_']['machine'] extends MachineConfig<
90 | infer TContext,
91 | any,
92 | infer TEvent
93 | >
94 | ? Interpreter['send']
95 | : (event: any) => void;
96 | };
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/packages/react/src/batchManager.ts:
--------------------------------------------------------------------------------
1 | import { Store, Model, utils } from '@modern-js-reduck/store';
2 | import { unstable_batchedUpdates } from 'react-dom';
3 |
4 | const combineSubscribe = (
5 | store: Store,
6 | subscribes: ((handler: () => void) => () => void)[],
7 | ) => {
8 | let changed = false;
9 | const handlers = new Set();
10 |
11 | return (handler: () => void) => {
12 | handlers.add(handler);
13 |
14 | const disposer: any[] = [];
15 |
16 | subscribes.forEach(subscribe => {
17 | disposer.push(
18 | subscribe(() => {
19 | changed = true;
20 | }),
21 | );
22 | });
23 |
24 | const unsubscribeStore = store.subscribe(() => {
25 | if (changed) {
26 | handlers.forEach(h => h());
27 | }
28 |
29 | changed = false;
30 | });
31 |
32 | return () => {
33 | unsubscribeStore();
34 | disposer.forEach(dispose => dispose());
35 | };
36 | };
37 | };
38 |
39 | const createBatchManager = (store: Store) => {
40 | // Models are in using now
41 | const usingModelsMap = new Map();
42 |
43 | let unsubscribe: undefined | (() => void);
44 | const updateList: (() => void)[] = [];
45 |
46 | // listen to models in using
47 | const setupSubscribe = () => {
48 | if (typeof unsubscribe === 'function') {
49 | unsubscribe();
50 | }
51 |
52 | const modelSet = new Set();
53 |
54 | for (const [model, count] of usingModelsMap) {
55 | if (count !== 0) {
56 | modelSet.add(model);
57 | }
58 | }
59 |
60 | const subscribe = combineSubscribe(
61 | store,
62 | [...modelSet].map(m => store.use(m)[2]),
63 | );
64 |
65 | unsubscribe = subscribe(() => {
66 | unstable_batchedUpdates(() => {
67 | let update: (() => void) | undefined = updateList.shift();
68 |
69 | while (update) {
70 | update();
71 |
72 | update = updateList.shift();
73 | }
74 | });
75 | });
76 | };
77 |
78 | const changeModels = (action: 'remove' | 'add', ...models: Model[]) => {
79 | models.forEach(model => {
80 | if (!utils.isModel(model)) {
81 | return;
82 | }
83 |
84 | let usingCount = usingModelsMap.get(model);
85 |
86 | if (action === 'add') {
87 | usingModelsMap.set(model, (usingCount || 0) + 1);
88 | } else if (action === 'remove') {
89 | if (usingCount) {
90 | usingCount -= 1;
91 |
92 | if (usingCount === 0) {
93 | usingModelsMap.delete(model);
94 | } else {
95 | usingModelsMap.set(model, usingCount);
96 | }
97 | }
98 | }
99 | });
100 |
101 | setupSubscribe();
102 | };
103 |
104 | // add models to listen
105 | const addModels = (...args: Model[]) => changeModels('add', ...args);
106 |
107 | // remove models to listen
108 | const removeModels = (...args: Model[]) => changeModels('remove', ...args);
109 |
110 | const pushUpdate = (update: () => void) => {
111 | updateList.push(update);
112 | };
113 |
114 | return {
115 | addModels,
116 | removeModels,
117 | pushUpdate,
118 | };
119 | };
120 |
121 | export { createBatchManager };
122 |
--------------------------------------------------------------------------------
/packages/store/src/__tsd__/model.tsd.ts:
--------------------------------------------------------------------------------
1 | // import { expectType, expectAssignable } from 'tsd';
2 | // import { useModel } from '@modern-js/runtime/model';
3 | // import { model } from '..';
4 | // import { ReduxAction } from '@/types';
5 |
6 | // type StateManual = { count: number; name: 'a' | 'b' };
7 | // const counterManual = model('counter').define({
8 | // state: { count: 1 },
9 | // actions: {
10 | // add(state, n: number) {
11 | // expectType(state);
12 | // return { count: state.count + n, name: 'a' };
13 | // },
14 | // empty(state) {
15 | // expectType(state);
16 | // },
17 | // test: {
18 | // a(s) {
19 | // return s;
20 | // },
21 | // },
22 | // },
23 | // });
24 |
25 | // type StateInfer = { count: number; name: string };
26 | // const counterInfer = model('counter').define({
27 | // state: { count: 1, name: 'a' },
28 | // actions: {
29 | // add(state, n: number) {
30 | // expectType(state);
31 | // return { count: state.count + n, name: 'b' };
32 | // },
33 | // empty(state) {
34 | // expectType(state);
35 | // },
36 | // test: {
37 | // a(state) {
38 | // expectType(state);
39 | // return state;
40 | // },
41 | // },
42 | // },
43 | // });
44 |
45 | // describe('action and state manually type', () => {
46 | // expectType(counterManual.name);
47 |
48 | // expectAssignable<(s: StateManual, n: number) => StateManual>(
49 | // counterManual._.actions.add,
50 | // );
51 | // expectType<(s: StateManual) => void>(counterManual._.actions.empty);
52 | // const [state, actions] = useModel(counterManual);
53 | // expectType(state);
54 | // expectType<(n: number) => ReduxAction>(actions.add);
55 | // });
56 |
57 | // describe('action and state auto infer', () => {
58 | // expectType(counterInfer.name);
59 | // expectType<(s: StateInfer, n: number) => StateInfer>(
60 | // counterInfer._.actions.add,
61 | // );
62 | // expectType<(s: StateInfer) => void>(counterInfer._.actions.empty);
63 | // const [state, actions] = useModel(counterInfer);
64 | // expectType(state);
65 | // expectType<(n: number) => ReduxAction>(actions.add);
66 | // });
67 |
68 | // describe('action and state union type', () => {
69 | // const [state] = useModel(counterManual);
70 | // expectType<'a' | 'b'>(state.name);
71 | // });
72 |
73 | // describe('action and state function Initial', () => {
74 | // const counter = model('counter').define(() => ({
75 | // state: { c: 1 },
76 | // actions: {
77 | // add(state, payload: number) {
78 | // expectType(state.c);
79 | // return { c: state.c + payload };
80 | // },
81 | // test: {
82 | // a(s) {
83 | // return s;
84 | // },
85 | // b(s, p: number) {
86 | // return { ...s, c: s.c + p };
87 | // },
88 | // },
89 | // },
90 | // }));
91 | // const [state, actions] = useModel(counter);
92 | // expectType<(s: { c: number }, n: number) => { c: number }>(
93 | // counter._.actions.add,
94 | // );
95 | // expectType(state.c);
96 | // expectType<() => ReduxAction>(actions.test.a);
97 | // expectType<(n: number) => ReduxAction>(actions.test.b);
98 | // });
99 |
--------------------------------------------------------------------------------
/packages/store/src/model/model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ReduckContext,
3 | Context,
4 | ModelDesc,
5 | OnMountHook,
6 | Actions,
7 | Computed,
8 | } from '@/types';
9 | import { initializerSymbol } from '@/utils/misc';
10 |
11 | type ModelDescWithoutName = Omit, 'name'>;
12 |
13 | type ModelInitialParams = [
14 | context: ReduckContext,
15 | hook: {
16 | use: Context['apis']['useModel'];
17 | onMount: OnMountHook;
18 | },
19 | ];
20 |
21 | type ModelInitial = (...args: ModelInitialParams) => ModelDescWithoutName;
22 |
23 | type ExtDesc = {
24 | actions?: Actions;
25 | computed?: Computed;
26 | };
27 |
28 | type ModelFn = (
29 | name: string,
30 | ) => {
31 | define: (<
32 | S,
33 | M extends ExtDesc & { state: S } = ExtDesc & {
34 | state: S;
35 | },
36 | Resp = {
37 | _name: string;
38 | _: Omit & { state: State extends void ? S : State };
39 | },
40 | >(
41 | c: (...args: ModelInitialParams) => M & { state: S },
42 | ) => Resp &
43 | ((ns: string) => Resp & ((ns: string) => Resp)) & {
44 | state: State extends void ? S : State;
45 | }) &
46 | (<
47 | S,
48 | M extends ExtDesc & { state: S } = ExtDesc & {
49 | state: S;
50 | },
51 | Resp = {
52 | _name: string;
53 | _: Omit & { state: State extends void ? S : State };
54 | },
55 | >(
56 | c: M & { state: S },
57 | ) => Resp & {
58 | (ns: string): Resp & ((ns: string) => Resp);
59 | _name: string;
60 | _: Omit & { state: State extends void ? S : State };
61 | });
62 | };
63 | const model: ModelFn = name => ({
64 | define(modelDesc) {
65 | let modelInitializer: ModelInitial;
66 |
67 | if (typeof modelDesc === 'function') {
68 | modelInitializer = modelDesc;
69 | } else {
70 | modelInitializer = () => modelDesc;
71 | }
72 |
73 | const modelCache = new Map>();
74 |
75 | const createResponse = (initializer: ModelInitial) => {
76 | /**
77 | * Use to change model namespace when mount model
78 | * @example
79 | * use(someModel('hello'))
80 | */
81 | const response = (namespace: string) => {
82 | const cachedModel = modelCache.get(namespace);
83 |
84 | if (cachedModel) {
85 | return cachedModel;
86 | }
87 |
88 | const clonedModelInitializer = (...args: [Context, any]) => {
89 | const result = initializer(...args);
90 |
91 | return result;
92 | };
93 |
94 | const modelInstance = createResponse(clonedModelInitializer);
95 | modelCache.set(namespace, modelInstance);
96 | modelInstance._name = namespace || name;
97 |
98 | return modelInstance;
99 | };
100 |
101 | response._name = name;
102 |
103 | Object.defineProperty(response, initializerSymbol, {
104 | configurable: false,
105 | enumerable: false,
106 | value: initializer,
107 | });
108 |
109 | return response as any;
110 | };
111 |
112 | return createResponse(modelInitializer);
113 | },
114 | });
115 |
116 | export default model;
117 |
--------------------------------------------------------------------------------
/packages/store/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @modern-js-reduck/store
2 |
3 | ## 1.1.13
4 |
5 | ### Patch Changes
6 |
7 | - d5d3a2c: fix: should run prepare before publish
8 |
9 | ## 1.1.12
10 |
11 | ## 1.1.11
12 |
13 | ### Patch Changes
14 |
15 | - 1b6f119: chore: update build tools and build config, reduck product size.
16 | chore: 更新构建工具和配置,减少产物体积
17 | - 3979805: fix: add node to package.json exports
18 | - 96afcec: fix: correct exports
19 |
20 | ## 1.1.10
21 |
22 | ### Patch Changes
23 |
24 | - 8b81fc9: fix: fix initial state missing
25 |
26 | ## 1.1.9
27 |
28 | ### Patch Changes
29 |
30 | - d5d5f8b: chore: add subpath exports "types" in @modern-js-reduck/store
31 | chore: 添加 @modern-js-reduck/store 的 types 子路径导出
32 |
33 | ## 1.1.8
34 |
35 | ## 1.1.7
36 |
37 | ### Patch Changes
38 |
39 | - 856a79d: fix: add the types field in package.json
40 | fix: 在 package.json 中添加 types 字段
41 | - 1b6773e: feat: remove node entry in all pkg package.json
42 |
43 | ## 1.1.6
44 |
45 | ### Patch Changes
46 |
47 | - 4a4e8925: fix: fix computed type
48 |
49 | ## 1.1.5
50 |
51 | ## 1.1.4
52 |
53 | ### Patch Changes
54 |
55 | - chore: modify export types
56 |
57 | ## 1.1.3
58 |
59 | ## 1.1.2
60 |
61 | ### Patch Changes
62 |
63 | - feat: export util functions from main entry
64 |
65 | ## 1.1.1
66 |
67 | ### Patch Changes
68 |
69 | - chore: release v1.1.1
70 |
71 | ## 1.1.0
72 |
73 | ## 1.0.6
74 |
75 | ### Patch Changes
76 |
77 | - feat: uniform package version
78 |
79 | ## 1.0.5
80 |
81 | ### Patch Changes
82 |
83 | - ac05b04: fix: typos
84 | - 30b1167: fix: fix action return value
85 | - ed80d6e: build: build modern js dist
86 | - b33f42a: feat: computed properties
87 | - 9c09d2a: fix: fix `connect` missing component props
88 | - 51721bc: feat: `handleEffect` API
89 |
90 | ## 1.0.4
91 |
92 | ### Patch Changes
93 |
94 | - 2b7d19f: fix: merge actions bug when use mutiple models
95 |
96 | ## 1.0.1
97 |
98 | ### Patch Changes
99 |
100 | - fix: fix git url
101 |
102 | ## 1.0.0
103 |
104 | ### Minor Changes
105 |
106 | - feat: ready for publish 1.0.0
107 |
108 | ### Patch Changes
109 |
110 | - a7bd932: release: rc.7
111 | - 26d5144: fix(type): actions can return void
112 | - 9452589: fix: ts type
113 | - a7bd932: fix: use model State type
114 | - fd81b6b: feat: useLocalModel support
115 | - feat: ready for publish 1.0.0
116 | - a7bd932: fix: model type
117 | - a7bd932: fix: model when State passed use State
118 |
119 | ## 1.0.0-rc.10
120 |
121 | ### Patch Changes
122 |
123 | - a7bd932: release: rc.7
124 | - 26d5144: fix(type): actions can return void
125 | - 9452589: fix: ts type
126 | - a7bd932: fix: use model State type
127 | - feat: useLocalModel support
128 | - a7bd932: fix: model type
129 | - a7bd932: fix: model when State passed use State
130 |
131 | ## 1.0.0-rc.9
132 |
133 | ### Patch Changes
134 |
135 | - ac4124f: release: rc.7
136 | - 26d5144: fix(type): actions can return void
137 | - 9452589: fix: ts type
138 | - 1085d2d: fix: use model State type
139 | - fix: model type
140 | - 1085d2d: fix: model when State passed use State
141 |
142 | ## 1.0.0-rc.8
143 |
144 | ### Patch Changes
145 |
146 | - release: rc.7
147 | - 26d5144: fix(type): actions can return void
148 | - 9452589: fix: ts type
149 | - 1085d2d: fix: use model State type
150 | - 1085d2d: fix: model when State passed use State
151 |
152 | ## 1.0.0-next.7
153 |
154 | ### Patch Changes
155 |
156 | - 26d5144: fix(type): actions can return void
157 | - 9452589: fix: ts type
158 | - fix: use model State type
159 | - fix: model when State passed use State
160 |
161 | ## 1.0.0-rc.6
162 |
163 | ### Patch Changes
164 |
165 | - fix(type): actions can return void
166 | - 9452589: fix: ts type
167 |
168 | ## 1.0.0-rc.5
169 |
170 | ### Patch Changes
171 |
172 | - fix: ts type
173 |
--------------------------------------------------------------------------------
/packages/plugins/effects/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import { createPlugin } from '@modern-js-reduck/store';
2 | import { Model } from '@modern-js-reduck/store/types';
3 | import { createPromise } from 'redux-promise-middleware';
4 |
5 | type AsyncEffect = (...args: any[]) => Promise;
6 | type VoidEffect = (...args: any[]) => void;
7 | type ThunkEffect = (...args: any[]) => () => any;
8 |
9 | interface Effects {
10 | [key: string]: AsyncEffect | VoidEffect | ThunkEffect | Effects;
11 | }
12 |
13 | declare module '@modern-js-reduck/store' {
14 | // Add `effects` type when use model({effects}).
15 | interface ModelDesc {
16 | effects?: Effects;
17 | }
18 |
19 | // Overload GetActions interface to add actions type to useModel's return
20 | interface GetActions {
21 | effectsActions: M['_']['effects'] & {
22 | [key in keyof M['_']['actions']]: unknown;
23 | };
24 | }
25 | }
26 |
27 | const isReduxPromiseFulfilled = (data: any) => {
28 | return (
29 | typeof data === 'object' &&
30 | data.hasOwnProperty('action') &&
31 | data.hasOwnProperty('value')
32 | );
33 | };
34 |
35 | const isPromise = (value: any) => {
36 | if (value !== null && typeof value === 'object') {
37 | return value && typeof value.then === 'function';
38 | }
39 |
40 | return false;
41 | };
42 |
43 | /**
44 | * Generate dispatch action from effects definitions.
45 | */
46 | const createDispatchActionsFromEffects = (
47 | store: any,
48 | name: string,
49 | effects: Effects,
50 | setDispatchAction: (path: string[], action: any) => void,
51 | ) => {
52 | const path = [name];
53 |
54 | const traverse = (_effects: Effects[string]) => {
55 | if (typeof _effects === 'function') {
56 | const type = path.join('/').toUpperCase();
57 |
58 | setDispatchAction(path.slice(), (...args: any[]) => {
59 | const value = (_effects as (..._args: any[]) => any)(...args);
60 | const dispatch = (payload: any) =>
61 | store.dispatch({
62 | type,
63 | payload,
64 | });
65 |
66 | // Handled by promise middleware or redux thunk
67 | // Otherwise, do not dispatch action, just exec the effect function.
68 | if (isPromise(value) || typeof value === 'function') {
69 | const res = dispatch(value);
70 | if (isPromise(res)) {
71 | // parse redux-promise result, return orginal value of the effect
72 | return res.then((data: any) =>
73 | isReduxPromiseFulfilled(data) ? data.value : data,
74 | );
75 | }
76 | return res;
77 | }
78 |
79 | return value;
80 | });
81 | } else {
82 | Object.keys(_effects).forEach(key => {
83 | path.push(key);
84 | traverse(_effects[key]);
85 | path.pop();
86 | });
87 | }
88 | };
89 |
90 | traverse(effects);
91 | };
92 |
93 | const plugin = createPlugin(context => ({
94 | config(storeConfig) {
95 | return {
96 | ...storeConfig,
97 | middlewares: [
98 | createPromise({ promiseTypeDelimiter: '/' }),
99 | // middlewares from config are at the end
100 | ...(storeConfig.middlewares || []),
101 | ],
102 | };
103 | },
104 | modelMount({ modelDesc, mountedModel }, { setDispatchAction }) {
105 | const { effects } = modelDesc;
106 |
107 | if (!effects) {
108 | return {
109 | modelDesc,
110 | mountedModel,
111 | };
112 | }
113 |
114 | createDispatchActionsFromEffects(
115 | context.store,
116 | modelDesc.name,
117 | modelDesc.effects!,
118 | setDispatchAction,
119 | );
120 |
121 | return {
122 | modelDesc,
123 | mountedModel,
124 | } as any;
125 | },
126 | }));
127 |
128 | export default plugin;
129 |
--------------------------------------------------------------------------------
/packages/store/tests/useModel.test.ts:
--------------------------------------------------------------------------------
1 | import { createStore, model } from '../src';
2 |
3 | const count1Model = model<{ value: number }>('count1').define(() => ({
4 | state: {
5 | value: 1,
6 | },
7 | actions: {
8 | add(state) {
9 | return {
10 | ...state,
11 | value: state.value + 1,
12 | };
13 | },
14 | },
15 | }));
16 |
17 | const count2Model = model<{ value: number }>('count2').define((_, { use }) => {
18 | use(count1Model);
19 |
20 | return {
21 | state: {
22 | value: 1,
23 | },
24 | actions: {
25 | addCount1Value(state) {
26 | const [{ value: count1Value }] = use(count1Model);
27 |
28 | return {
29 | ...state,
30 | value: state.value + count1Value,
31 | };
32 | },
33 | },
34 | };
35 | });
36 |
37 | const count3Model = model<{ value: number }>('count3').define(() => {
38 | return {
39 | state: {
40 | value: 1,
41 | },
42 | computed: {
43 | addOne: state => state.value + 1,
44 | },
45 | };
46 | });
47 |
48 | describe('test useModel', () => {
49 | test('actions should work', () => {
50 | const store = createStore();
51 |
52 | const [, actions] = store.use(count2Model);
53 | const [, count1Actions] = store.use(count1Model);
54 |
55 | expect(store.getState()).toEqual({
56 | count1: { value: 1 },
57 | count2: { value: 1 },
58 | });
59 |
60 | actions.addCount1Value();
61 |
62 | expect(store.getState()).toEqual({
63 | count1: { value: 1 },
64 | count2: { value: 2 },
65 | });
66 |
67 | count1Actions.add();
68 |
69 | expect(store.getState()).toEqual({
70 | count1: { value: 2 },
71 | count2: { value: 2 },
72 | });
73 |
74 | actions.addCount1Value();
75 |
76 | expect(store.getState()).toEqual({
77 | count1: { value: 2 },
78 | count2: { value: 4 },
79 | });
80 | });
81 |
82 | test('state reference is same, when passing same single model without computed properties', () => {
83 | const store = createStore();
84 |
85 | const [count1State] = store.use(count1Model);
86 | const [newCount1State] = store.use(count1Model);
87 | expect(count1State).toBe(newCount1State);
88 | });
89 |
90 | test('state reference changes, when passing multiple models', () => {
91 | const store = createStore();
92 |
93 | const [state] = store.use([count1Model, count2Model]);
94 | const [state2] = store.use([count1Model, count2Model]);
95 | expect(state).not.toBe(state2);
96 | expect(state).toEqual(state2);
97 | });
98 |
99 | test('state reference changes, when passing model with computed properties', () => {
100 | const store = createStore();
101 |
102 | const [count3State] = store.use(count3Model);
103 | const [newCount3State] = store.use(count3Model);
104 | expect(count3State).not.toBe(newCount3State);
105 | expect(count3State).toEqual(newCount3State);
106 | });
107 |
108 | test('use models with same name would be ignored', () => {
109 | const store = createStore();
110 | const countModel = model('count1').define({
111 | state: 2,
112 | });
113 |
114 | const [state] = store.use(count1Model, countModel);
115 |
116 | expect(store.getState()).toEqual({ count1: { value: 1 } });
117 | expect(state).toEqual({ value: 1 });
118 | });
119 |
120 | test('use self in model will get error', () => {
121 | const test = model('name').define((_, { use }) => {
122 | use(test);
123 |
124 | return {
125 | state: 1,
126 | };
127 | });
128 |
129 | expect(() => createStore().use(test)).toThrow(Error);
130 | });
131 |
132 | test('use multiple model with primitive state get error', () => {
133 | const test = model('name').define({
134 | state: 1,
135 | });
136 |
137 | expect(() => createStore().use(test, count1Model)).toThrow(Error);
138 | });
139 | });
140 |
--------------------------------------------------------------------------------
/packages/store/src/store/context.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers, Reducer, Store } from 'redux';
2 | import { createUseModel } from '@/model/useModel';
3 | import { Context, Model, MountedModel } from '@/types';
4 | import { createPluginCore } from '@/plugin';
5 | import { createSubscribe, GetUnsubscribe } from '@/model/subscribe';
6 |
7 | const dummyReducer = '__REDUCK_DUMMY_REDUCER__';
8 |
9 | const createContext = (store: Store) => {
10 | const reducers: Record = {};
11 | const mountedModels = new Map();
12 | const subscriptions = new Map>();
13 | const mountingModelNames = new Set();
14 | let lastState: any;
15 |
16 | /**
17 | * Dynamic add reducer to store
18 | */
19 | const addReducers: Context['apis']['addReducers'] = _reducers => {
20 | if (!lastState) {
21 | store.subscribe(() => {
22 | lastState = store.getState();
23 | });
24 | }
25 |
26 | // remove dummy reducer we may add when unmountting a model
27 | if (reducers[dummyReducer]) {
28 | delete reducers[dummyReducer];
29 | }
30 |
31 | Object.assign(reducers, _reducers);
32 | Object.keys(_reducers).forEach(key => mountingModelNames.delete(key));
33 | store.replaceReducer(combineReducers(reducers));
34 | };
35 |
36 | /**
37 | * Add to exported models
38 | */
39 | const addModel: Context['apis']['addModel'] = (model, mountedModel) => {
40 | mountedModels.set(model._name, mountedModel);
41 | subscriptions.set(model._name, createSubscribe(context, model));
42 | };
43 |
44 | const getModel: Context['apis']['getModel'] = model => {
45 | const mountedModel = getModelByName(model._name);
46 |
47 | if (!mountedModel) {
48 | return null;
49 | }
50 |
51 | return {
52 | name: mountedModel.name,
53 | state: lastState[mountedModel.name],
54 | actions: mountedModel.actions,
55 | modelDesc: mountedModel.modelDesc,
56 | } as any;
57 | };
58 |
59 | const getModelByName = (name: string) => {
60 | let model = null;
61 |
62 | for (const [, mountedModel] of mountedModels) {
63 | if (mountedModel.name === name) {
64 | model = mountedModel;
65 | break;
66 | }
67 | }
68 |
69 | return model;
70 | };
71 |
72 | // Get function to subscribe model
73 | const getModelSubscribe: Context['apis']['getModelSubscribe'] = (
74 | model: Model,
75 | ) => subscriptions.get(model._name);
76 |
77 | const mountingModel = (name: string) => {
78 | if (mountingModelNames.has(name)) {
79 | throw new Error(
80 | `You are mounting the model: ${name} which is already in mounting process`,
81 | );
82 | }
83 |
84 | mountingModelNames.add(name);
85 | };
86 |
87 | const unmountModel = (model: Model) => {
88 | if (!getModel(model)) {
89 | return;
90 | }
91 |
92 | const subscription = subscriptions.get(model._name);
93 | subscription[GetUnsubscribe]()?.();
94 |
95 | mountedModels.delete(model._name);
96 | subscriptions.delete(model._name);
97 |
98 | delete lastState[model._name];
99 | delete reducers[model._name];
100 |
101 | // redux cannot accept empty reducers, so we fake one.
102 | if (Object.keys(reducers).length === 0) {
103 | reducers[dummyReducer] = () => {
104 | return null;
105 | };
106 | }
107 |
108 | store.replaceReducer(combineReducers(reducers));
109 | };
110 |
111 | const pluginCore = createPluginCore({ store });
112 |
113 | /**
114 | * Add all to context
115 | */
116 | const context = {
117 | store,
118 | apis: {
119 | addReducers,
120 | addModel,
121 | getModel,
122 | getModelSubscribe,
123 | mountingModel,
124 | unmountModel,
125 | },
126 | pluginCore,
127 | } as Context;
128 |
129 | context.apis.useModel = createUseModel(context);
130 |
131 | return context;
132 | };
133 |
134 | export { createContext };
135 |
--------------------------------------------------------------------------------
/packages/react/tests/batch.test.tsx:
--------------------------------------------------------------------------------
1 | import { model } from '@modern-js-reduck/store';
2 | import { render, fireEvent, act, screen } from '@testing-library/react';
3 | import '@testing-library/jest-dom/extend-expect';
4 | import { useModel, Provider } from '../src';
5 |
6 | const countModel = model('name').define({
7 | state: {
8 | value: 1,
9 | value1: 1,
10 | },
11 | actions: {
12 | addValue(state) {
13 | return {
14 | ...state,
15 | value: state.value + 1,
16 | };
17 | },
18 | addValue1(state) {
19 | return {
20 | ...state,
21 | value1: state.value1 + 1,
22 | };
23 | },
24 | },
25 | });
26 |
27 | describe('test batch', () => {
28 | test('once store change, update should batch in one render', async () => {
29 | let renderCount = 0;
30 |
31 | function SubApp() {
32 | renderCount += 1;
33 | const [{ value1 }, { addValue1 }] = useModel(countModel);
34 |
35 | return (
36 | <>
37 | value1:{value1}
38 | {
40 | // React 17 not auto batch updates in setTimeout.
41 | // FIXME: In test environment, React 17 seems to always batch updates.
42 | setTimeout(() => {
43 | act(() => {
44 | addValue1();
45 | });
46 | }, 10);
47 | }}
48 | >
49 | addValue1
50 |
51 | >
52 | );
53 | }
54 |
55 | function App() {
56 | const [{ value }, { addValue }] = useModel(countModel);
57 | return (
58 |
59 |
value:{value}
60 |
addValue()}>addValue
61 |
62 |
63 | );
64 | }
65 |
66 | const result = render(
67 |
68 |
69 | ,
70 | );
71 |
72 | expect(renderCount).toBe(1);
73 | fireEvent.click(result.getByText('addValue'));
74 | expect(renderCount).toBe(2);
75 | expect(result.getByText('value:2')).toBeInTheDocument();
76 |
77 | fireEvent.click(result.getByText('addValue1'));
78 |
79 | await screen.findByText('value1:2');
80 | expect(renderCount).toBe(3);
81 | });
82 |
83 | test('state selector should reduce the rerender times', () => {
84 | let parentRenderCount = 0;
85 | let childRenderCount = 0;
86 |
87 | function SubApp() {
88 | childRenderCount += 1;
89 |
90 | const [{ value1 }, { addValue1 }] = useModel(countModel, state => ({
91 | value1: state.value1,
92 | }));
93 |
94 | return (
95 | <>
96 | value1:{value1}
97 | {
99 | addValue1();
100 | }}
101 | >
102 | addValue1
103 |
104 | >
105 | );
106 | }
107 |
108 | function App() {
109 | parentRenderCount += 1;
110 | const [{ value }, { addValue }] = useModel(countModel, state => ({
111 | value: state.value,
112 | }));
113 |
114 | return (
115 |
116 |
value:{value}
117 |
addValue()}>addValue
118 |
119 |
120 | );
121 | }
122 |
123 | const result = render(
124 |
125 |
126 | ,
127 | );
128 |
129 | expect(parentRenderCount).toBe(1);
130 | expect(childRenderCount).toBe(1);
131 |
132 | fireEvent.click(result.getByText('addValue'));
133 | expect(parentRenderCount).toBe(2);
134 | expect(childRenderCount).toBe(2);
135 |
136 | fireEvent.click(result.getByText('addValue1'));
137 | expect(parentRenderCount).toBe(2);
138 | expect(childRenderCount).toBe(3);
139 |
140 | expect(result.getByText('value:2')).toBeInTheDocument();
141 | expect(result.getByText('value1:2')).toBeInTheDocument();
142 | });
143 | });
144 |
--------------------------------------------------------------------------------
/packages/plugins/xstate/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import { createPlugin } from '@modern-js-reduck/store';
2 | import type { EventData, SCXML, SingleOrArray } from 'xstate';
3 | import { isMachineModel } from './check';
4 | import { ActionTypes, MachineActionPrefix, MachineStateSymbol } from './const';
5 | import { mergeMachineMap } from './map';
6 | import type { MachineMap } from './types';
7 | import { getEventType } from './utils';
8 |
9 | const machineMap: MachineMap = {};
10 |
11 | const plugin = createPlugin(context => ({
12 | /** modify modelDesc */
13 | prepareModelDesc(modelDesc) {
14 | // don't change modelDesc if no 'machine' in modelDesc
15 | if (!isMachineModel(modelDesc)) {
16 | return modelDesc;
17 | }
18 |
19 | // guarantee same id between machine and modelDesc
20 | if (modelDesc.machine.id && modelDesc.machine.id !== modelDesc.name) {
21 | console.warn(
22 | 'Unexpected machine id is not consistent with model name, it would be changed to model name',
23 | );
24 | }
25 |
26 | modelDesc.machine.id = modelDesc.name;
27 |
28 | function setAction(state: any) {
29 | return {
30 | ...state,
31 | [MachineStateSymbol]: '',
32 | };
33 | }
34 |
35 | return {
36 | ...modelDesc,
37 | state: {
38 | ...modelDesc.state,
39 | },
40 | actions: {
41 | ...modelDesc.actions,
42 | [MachineActionPrefix]: {
43 | [ActionTypes.SET]: setAction,
44 | },
45 | },
46 | };
47 | },
48 | modelMount({ modelDesc, mountedModel }) {
49 | if (!isMachineModel(modelDesc)) {
50 | return { modelDesc, mountedModel };
51 | }
52 |
53 | mergeMachineMap(machineMap, modelDesc);
54 |
55 | return { modelDesc, mountedModel } as any;
56 | },
57 | useModel(bypassParams, { mountedModels }) {
58 | if (mountedModels.length !== 1) {
59 | const hasMachineModel = mountedModels.some(mountedModel =>
60 | isMachineModel(mountedModel.modelDesc),
61 | );
62 |
63 | if (hasMachineModel) {
64 | throw new Error(
65 | 'model.machine not support array parameter for useModel currently.',
66 | );
67 | }
68 |
69 | return bypassParams;
70 | }
71 |
72 | const { modelDesc } = mountedModels[0];
73 |
74 | // don't change modelDesc if no 'machine' in modelDesc
75 | if (!isMachineModel(modelDesc)) {
76 | return bypassParams;
77 | }
78 |
79 | const machine = machineMap[modelDesc.name];
80 |
81 | if (!machine) {
82 | throw new Error(
83 | `Unexpected no machine service for model <${modelDesc.name}>`,
84 | );
85 | }
86 |
87 | const machineState = {
88 | machine: {
89 | context: machine.service.state.context,
90 | meta: machine.service.state.meta,
91 | value: machine.service.state.value,
92 | state: machine.service.state,
93 | },
94 | };
95 |
96 | delete bypassParams.state[MachineStateSymbol];
97 |
98 | const state = {
99 | ...bypassParams.state,
100 | ...machineState,
101 | };
102 |
103 | const sendAction = (
104 | event: SingleOrArray | SCXML.Event,
105 | payload?: EventData | undefined,
106 | ) => {
107 | const sendType = getEventType(modelDesc.name, ActionTypes.SEND);
108 | const setType = getEventType(modelDesc.name, ActionTypes.SET);
109 |
110 | context.store.dispatch({
111 | type: sendType,
112 | payload: { event, payload },
113 | });
114 | const results = machine.service.send(event, payload);
115 |
116 | context.store.dispatch({ type: setType, payload: results });
117 | return results;
118 | };
119 |
120 | // remove internal machine action
121 | delete bypassParams.actions[MachineActionPrefix];
122 |
123 | const actions: Record = {
124 | ...bypassParams.actions,
125 | send: sendAction,
126 | };
127 |
128 | return {
129 | ...bypassParams,
130 | state,
131 | actions,
132 | };
133 | },
134 | }));
135 |
136 | export default plugin;
137 |
--------------------------------------------------------------------------------
/packages/store/tests/model.test.ts:
--------------------------------------------------------------------------------
1 | import { model, createStore } from '../src';
2 |
3 | interface State {
4 | value: number;
5 | }
6 |
7 | const countModel = model('counter').define({
8 | state: {
9 | value: 1,
10 | },
11 | actions: {
12 | add(state: State) {
13 | return {
14 | ...state,
15 | value: state.value + 1,
16 | };
17 | },
18 | },
19 | });
20 |
21 | const count2Model = model('counter2').define({
22 | state: {
23 | value: 1,
24 | },
25 | actions: {
26 | add2(state: State) {
27 | return {
28 | ...state,
29 | value: state.value + 1,
30 | };
31 | },
32 | },
33 | });
34 |
35 | let store = createStore();
36 |
37 | describe('test model', () => {
38 | beforeEach(() => {
39 | store = createStore();
40 | });
41 |
42 | test('model can be used by store', () => {
43 | store.use(countModel);
44 | expect(store.getState()).toEqual({
45 | counter: {
46 | value: 1,
47 | },
48 | });
49 | });
50 |
51 | test('model(name) will return a new model', () => {
52 | store.use(countModel('counter1'));
53 | expect(store.getState()).toEqual({
54 | counter1: {
55 | value: 1,
56 | },
57 | });
58 |
59 | store.use(countModel('counter1'))[1].add();
60 | expect(store.getState()).toEqual({
61 | counter1: {
62 | value: 2,
63 | },
64 | });
65 | });
66 |
67 | test(`someModel('a') and someModel('a') should return same reference model`, () => {
68 | expect(countModel('1')).toBe(countModel('1'));
69 | });
70 |
71 | test('unmount model when store only has mounted single model', () => {
72 | const [, actions, subscribe] = store.use(countModel);
73 | const mockFn = jest.fn();
74 | subscribe(mockFn);
75 |
76 | actions.add();
77 | expect(store.getState()).toEqual({
78 | counter: {
79 | value: 2,
80 | },
81 | });
82 | expect(mockFn).toBeCalledTimes(1);
83 |
84 | store.unmount(countModel);
85 | expect(store.getState().counter).toBeUndefined();
86 |
87 | mockFn.mockClear();
88 | actions.add();
89 | expect(mockFn).toBeCalledTimes(0);
90 |
91 | const [, newActions, newSubscribe] = store.use(countModel);
92 | newSubscribe(mockFn);
93 | newActions.add();
94 | expect(mockFn).toBeCalledTimes(1);
95 | expect(store.getState()).toEqual({
96 | counter: {
97 | value: 2,
98 | },
99 | });
100 | });
101 |
102 | test('unmount model when store has mounted multiple models', () => {
103 | const [, actions, subscribe] = store.use(countModel);
104 | const [, count2Actions, count2Subscribe] = store.use(count2Model);
105 |
106 | const countCbFn = jest.fn();
107 | const count2CbFn = jest.fn();
108 | subscribe(countCbFn);
109 | count2Subscribe(count2CbFn);
110 |
111 | actions.add();
112 | expect(store.getState()).toEqual({
113 | counter: {
114 | value: 2,
115 | },
116 | counter2: {
117 | value: 1,
118 | },
119 | });
120 | expect(countCbFn).toBeCalledTimes(1);
121 | expect(count2CbFn).toBeCalledTimes(0);
122 |
123 | store.unmount(countModel);
124 | expect(store.getState().counter).toBeUndefined();
125 |
126 | count2Actions.add2();
127 | expect(count2CbFn).toBeCalledTimes(1);
128 |
129 | expect(store.getState()).toEqual({
130 | counter2: {
131 | value: 2,
132 | },
133 | });
134 |
135 | countCbFn.mockClear();
136 | count2CbFn.mockClear();
137 |
138 | actions.add();
139 | expect(countCbFn).toBeCalledTimes(0);
140 | expect(count2CbFn).toBeCalledTimes(0);
141 |
142 | const [, newActions, newSubscribe] = store.use(countModel);
143 | newSubscribe(countCbFn);
144 | newActions.add();
145 | expect(countCbFn).toBeCalledTimes(1);
146 | expect(count2CbFn).toBeCalledTimes(0);
147 |
148 | count2Actions.add2();
149 | expect(count2CbFn).toBeCalledTimes(1);
150 |
151 | expect(store.getState()).toEqual({
152 | counter: {
153 | value: 2,
154 | },
155 | counter2: {
156 | value: 3,
157 | },
158 | });
159 | });
160 | });
161 |
--------------------------------------------------------------------------------
/packages/store/src/model/useModel.ts:
--------------------------------------------------------------------------------
1 | import mountModel from './mountModel';
2 | import { combineSubscribe } from './subscribe';
3 | import { Context, Model, UseModel } from '@/types';
4 | import { getComputedDepModels, isModel } from '@/utils/misc';
5 |
6 | function createUseModel(context: Context): UseModel {
7 | function useModel(...args: any) {
8 | const flattenedArgs = Array.isArray(args[0])
9 | ? [...args[0], ...args.slice(1)]
10 | : args;
11 |
12 | flattenedArgs.forEach(model => {
13 | if (isModel(model)) {
14 | mountModel(context, model);
15 | }
16 | });
17 |
18 | const { getState, getActions, actualModels, subscribe } = parseModelParams(
19 | context,
20 | flattenedArgs,
21 | );
22 |
23 | const computedArr = actualModels.map(m => {
24 | const {
25 | modelDesc: { computed },
26 | } = context.apis.getModel(m);
27 | return computed;
28 | });
29 |
30 | const computedDepModels = getComputedDepModels(computedArr);
31 |
32 | computedDepModels.forEach(model => {
33 | if (isModel(model)) {
34 | mountModel(context, model);
35 | }
36 | });
37 |
38 | let [state, actions] = [getState(), getActions()];
39 |
40 | ({ state, actions } = context.pluginCore.invokePipeline(
41 | 'useModel',
42 | {
43 | state,
44 | actions,
45 | },
46 | {
47 | models: actualModels,
48 | mountedModels: actualModels.map(model => context.apis.getModel(model)),
49 | },
50 | ));
51 |
52 | return [state, actions, subscribe];
53 | }
54 |
55 | return useModel as UseModel;
56 | }
57 |
58 | const parseModelParams = (context: Context, _models: any) => {
59 | const models = Array.isArray(_models) ? _models : [_models];
60 | const actualModels = [];
61 | const selectors = [];
62 |
63 | for (const model of models) {
64 | if (isModel(model)) {
65 | actualModels.push(model);
66 | } else {
67 | selectors.push(model);
68 | }
69 | }
70 | const [stateSelector, actionSelector] = selectors;
71 |
72 | if (actualModels.length > 1) {
73 | actualModels.forEach(m => {
74 | if (
75 | Object.prototype.toString.call(context.apis.getModel(m).state) !==
76 | '[object Object]'
77 | ) {
78 | throw new Error(
79 | `You cant use multiple model one of which's state is primitive data`,
80 | );
81 | }
82 | });
83 | }
84 |
85 | const getStateWithComputed = (model: Model) => {
86 | const {
87 | state,
88 | modelDesc: { computed },
89 | } = context.apis.getModel(model);
90 |
91 | let computedState: any;
92 |
93 | if (computed) {
94 | computedState = Object.keys(computed).reduce((curState, computedKey) => {
95 | curState[computedKey] = state[computedKey];
96 | return curState;
97 | }, {});
98 | // state reference always changes when model has computed properties
99 | return { ...state, ...computedState };
100 | }
101 |
102 | return state;
103 | };
104 |
105 | const finalStateSelector = (...models: any[]) => {
106 | if (stateSelector) {
107 | return stateSelector(
108 | ...actualModels.map(model => getStateWithComputed(model)),
109 | );
110 | }
111 |
112 | if (models.length === 1) {
113 | return getStateWithComputed(models[0]);
114 | }
115 |
116 | return models.reduce(
117 | (res, model) => ({ ...res, ...getStateWithComputed(model) }),
118 | {},
119 | );
120 | };
121 |
122 | const finalActionSelector =
123 | actionSelector ||
124 | ((...actions: any[]) =>
125 | actions.reduce((res, action) => Object.assign(res, action), {}));
126 |
127 | return {
128 | getState: () => finalStateSelector(...actualModels),
129 | getActions: () =>
130 | finalActionSelector(
131 | ...actualModels.map(model => context.apis.getModel(model)!.actions),
132 | ),
133 | subscribe: (handler: () => void) =>
134 | combineSubscribe(
135 | context,
136 | ...actualModels.map(model => context.apis.getModelSubscribe(model)),
137 | )(handler),
138 | actualModels,
139 | };
140 | };
141 |
142 | export { createUseModel };
143 |
--------------------------------------------------------------------------------
/packages/plugins/xstate/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @modern-js-reduck/plugin-xstate
2 |
3 | ## 1.1.0
4 |
5 | ### Minor Changes
6 |
7 | - feat: the real v1.0
8 |
9 | ### Patch Changes
10 |
11 | - @modern-js-reduck/store@1.1.0
12 |
13 | ## 1.0.6
14 |
15 | ### Patch Changes
16 |
17 | - feat: uniform package version
18 | - Updated dependencies
19 | - @modern-js-reduck/store@1.0.6
20 |
21 | ## 1.0.3
22 |
23 | ### Patch Changes
24 |
25 | - ed80d6e: build: build modern js dist
26 | - Updated dependencies [ac05b04]
27 | - Updated dependencies [30b1167]
28 | - Updated dependencies [ed80d6e]
29 | - Updated dependencies [b33f42a]
30 | - Updated dependencies [9c09d2a]
31 | - Updated dependencies [51721bc]
32 | - @modern-js-reduck/store@1.0.5
33 |
34 | ## 1.0.2
35 |
36 | ### Patch Changes
37 |
38 | - bump dep
39 | - Updated dependencies [2b7d19f]
40 | - @modern-js-reduck/store@1.0.4
41 |
42 | ## 1.0.1
43 |
44 | ### Patch Changes
45 |
46 | - fix: fix git url
47 | - Updated dependencies [undefined]
48 | - @modern-js-reduck/store@1.0.1
49 |
50 | ## 1.0.0
51 |
52 | ### Minor Changes
53 |
54 | - feat: ready for publish 1.0.0
55 |
56 | ### Patch Changes
57 |
58 | - a7bd932: release: rc.7
59 | - 26d5144: fix(type): actions can return void
60 | - 9452589: fix: ts type
61 | - a7bd932: fix: use model State type
62 | - fd81b6b: feat: useLocalModel support
63 | - feat: ready for publish 1.0.0
64 | - a7bd932: fix: model type
65 | - a7bd932: fix: model when State passed use State
66 | - Updated dependencies [a7bd932]
67 | - Updated dependencies [undefined]
68 | - Updated dependencies [26d5144]
69 | - Updated dependencies [9452589]
70 | - Updated dependencies [a7bd932]
71 | - Updated dependencies [fd81b6b]
72 | - Updated dependencies [undefined]
73 | - Updated dependencies [a7bd932]
74 | - Updated dependencies [a7bd932]
75 | - @modern-js-reduck/store@1.0.0
76 |
77 | ## 1.0.0-rc.10
78 |
79 | ### Patch Changes
80 |
81 | - a7bd932: release: rc.7
82 | - 26d5144: fix(type): actions can return void
83 | - 9452589: fix: ts type
84 | - a7bd932: fix: use model State type
85 | - feat: useLocalModel support
86 | - a7bd932: fix: model type
87 | - a7bd932: fix: model when State passed use State
88 | - Updated dependencies [a7bd932]
89 | - Updated dependencies [26d5144]
90 | - Updated dependencies [9452589]
91 | - Updated dependencies [a7bd932]
92 | - Updated dependencies [undefined]
93 | - Updated dependencies [a7bd932]
94 | - Updated dependencies [a7bd932]
95 | - @modern-js-reduck/store@1.0.0-rc.10
96 |
97 | ## 1.0.0-rc.9
98 |
99 | ### Patch Changes
100 |
101 | - ac4124f: release: rc.7
102 | - 26d5144: fix(type): actions can return void
103 | - 9452589: fix: ts type
104 | - 1085d2d: fix: use model State type
105 | - fix: model type
106 | - 1085d2d: fix: model when State passed use State
107 | - Updated dependencies [ac4124f]
108 | - Updated dependencies [26d5144]
109 | - Updated dependencies [9452589]
110 | - Updated dependencies [1085d2d]
111 | - Updated dependencies [undefined]
112 | - Updated dependencies [1085d2d]
113 | - @modern-js-reduck/store@1.0.0-rc.9
114 |
115 | ## 1.0.0-rc.8
116 |
117 | ### Patch Changes
118 |
119 | - release: rc.7
120 | - 26d5144: fix(type): actions can return void
121 | - 9452589: fix: ts type
122 | - 1085d2d: fix: use model State type
123 | - 1085d2d: fix: model when State passed use State
124 | - Updated dependencies [undefined]
125 | - Updated dependencies [26d5144]
126 | - Updated dependencies [9452589]
127 | - Updated dependencies [1085d2d]
128 | - Updated dependencies [1085d2d]
129 | - @modern-js-reduck/store@1.0.0-rc.8
130 |
131 | ## 1.0.0-next.7
132 |
133 | ### Patch Changes
134 |
135 | - 26d5144: fix(type): actions can return void
136 | - 9452589: fix: ts type
137 | - fix: use model State type
138 | - fix: model when State passed use State
139 | - Updated dependencies [26d5144]
140 | - Updated dependencies [9452589]
141 | - Updated dependencies [undefined]
142 | - Updated dependencies [undefined]
143 | - @modern-js-reduck/store@1.0.0-next.7
144 |
145 | ## 1.0.0-rc.6
146 |
147 | ### Patch Changes
148 |
149 | - fix(type): actions can return void
150 | - 9452589: fix: ts type
151 | - Updated dependencies [undefined]
152 | - Updated dependencies [9452589]
153 | - @modern-js-reduck/store@1.0.0-rc.6
154 |
155 | ## 1.0.0-rc.5
156 |
157 | ### Patch Changes
158 |
159 | - fix: ts type
160 | - Updated dependencies [undefined]
161 | - @modern-js-reduck/store@1.0.0-rc.5
162 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | ".code-workspace": "jsonc",
4 | ".babelrc": "json",
5 | ".eslintrc": "jsonc",
6 | ".eslintrc*.json": "jsonc",
7 | ".stylelintrc": "jsonc",
8 | "stylelintrc": "jsonc",
9 | ".htmlhintrc": "jsonc",
10 | "htmlhintrc": "jsonc",
11 | "Procfile*": "shellscript",
12 | "README": "markdown"
13 | },
14 | "files.exclude": {
15 | "**/*/adapters/**/index.ts": true,
16 | "**/*/adapters/**/index.js": true
17 | },
18 | "search.useIgnoreFiles": true,
19 | "search.exclude": {
20 | "**/build": true,
21 | "**/output": true,
22 | "**/dist": true,
23 | "**/yarn.lock": true,
24 | "**/package-lock.json": true,
25 | "**/*.log": true,
26 | "**/*.pid": true,
27 | "**/.git": true,
28 | "**/node_modules": true,
29 | "**/bower_components": true
30 | },
31 | //
32 | "editor.rulers": [80, 120],
33 | "files.eol": "\n",
34 | "files.trimTrailingWhitespace": true,
35 | "files.insertFinalNewline": true,
36 | //
37 | "todo-tree.general.tags": ["TODO:", "FIXME:"],
38 | "todo-tree.highlights.defaultHighlight": {
39 | "gutterIcon": true
40 | },
41 | "todo-tree.highlights.customHighlight": {
42 | "TODO:": {
43 | "foreground": "#fff",
44 | "background": "#ffbd2a",
45 | "iconColour": "#ffbd2a"
46 | },
47 | "FIXME:": {
48 | "foreground": "#fff",
49 | "background": "#f06292",
50 | "icon": "flame",
51 | "iconColour": "#f06292"
52 | }
53 | },
54 | //
55 | "cSpell.diagnosticLevel": "Hint",
56 | "[javascript]": {
57 | "editor.defaultFormatter": "esbenp.prettier-vscode"
58 | },
59 | "[javascriptreact]": {
60 | "editor.defaultFormatter": "esbenp.prettier-vscode"
61 | },
62 | "[typescript]": {
63 | "editor.defaultFormatter": "esbenp.prettier-vscode"
64 | },
65 | "[typescriptreact]": {
66 | "editor.defaultFormatter": "esbenp.prettier-vscode"
67 | },
68 | "eslint.alwaysShowStatus": true,
69 | // "eslint.nodePath": "./node_modules",
70 | // "eslint.packageManager": "pnpm",
71 | "eslint.run": "onType",
72 | "eslint.options": {
73 | "rules": {
74 | "no-debugger": "off"
75 | }
76 | },
77 | "eslint.probe": [
78 | "javascript",
79 | "javascriptreact",
80 | "typescript",
81 | "typescriptreact",
82 | "vue"
83 | ],
84 | "eslint.format.enable": true,
85 | "eslint.lintTask.enable": true,
86 | "javascript.validate.enable": false,
87 | "typescript.validate.enable": true,
88 | "flow.enabled": false,
89 | //
90 | "htmlhint.enable": true,
91 | //
92 | "stylelint.enable": false,
93 | "css.validate": false,
94 | "scss.validate": false,
95 | "less.validate": false,
96 | "[css]": {
97 | "editor.defaultFormatter": "esbenp.prettier-vscode",
98 | "editor.formatOnType": true,
99 | "editor.formatOnPaste": true,
100 | "editor.formatOnSave": true
101 | },
102 | "[scss]": {
103 | "editor.defaultFormatter": "esbenp.prettier-vscode",
104 | "editor.formatOnType": true,
105 | "editor.formatOnPaste": true,
106 | "editor.formatOnSave": true
107 | },
108 | "[less]": {
109 | "editor.defaultFormatter": "esbenp.prettier-vscode",
110 | "editor.formatOnType": true,
111 | "editor.formatOnPaste": true,
112 | "editor.formatOnSave": true
113 | },
114 | "prettier.trailingComma": "all",
115 | "prettier.printWidth": 80,
116 | "prettier.semi": true,
117 | "prettier.arrowParens": "avoid",
118 | "prettier.bracketSpacing": true,
119 | "prettier.jsxBracketSameLine": true,
120 | //
121 | "editor.codeActionsOnSave": {
122 | "source.fixAll.eslint": "explicit"
123 | },
124 | "editor.codeActionsOnSaveTimeout": 5000,
125 | "editor.defaultFormatter": "EditorConfig.EditorConfig",
126 | "javascript.format.enable": false,
127 | "typescript.format.enable": false,
128 | //
129 | "json.format.enable": false,
130 | "[json]": {
131 | "editor.defaultFormatter": "esbenp.prettier-vscode",
132 | "editor.tabSize": 2,
133 | "editor.formatOnType": true,
134 | "editor.formatOnPaste": true,
135 | "editor.formatOnSave": true
136 | },
137 | "[jsonc]": {
138 | "editor.defaultFormatter": "esbenp.prettier-vscode",
139 | "editor.tabSize": 2,
140 | "editor.formatOnType": true,
141 | "editor.formatOnPaste": true,
142 | "editor.formatOnSave": true
143 | },
144 | "emmet.triggerExpansionOnTab": true,
145 | "typescript.tsdk": "node_modules/typescript/lib",
146 | "deno.enable": false,
147 | "cSpell.words": ["immer", "middlewares", "reduck", "xstate"]
148 | }
149 |
--------------------------------------------------------------------------------
/packages/plugins/xstate-immer/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @modern-js-reduck/plugin-xstate-immer
2 |
3 | ## 1.1.0
4 |
5 | ### Minor Changes
6 |
7 | - feat: the real v1.0
8 |
9 | ### Patch Changes
10 |
11 | - Updated dependencies
12 | - @modern-js-reduck/plugin-xstate@1.1.0
13 | - @modern-js-reduck/store@1.1.0
14 |
15 | ## 1.0.6
16 |
17 | ### Patch Changes
18 |
19 | - feat: uniform package version
20 | - Updated dependencies
21 | - @modern-js-reduck/plugin-xstate@1.0.4
22 | - @modern-js-reduck/store@1.0.6
23 |
24 | ## 1.0.2
25 |
26 | ### Patch Changes
27 |
28 | - ed80d6e: build: build modern js dist
29 | - Updated dependencies [ac05b04]
30 | - Updated dependencies [30b1167]
31 | - Updated dependencies [ed80d6e]
32 | - Updated dependencies [b33f42a]
33 | - Updated dependencies [9c09d2a]
34 | - Updated dependencies [51721bc]
35 | - @modern-js-reduck/store@1.0.5
36 | - @modern-js-reduck/plugin-xstate@1.0.3
37 |
38 | ## 1.0.1
39 |
40 | ### Patch Changes
41 |
42 | - fix: fix git url
43 | - Updated dependencies [undefined]
44 | - @modern-js-reduck/plugin-xstate@1.0.1
45 | - @modern-js-reduck/store@1.0.1
46 |
47 | ## 1.0.0
48 |
49 | ### Minor Changes
50 |
51 | - feat: ready for publish 1.0.0
52 |
53 | ### Patch Changes
54 |
55 | - a7bd932: release: rc.7
56 | - 26d5144: fix(type): actions can return void
57 | - 9452589: fix: ts type
58 | - a7bd932: fix: use model State type
59 | - fd81b6b: feat: useLocalModel support
60 | - feat: ready for publish 1.0.0
61 | - a7bd932: fix: model type
62 | - a7bd932: fix: model when State passed use State
63 | - Updated dependencies [a7bd932]
64 | - Updated dependencies [undefined]
65 | - Updated dependencies [26d5144]
66 | - Updated dependencies [9452589]
67 | - Updated dependencies [a7bd932]
68 | - Updated dependencies [fd81b6b]
69 | - Updated dependencies [undefined]
70 | - Updated dependencies [a7bd932]
71 | - Updated dependencies [a7bd932]
72 | - @modern-js-reduck/plugin-xstate@1.0.0
73 | - @modern-js-reduck/store@1.0.0
74 |
75 | ## 1.0.0-rc.10
76 |
77 | ### Patch Changes
78 |
79 | - a7bd932: release: rc.7
80 | - 26d5144: fix(type): actions can return void
81 | - 9452589: fix: ts type
82 | - a7bd932: fix: use model State type
83 | - feat: useLocalModel support
84 | - a7bd932: fix: model type
85 | - a7bd932: fix: model when State passed use State
86 | - Updated dependencies [a7bd932]
87 | - Updated dependencies [26d5144]
88 | - Updated dependencies [9452589]
89 | - Updated dependencies [a7bd932]
90 | - Updated dependencies [undefined]
91 | - Updated dependencies [a7bd932]
92 | - Updated dependencies [a7bd932]
93 | - @modern-js-reduck/plugin-xstate@1.0.0-rc.10
94 | - @modern-js-reduck/store@1.0.0-rc.10
95 |
96 | ## 1.0.0-rc.9
97 |
98 | ### Patch Changes
99 |
100 | - ac4124f: release: rc.7
101 | - 26d5144: fix(type): actions can return void
102 | - 9452589: fix: ts type
103 | - 1085d2d: fix: use model State type
104 | - fix: model type
105 | - 1085d2d: fix: model when State passed use State
106 | - Updated dependencies [ac4124f]
107 | - Updated dependencies [26d5144]
108 | - Updated dependencies [9452589]
109 | - Updated dependencies [1085d2d]
110 | - Updated dependencies [undefined]
111 | - Updated dependencies [1085d2d]
112 | - @modern-js-reduck/plugin-xstate@1.0.0-rc.9
113 | - @modern-js-reduck/store@1.0.0-rc.9
114 |
115 | ## 1.0.0-rc.8
116 |
117 | ### Patch Changes
118 |
119 | - release: rc.7
120 | - 26d5144: fix(type): actions can return void
121 | - 9452589: fix: ts type
122 | - 1085d2d: fix: use model State type
123 | - 1085d2d: fix: model when State passed use State
124 | - Updated dependencies [undefined]
125 | - Updated dependencies [26d5144]
126 | - Updated dependencies [9452589]
127 | - Updated dependencies [1085d2d]
128 | - Updated dependencies [1085d2d]
129 | - @modern-js-reduck/plugin-xstate@1.0.0-rc.8
130 | - @modern-js-reduck/store@1.0.0-rc.8
131 |
132 | ## 1.0.0-next.7
133 |
134 | ### Patch Changes
135 |
136 | - 26d5144: fix(type): actions can return void
137 | - 9452589: fix: ts type
138 | - fix: use model State type
139 | - fix: model when State passed use State
140 | - Updated dependencies [26d5144]
141 | - Updated dependencies [9452589]
142 | - Updated dependencies [undefined]
143 | - Updated dependencies [undefined]
144 | - @modern-js-reduck/plugin-xstate@1.0.0-next.7
145 | - @modern-js-reduck/store@1.0.0-next.7
146 |
147 | ## 1.0.0-rc.6
148 |
149 | ### Patch Changes
150 |
151 | - fix(type): actions can return void
152 | - 9452589: fix: ts type
153 | - Updated dependencies [undefined]
154 | - Updated dependencies [9452589]
155 | - @modern-js-reduck/plugin-xstate@1.0.0-rc.6
156 | - @modern-js-reduck/store@1.0.0-rc.6
157 |
158 | ## 1.0.0-rc.5
159 |
160 | ### Patch Changes
161 |
162 | - fix: ts type
163 | - Updated dependencies [undefined]
164 | - @modern-js-reduck/plugin-xstate@1.0.0-rc.5
165 | - @modern-js-reduck/store@1.0.0-rc.5
166 |
--------------------------------------------------------------------------------