├── .gitignore
├── README.md
├── examples
├── example-js
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── App.js
│ │ ├── components
│ │ │ ├── Counter.js
│ │ │ └── DemoRefresh.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── store.js
│ │ └── views
│ │ │ ├── DemoPage.js
│ │ │ ├── ModulesPage.js
│ │ │ └── PostsPage.js
│ └── yarn.lock
└── example-ts
│ ├── README.md
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ ├── src
│ ├── App.tsx
│ ├── components
│ │ ├── Post.tsx
│ │ └── PostDelete.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ ├── store.ts
│ ├── types
│ │ └── types.ts
│ └── views
│ │ ├── PostPage.tsx
│ │ └── PostsPage.tsx
│ ├── tsconfig.json
│ └── yarn.lock
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── tree.png
├── src
└── lib
│ ├── externalStore.ts
│ ├── getters.tsx
│ ├── helpers.ts
│ ├── hooks.ts
│ ├── index.ts
│ ├── storeContext.ts
│ ├── types.ts
│ └── withStore.tsx
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /lib
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 | .idea
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # examples
28 | /examples/example-ts/node_modules
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vuex - But for React! ⚛
2 |
3 | Enjoy the vuex API in your React applications with `vuex-but-for-react`, which uses only React Context and React use-sync-external-store under the hood.
4 |
5 | `vuex-but-for-react` was engineered with developer experience in mind, making it very easy to use. Invoke your getter or action by using a one-line hook and don't worry about unnecessary renders - **without** using `memo`.
6 |
7 | Your component will render only when its getter changes - and it doesn't care about the rest of the store!
8 |
9 | Are you on board? Read more 👇
10 |
11 | ## Installation
12 |
13 | `npm install vuex-but-for-react --save`
14 |
15 | `yarn add vuex-but-for-react`
16 |
17 | TS support included ✨
18 |
19 | ---
20 |
21 | 🚨 React versions
22 |
23 | **`vuex-but-for-react` >= `3.0.0` works with React `18.0.0`+**
24 |
25 | To use with older react versions, please install `vuex-but-for-react 2.0.6`
26 |
27 | ## Working example
28 |
29 | `store.js`
30 | ```javascript
31 | const store = {
32 | state: {
33 | posts: []
34 | },
35 | mutations: {
36 | POSTS_SET(state, data) {
37 | state.posts = data
38 | }
39 | },
40 | actions: {
41 | async POSTS_FETCH(context) {
42 | const response = await fetch('https://jsonplaceholder.typicode.com/posts')
43 | const data = await response.json()
44 | context.mutations.POSTS_SET(data)
45 | }
46 | },
47 | getters: {
48 | posts (state) {
49 | return state.posts
50 | }
51 | }
52 | }
53 | ```
54 |
55 | `index.js`
56 | ```javascript
57 | import React from 'react';
58 | import ReactDOM from 'react-dom';
59 | import { withStore } from 'vuex-but-for-react';
60 |
61 | import App from './App';
62 | import store from './store';
63 |
64 | const AppWithStore = withStore(App, store);
65 |
66 | ReactDOM.render(
67 | ,
68 | document.getElementById('root')
69 | );
70 | ```
71 |
72 | `Posts.js`
73 | ```javascript
74 | import React, { useEffect } from 'react';
75 | import { useAction, useGetter } from 'vuex-but-for-react';
76 |
77 | const Posts = () => {
78 | const handleAction = useAction('POSTS_FETCH');
79 | const posts = useGetter('posts');
80 |
81 | useEffect(() => {
82 | handleAction();
83 | }, [handleAction]) // don't worry, it doesn't re-create!
84 |
85 | return (
86 |
87 | {posts.map(post => {post.title} )}
88 |
89 | );
90 | }
91 |
92 | export default Posts
93 | ```
94 |
95 | Check the examples section to see JavaScript and TypeScript working apps!
96 |
97 | ## API
98 |
99 | ### useAction(`actionName`)
100 |
101 | An action is used for async data logic, especially API calls. You can dispatch mutations and other actions from within an action.
102 |
103 | The function returned by the `useAction()` hook is *never* re-created.
104 |
105 | ```javascript
106 | import { useAction } from 'vuex-but-for-react';
107 |
108 | const PostsPage = () => {
109 | const handleFetch = useAction('POSTS_FETCH');
110 |
111 | useEffect(() => {
112 | handleFetch();
113 | }, [handleFetch])
114 |
115 | return (
116 | ...
117 | )
118 | }
119 | ```
120 |
121 | ### useMutation(`actionName`)
122 |
123 | A mutation is used for sync data operations. It has access to the current state in order to alter it.
124 |
125 | The function returned by the `useMutation()` hook is *never* re-created.
126 |
127 | ```javascript
128 | import { useMutation } from 'vuex-but-for-react';
129 |
130 | const Counter = () => {
131 | const handleIncrement = useMutation('COUNTER_INCREMENT');
132 | const handleDecrement = useMutation('COUNTER_DECREMENT');
133 |
134 | return (
135 | <>
136 | -
137 | +
138 | >
139 | )
140 | }
141 | ```
142 |
143 | ### useGetter(`actionName`)
144 | A getter gives you the current stored value based on your config. It has access to the current state.
145 |
146 | The data returned by the `useGetter()` hook is updated *only in case the shallow value changes*.
147 | An update of one getter value won't trigger the update of another getter value.
148 |
149 | ```javascript
150 | import { useGetter } from 'vuex-but-for-react';
151 |
152 | const PostsPage = () => {
153 | const posts = useGetter('posts');
154 |
155 | return (
156 |
157 | {posts.map(post => (
158 | {post.title}
159 | ))}
160 |
161 | )
162 | }
163 | ```
164 |
165 | ### withStore(`Component`, `config`)
166 |
167 | In order to initialize the global store, wrap your (chosen) root component in your store config.
168 |
169 | ```javascript
170 | import { withStore } from 'vuex-but-for-react';
171 |
172 | const AppWithStore = withStore(App, store);
173 | ```
174 |
175 | And more amazing stuff!
176 |
177 | #### useActionOnMount(`actionName`)
178 |
179 | Avoid calling useEffect manually. Just pass the action name and it will be executed on component mount automatically.
180 |
181 | ```javascript
182 | import { useActionOnMount, useGetter } from 'vuex-but-for-react';
183 |
184 | const PostsPage = () => {
185 | useActionOnMount('POSTS_FETCH');
186 | const posts = useGetter('posts');
187 |
188 | return (
189 | ...
190 | )
191 | }
192 | ```
193 |
194 |
--------------------------------------------------------------------------------
/examples/example-js/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/example-js/README.md:
--------------------------------------------------------------------------------
1 | # Example - JS
2 |
3 | A JavaScript example using `vuex-but-for-react` with a blog-like global state logic
4 |
5 | * Check the views/DemoPage for use of a **mutation** and monitoring component re-renders
6 | * Check the views/PostsPage for use of an **action** and a **getter**
7 |
8 | The store config is as follows:
9 | ```javascript
10 | const store = {
11 | state: {
12 | demoRefreshValue: 0,
13 | counter: 0,
14 | posts: []
15 | },
16 | mutations: {
17 | POSTS_SET(state, data) {
18 | state.posts = data
19 | },
20 | COUNTER_INCREMENT(state) {
21 | state.counter++
22 | },
23 | COUNTER_DECREMENT(state) {
24 | state.counter--
25 | },
26 | DEMO_VALUE_SET(state, value) {
27 | state.demoRefreshValue = value
28 | }
29 | },
30 | actions: {
31 | async POSTS_FETCH(context) {
32 | const response = await fetch('https://jsonplaceholder.typicode.com/posts')
33 | const data = await response.json()
34 | context.mutations.POSTS_SET(data)
35 | }
36 | },
37 | getters: {
38 | posts (state) {
39 | return state.posts
40 | }
41 | }
42 | }
43 | ```
44 |
45 | ## Available Scripts
46 |
47 | In the project directory, you can run:
48 |
49 | ### `yarn start`
50 |
51 | Runs the app in the development mode.\
52 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
53 |
54 | The page will reload if you make edits.\
55 | You will also see any lint errors in the console.
56 |
--------------------------------------------------------------------------------
/examples/example-js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-js",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react-dom": "^18.2.0",
7 | "react-router-dom": "^6.4.0",
8 | "react-scripts": "^5.0.1",
9 | "vuex-but-for-react": "link:../..",
10 | "web-vitals": "^1.0.1"
11 | },
12 | "scripts": {
13 | "start": "react-scripts start",
14 | "build": "react-scripts build",
15 | "test": "react-scripts test",
16 | "eject": "react-scripts eject"
17 | },
18 | "eslintConfig": {
19 | "extends": [
20 | "react-app",
21 | "react-app/jest"
22 | ]
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/example-js/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DJanoskova/vuex-but-for-react/c958008d3edc83fc1f8ead7389f95b9c68092135/examples/example-js/public/favicon.ico
--------------------------------------------------------------------------------
/examples/example-js/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/example-js/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DJanoskova/vuex-but-for-react/c958008d3edc83fc1f8ead7389f95b9c68092135/examples/example-js/public/logo192.png
--------------------------------------------------------------------------------
/examples/example-js/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DJanoskova/vuex-but-for-react/c958008d3edc83fc1f8ead7389f95b9c68092135/examples/example-js/public/logo512.png
--------------------------------------------------------------------------------
/examples/example-js/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/examples/example-js/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/examples/example-js/src/App.js:
--------------------------------------------------------------------------------
1 | import { HashRouter, Routes, Route, Link } from 'react-router-dom';
2 |
3 | import DemoPage from './views/DemoPage';
4 | import PostsPage from './views/PostsPage';
5 | import ModulesPage from './views/ModulesPage';
6 |
7 | function App() {
8 | return (
9 |
10 |
11 |
12 | Demo
13 | Posts
14 | Modules
15 |
16 |
17 |
18 | } />
21 | } />
24 | } />
27 | {/* }/>*/}
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | export default App;
35 |
--------------------------------------------------------------------------------
/examples/example-js/src/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useGetter, useMutation } from 'vuex-but-for-react';
3 |
4 | const Counter = () => {
5 | const handleIncrement = useMutation('COUNTER_INCREMENT');
6 | const handleDecrement = useMutation('COUNTER_DECREMENT');
7 | const counter = useGetter('counter');
8 |
9 | console.log('Counter re-render', counter);
10 |
11 | return (
12 |
13 | Counter
14 |
15 | -
16 |
17 | {counter}
18 |
19 | +
20 |
21 | )
22 | }
23 |
24 | export default Counter;
25 |
--------------------------------------------------------------------------------
/examples/example-js/src/components/DemoRefresh.js:
--------------------------------------------------------------------------------
1 | import { useGetter, useMutation } from 'vuex-but-for-react';
2 |
3 | const DemoRefresh = () => {
4 | const handleClick = useMutation('DEMO_VALUE_SET');
5 | const value = useGetter('demoRefreshValue');
6 |
7 | const onClick = () => {
8 | handleClick(1);
9 | }
10 |
11 | console.log('Demo refresh re-render: ', value);
12 |
13 | return (
14 |
15 | Demo refresh
16 |
17 | Current value: {value}
18 |
19 | Change value to 1
20 |
21 | )
22 | }
23 |
24 | export default DemoRefresh;
25 |
--------------------------------------------------------------------------------
/examples/example-js/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
15 | .app {
16 | max-width: 800px;
17 | margin: 30px auto;
18 | }
19 |
20 | .nav a + a {
21 | margin-left: 10px;
22 | }
23 |
24 | .list-none {
25 | list-style-type: none;
26 | padding: 0;
27 | }
28 |
29 | .blog-item {
30 | margin-bottom: 1px;
31 | }
32 |
33 | .button-x {
34 | margin-right: 10px;
35 | }
--------------------------------------------------------------------------------
/examples/example-js/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { withStore } from 'vuex-but-for-react';
4 |
5 | import './index.css';
6 |
7 | import App from './App';
8 | import store from './store';
9 |
10 | const AppWithStore = withStore(App, store);
11 |
12 | const root = ReactDOM.createRoot(document.getElementById("root"));
13 | root.render( );
--------------------------------------------------------------------------------
/examples/example-js/src/store.js:
--------------------------------------------------------------------------------
1 | const projectsModule = {
2 | state: {
3 | data: [{ id: 1, title: 'Dummy project' }]
4 | },
5 | mutations: {
6 | PROJECTS_SET(state, data) {
7 | state.data = data
8 | }
9 | },
10 | actions: {
11 | async PROJECTS_FETCH(context) {
12 | const response = await fetch('https://jsonplaceholder.typicode.com/posts')
13 | const data = await response.json()
14 | context.mutations.PROJECTS_SET(data.splice(20, 20))
15 | }
16 | },
17 | getters: {
18 | projects (state) {
19 | return state.data
20 | }
21 | }
22 | }
23 |
24 | const store = {
25 | state: {
26 | demoRefreshValue: 0,
27 | counter: 0,
28 | // use nested structure to showcase deep reactivity
29 | blog: {
30 | name: 'My Blog!',
31 | posts: []
32 | },
33 | },
34 | mutations: {
35 | POSTS_SET(state, data) {
36 | state.blog.posts = data
37 | },
38 | POST_REMOVE(state, id) {
39 | state.blog.posts = state.blog.posts.filter(p => p.id !== id)
40 | },
41 | COUNTER_INCREMENT(state) {
42 | state.counter++
43 | },
44 | COUNTER_DECREMENT(state) {
45 | state.counter--
46 | },
47 | DEMO_VALUE_SET(state, value) {
48 | state.demoRefreshValue = value
49 | }
50 | },
51 | actions: {
52 | async POSTS_FETCH(context) {
53 | const response = await fetch('https://jsonplaceholder.typicode.com/posts')
54 | const data = await response.json()
55 | context.mutations.POSTS_SET(data)
56 | }
57 | },
58 | getters: {
59 | demoRefreshValue(state) {
60 | return state.demoRefreshValue
61 | },
62 | counter (state) {
63 | return state.counter
64 | },
65 | blog (state) {
66 | return state.blog
67 | }
68 | },
69 | modules: {
70 | projects: projectsModule
71 | }
72 | }
73 |
74 | export default store
--------------------------------------------------------------------------------
/examples/example-js/src/views/DemoPage.js:
--------------------------------------------------------------------------------
1 | import Counter from '../components/Counter';
2 | import DemoRefresh from '../components/DemoRefresh';
3 |
4 | const DemoPage = () => {
5 | return (
6 |
7 |
Check the console logs for re-renders!
8 |
9 | Neither Counter nor DemoRefresh component is wrapped in memo, yet they update only when their value changes.
10 |
11 |
12 | Demo refresh updates only once, even though we fire the mutation on each
13 | click. vuex-but-for-react
won't update the context when the value is the same.
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default DemoPage;
23 |
--------------------------------------------------------------------------------
/examples/example-js/src/views/ModulesPage.js:
--------------------------------------------------------------------------------
1 | import { useActionOnMount, useGetter } from 'vuex-but-for-react';
2 |
3 | const ModulesPage = () => {
4 | const projects = useGetter('projects/projects');
5 | useActionOnMount('projects/PROJECTS_FETCH');
6 |
7 | return (
8 |
9 | {projects.map(project => (
10 | {project.title}
11 | ))}
12 |
13 | )
14 | }
15 |
16 | export default ModulesPage;
17 |
--------------------------------------------------------------------------------
/examples/example-js/src/views/PostsPage.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useAction, useGetter, useMutation } from 'vuex-but-for-react';
3 |
4 | const PostsPage = () => {
5 | const handleFetch = useAction('POSTS_FETCH');
6 | const handleRemove = useMutation('POST_REMOVE');
7 | const blog = useGetter('blog');
8 |
9 | useEffect(() => {
10 | handleFetch();
11 | }, [handleFetch])
12 |
13 | // You can use useActionOnMount() instead of useAction and useEffect!
14 | // useActionOnMount('POSTS_FETCH');
15 |
16 | return (
17 |
18 | {blog.posts.map(post => (
19 |
20 | handleRemove(post.id)}>
21 | x
22 |
23 | {post.title}
24 |
25 | ))}
26 |
27 | )
28 | }
29 |
30 | export default PostsPage;
31 |
--------------------------------------------------------------------------------
/examples/example-ts/README.md:
--------------------------------------------------------------------------------
1 | # Example - TS
2 |
3 | A TypeScript example using `vuex-but-for-react` with a blog-like global state logic
4 |
5 | * Check the views/PostsPage for use of an **action** and a **getter**
6 | * Check the views/PostPage for use of an async **action** and storing the data locally.
7 | * Check the components/PostDelete page for use of **action** which dispatches a mutation from the inside.
8 |
9 | The store config is as follows:
10 | ```typescript
11 | const store: StoreType<{ posts: PostType[] }> = {
12 | state: {
13 | posts: []
14 | },
15 | mutations: {
16 | POSTS_SET(state, data) {
17 | state.posts = data
18 | },
19 | POST_REMOVE(state, id) {
20 | state.posts = state.posts.filter((p: PostType) => p.id !== id)
21 | }
22 | },
23 | actions: {
24 | async POSTS_FETCH(context) {
25 | const response = await fetch('https://jsonplaceholder.typicode.com/posts')
26 | const data = await response.json()
27 | context.mutations.POSTS_SET(data)
28 | },
29 | async POST_FETCH(_, id) {
30 | const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
31 | const data = await response.json()
32 | return data
33 | },
34 | async POST_DELETE(context, id) {
35 | await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
36 | method: 'DELETE',
37 | })
38 | context.mutations.POST_REMOVE(id)
39 | }
40 | },
41 | getters: {
42 | posts (state) {
43 | return state.posts
44 | }
45 | }
46 | }
47 | ```
48 |
49 | ## Available Scripts
50 |
51 | In the project directory, you can run:
52 |
53 | ### `yarn start`
54 |
55 | Runs the app in the development mode.\
56 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
57 |
58 | The page will reload if you make edits.\
59 | You will also see any lint errors in the console.
60 |
--------------------------------------------------------------------------------
/examples/example-ts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-ts",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/node": "^12.0.0",
7 | "@types/react": "^18.0.2",
8 | "@types/react-dom": "^18.0.2",
9 | "react-dom": "^18.2.0",
10 | "react-router-dom": "^6.4.0",
11 | "react-scripts": "^5.0.1",
12 | "typescript": "^4.1.2",
13 | "vuex-but-for-react": "link:../.."
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": [
23 | "react-app",
24 | "react-app/jest"
25 | ]
26 | },
27 | "browserslist": {
28 | "production": [
29 | ">0.2%",
30 | "not dead",
31 | "not op_mini all"
32 | ],
33 | "development": [
34 | "last 1 chrome version",
35 | "last 1 firefox version",
36 | "last 1 safari version"
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/example-ts/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DJanoskova/vuex-but-for-react/c958008d3edc83fc1f8ead7389f95b9c68092135/examples/example-ts/public/favicon.ico
--------------------------------------------------------------------------------
/examples/example-ts/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/example-ts/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DJanoskova/vuex-but-for-react/c958008d3edc83fc1f8ead7389f95b9c68092135/examples/example-ts/public/logo192.png
--------------------------------------------------------------------------------
/examples/example-ts/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DJanoskova/vuex-but-for-react/c958008d3edc83fc1f8ead7389f95b9c68092135/examples/example-ts/public/logo512.png
--------------------------------------------------------------------------------
/examples/example-ts/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/examples/example-ts/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/examples/example-ts/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter, Routes, Route } from 'react-router-dom';
3 |
4 | import PostsPage from "./views/PostsPage";
5 | import PostPage from "./views/PostPage";
6 |
7 | function App() {
8 | return (
9 |
10 |
11 |
12 | }
15 | />
16 | }
19 | />
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/examples/example-ts/src/components/Post.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, memo } from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | import { PostType } from "../types/types";
5 |
6 | import PostDelete from "./PostDelete";
7 |
8 | interface IProps {
9 | post: PostType;
10 | }
11 |
12 | const Post: FunctionComponent = ({ post }) => {
13 | console.log('post rendered', post.id)
14 | return (
15 |
16 |
{post.title}
17 | {post.body}
18 |
19 |
Detail
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | // Wrapping the post in memo() will prevent re-renders when a post is removed
27 | // the other post objects aren't re-created, so a shallow comparison is enough
28 | export default memo(Post);
29 |
--------------------------------------------------------------------------------
/examples/example-ts/src/components/PostDelete.tsx:
--------------------------------------------------------------------------------
1 | import React, { FormEvent, FunctionComponent } from 'react';
2 | import { useAction } from "vuex-but-for-react";
3 |
4 | interface IProps {
5 | id: number
6 | }
7 |
8 | const PostDelete: FunctionComponent = ({ id }) => {
9 | const onDelete = useAction('POST_DELETE');
10 |
11 | const handleDelete = (e: FormEvent) => {
12 | e.stopPropagation();
13 | onDelete(id);
14 | }
15 |
16 | return (
17 |
18 | Delete post
19 |
20 | )
21 | }
22 |
23 | export default PostDelete;
24 |
--------------------------------------------------------------------------------
/examples/example-ts/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
15 | .app {
16 | max-width: 800px;
17 | margin: 30px auto;
18 | }
19 |
20 | .post {
21 | padding: 20px;
22 | border-bottom: 1px #eaeaea solid;
23 | }
24 |
25 | button.link {
26 | border: none;
27 | padding: 0;
28 | font-size: inherit;
29 | background: none;
30 | color: -webkit-link;
31 | cursor: pointer;
32 | text-decoration: underline;
33 | }
--------------------------------------------------------------------------------
/examples/example-ts/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from "react-dom/client";
3 | import { withStore } from 'vuex-but-for-react';
4 |
5 | import './index.css';
6 |
7 | import App from './App';
8 | import store from "./store";
9 |
10 | const AppWithStore = withStore(App, store, { localStorageName: 'test-store' });
11 |
12 | const root = ReactDOM.createRoot(document.getElementById("root"));
13 | root.render( );
--------------------------------------------------------------------------------
/examples/example-ts/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/example-ts/src/store.ts:
--------------------------------------------------------------------------------
1 | import { VuexStoreType } from "vuex-but-for-react";
2 |
3 | import { PostType } from "./types/types";
4 |
5 | export interface StoreType { posts: PostType[] }
6 |
7 | const store: VuexStoreType = {
8 | state: {
9 | posts: []
10 | },
11 | mutations: {
12 | POSTS_SET(state, data: PostType[]) {
13 | state.posts = data
14 | },
15 | POST_REMOVE(state, id: number) {
16 | state.posts = state.posts.filter((p) => p.id !== id)
17 | }
18 | },
19 | actions: {
20 | async POSTS_FETCH(context) {
21 | const response = await fetch('https://jsonplaceholder.typicode.com/posts')
22 | const data = await response.json()
23 | context.mutations.POSTS_SET(data.slice(0, 20));
24 | },
25 | async POST_FETCH(_, id) {
26 | const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
27 | const data = await response.json()
28 | return data;
29 | },
30 | async POST_DELETE(context, id) {
31 | await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
32 | method: 'DELETE',
33 | })
34 | context.mutations.POST_REMOVE(id)
35 | }
36 | },
37 | getters: {
38 | posts (state) {
39 | return state.posts
40 | }
41 | },
42 | }
43 |
44 | export default store
--------------------------------------------------------------------------------
/examples/example-ts/src/types/types.ts:
--------------------------------------------------------------------------------
1 | export interface PostType {
2 | id: number;
3 | title: string;
4 | body: string;
5 | }
--------------------------------------------------------------------------------
/examples/example-ts/src/views/PostPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, useEffect, useState } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import { useAction } from "vuex-but-for-react";
4 |
5 | import { PostType } from "../types/types";
6 |
7 | const PostPage: FunctionComponent = () => {
8 | const { id } = useParams<{ id: string }>();
9 | const [postData, setPostData] = useState(null);
10 | const handleFetch = useAction('POST_FETCH');
11 |
12 | useEffect(() => {
13 | async function fetchFn() {
14 | const data = await handleFetch(id);
15 | setPostData(data);
16 | }
17 | fetchFn();
18 | }, [handleFetch, id]);
19 |
20 | if (!postData) return <>Loading...>;
21 |
22 | return (
23 |
24 |
{postData.title}
25 | {postData.body}
26 |
27 | )
28 | }
29 |
30 | export default PostPage;
31 |
--------------------------------------------------------------------------------
/examples/example-ts/src/views/PostsPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useAction, useGetter } from "vuex-but-for-react";
3 |
4 | import { PostType } from '../types/types';
5 |
6 | import Post from "../components/Post";
7 |
8 | const PostsPage = () => {
9 | const handleFetch = useAction('POSTS_FETCH');
10 | const posts = useGetter('posts');
11 |
12 | useEffect(() => {
13 | handleFetch();
14 | }, [handleFetch]);
15 |
16 | return (
17 |
18 | {posts.map(post => (
19 |
20 | ))}
21 |
22 | )
23 | }
24 |
25 | export default PostsPage;
26 |
--------------------------------------------------------------------------------
/examples/example-ts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "strictNullChecks": false,
22 | "jsx": "react-jsx"
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vuex-but-for-react",
3 | "version": "3.0.2",
4 | "private": false,
5 | "main": "./lib/cjs/index.js",
6 | "module": "./lib/esm/index.js",
7 | "types": "./lib/esm/index.d.ts",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/DJanoskova/vuex-but-for-react"
11 | },
12 | "homepage": "https://vuex-but-for-react.netlify.app/",
13 | "keywords": [
14 | "react",
15 | "reducer",
16 | "state",
17 | "store",
18 | "functional",
19 | "flux"
20 | ],
21 | "peerDependencies": {
22 | "react": ">=18"
23 | },
24 | "devDependencies": {
25 | "react": "^18.2.0",
26 | "typescript": "^4.1.2"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "rm -rf ./lib && yarn build:esm && yarn build:cjs",
31 | "build:esm": "tsc",
32 | "build:cjs": "tsc --module commonjs --outDir lib/cjs"
33 | },
34 | "eslintConfig": {
35 | "extends": [
36 | "react-app",
37 | "react-app/jest"
38 | ]
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | },
52 | "files": [
53 | "/lib"
54 | ],
55 | "dependencies": {
56 | "object-deep-recreate": "^1.0.1"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DJanoskova/vuex-but-for-react/c958008d3edc83fc1f8ead7389f95b9c68092135/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DJanoskova/vuex-but-for-react/c958008d3edc83fc1f8ead7389f95b9c68092135/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DJanoskova/vuex-but-for-react/c958008d3edc83fc1f8ead7389f95b9c68092135/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DJanoskova/vuex-but-for-react/c958008d3edc83fc1f8ead7389f95b9c68092135/public/tree.png
--------------------------------------------------------------------------------
/src/lib/externalStore.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useSyncExternalStore } from 'react';
2 |
3 | export type ExternalStoreListenerType = () => void;
4 | export type StateType> = T;
5 | export type SetStateType = (previous: T) => T;
6 |
7 | export type ExternalStoreType = {
8 | getState: () => T;
9 | setState: (fn: SetStateType) => void;
10 | subscribe: (listener: ExternalStoreListenerType) => () => void;
11 | };
12 |
13 | export const useStore = (store: ExternalStoreType, propertyName: string): T => {
14 | const getSnapshot = useCallback(() => {
15 | return store.getState()[propertyName];
16 | }, [store, propertyName]);
17 |
18 | return useSyncExternalStore(store.subscribe, getSnapshot);
19 | };
20 |
21 | /**
22 | * https://blog.saeloun.com/2021/12/30/react-18-usesyncexternalstore-api
23 | */
24 | export const createStore = (initialState: T): ExternalStoreType => {
25 | let state: T = initialState;
26 | const getState = () => state;
27 |
28 | const listeners: Set = new Set();
29 |
30 | const setState = (fn: SetStateType) => {
31 | state = fn(state);
32 | listeners.forEach((l) => l());
33 | };
34 |
35 | const subscribe = (listener: ExternalStoreListenerType) => {
36 | listeners.add(listener);
37 |
38 | return () => {
39 | listeners.delete(listener);
40 | };
41 | };
42 |
43 | return { getState, setState, subscribe };
44 | };
45 |
--------------------------------------------------------------------------------
/src/lib/getters.tsx:
--------------------------------------------------------------------------------
1 | import { MutableRefObject } from 'react';
2 | import { deepRecreate } from "object-deep-recreate";
3 |
4 | import { GetterType, VuexStoreType } from "./types";
5 | import { getStoreKeyModuleValues, getStoreModule } from "./helpers";
6 | import { ExternalStoreType, StateType } from './externalStore';
7 |
8 | /**
9 | * gets the current state
10 | * runs it through all the getters
11 | * updates the gettersValues where needed
12 | * @param store
13 | * @param state
14 | * @param globalGetters
15 | * @param prevValuesRef
16 | */
17 | export const calcAndSetGettersValues = (
18 | store: VuexStoreType,
19 | state: T,
20 | globalGetters: ExternalStoreType>,
21 | prevValuesRef: MutableRefObject>,
22 | ) => {
23 | const getters = getStoreKeyModuleValues(store, 'getters');
24 | const getterNames = Object.keys(getters);
25 | if (!getterNames.length) return;
26 |
27 | const setter = (values) => {
28 | const prevValues = prevValuesRef.current;
29 |
30 | getterNames.forEach(getterPath => {
31 | const moduleNames = getterPath.split('/');
32 | let originalFn: GetterType;
33 |
34 | let value;
35 |
36 | // alter the state with the logic given in the store config
37 | if (moduleNames.length === 1) {
38 | originalFn = store.getters?.[getterPath] as GetterType;
39 |
40 | value = originalFn(state);
41 | } else {
42 | const moduleStore = getStoreModule(store, getterPath) as StateType;
43 | const moduleState = getStoreModule(state, getterPath) as T;
44 |
45 | const getterName = moduleNames[moduleNames.length - 1];
46 | originalFn = moduleStore.getters?.[getterName] as GetterType;
47 |
48 | value = originalFn(moduleState);
49 | }
50 |
51 | const prevValueStringified = JSON.stringify(prevValues[getterPath]);
52 | const isChanged = JSON.stringify(value) !== prevValueStringified;
53 | if (isChanged) {
54 | values[getterPath] = deepRecreate(value, JSON.parse(prevValueStringified));
55 | }
56 | });
57 |
58 | prevValuesRef.current = JSON.parse(JSON.stringify(values));
59 | return values;
60 | }
61 |
62 | globalGetters.setState(setter)
63 | }
64 |
65 | export const getGetterInitialValue = (getterName: string, gettersFns: Record, store: VuexStoreType) => {
66 | const originalFn = gettersFns[getterName] as GetterType;
67 | const moduleNames = getterName.split('/');
68 | let value;
69 |
70 | // alter the state with the logic given in the store config
71 | if (moduleNames.length === 1) {
72 | value = originalFn(store.state as StateType);
73 | } else {
74 | const moduleStore = getStoreModule(store, getterName) as StateType;
75 | value = originalFn(moduleStore.state);
76 | }
77 |
78 | return value;
79 | }
80 |
81 | export const getGettersInitialValues = (store: VuexStoreType) => {
82 | const gettersFns = getStoreKeyModuleValues(store, 'getters');
83 | const getterNames = Object.keys(gettersFns);
84 |
85 | const result: Record = {};
86 |
87 | if (!getterNames.length) return result;
88 |
89 | getterNames.forEach(getterName => {
90 | const value = getGetterInitialValue(getterName, gettersFns, store);
91 | result[getterName] = value;
92 | });
93 |
94 | return result;
95 | }
96 |
--------------------------------------------------------------------------------
/src/lib/helpers.ts:
--------------------------------------------------------------------------------
1 | import { VuexStoreType } from "./types";
2 |
3 | /**
4 | * Returns an object with keys and fn values
5 | * for mutations, actions and getters
6 | * Accounts for infinite levels of children modules
7 | * @param store
8 | * @param storeType
9 | * @param result
10 | * @param prefix
11 | */
12 | export const getStoreKeyModuleValues = (
13 | store: VuexStoreType,
14 | storeType: 'mutations' | 'actions' | 'getters',
15 | result: Record = {},
16 | prefix = ''
17 | ) => {
18 | // get the current key names with added prefix
19 | if (store[storeType]) {
20 | let keyNames = Object.keys(store[storeType] ?? {});
21 |
22 | keyNames.forEach(keyName => {
23 | const keyNameWithPrefix = prefix ? `${prefix}/${keyName}` : keyName;
24 | Object.assign(result, { [keyNameWithPrefix]: store[storeType]?.[keyName] })
25 | })
26 | }
27 |
28 | // check for child modules
29 | const childModules = Object.keys(store.modules ?? {});
30 | if (childModules.length) {
31 | childModules.forEach(moduleName => {
32 | const childPrefix = prefix ? `${prefix}/${moduleName}` : moduleName;
33 | if (store.modules) getStoreKeyModuleValues(store.modules[moduleName], storeType, result, childPrefix);
34 | })
35 | }
36 |
37 | return result;
38 | }
39 |
40 | /**
41 | * from projects/chemistry/POSTS_FETCH -> to projects/chemistry
42 | * @param path
43 | */
44 | export const getStoreModuleName = (path: string) => {
45 | const moduleNames = path.split('/')
46 | // remove the last action/mutation name, keep module levels only
47 | moduleNames.splice(moduleNames.length - 1, 1)
48 |
49 | return moduleNames.join('/') + '/'
50 | }
51 |
52 | export const filterObjectModuleKeys = (data: Record, keyName) => {
53 | const modulePath = getStoreModuleName(keyName)
54 |
55 | const clonedData = { ...data }
56 |
57 | Object.keys(clonedData).forEach(key => {
58 | if (key.includes(modulePath)) {
59 | // store the module data in the root
60 | const data = clonedData[key]
61 | clonedData[key.replace(modulePath, '')] = data
62 | }
63 | delete clonedData[key]
64 | })
65 |
66 | return clonedData;
67 | }
68 |
69 | export function getStoreModule(obj: Record, propString: string) {
70 | if (!propString)
71 | return obj;
72 |
73 | let clonedOriginal = { ...obj }
74 |
75 | const props = propString.split('/');
76 | let prop: string
77 |
78 | for (let i = 0, iLen = props.length - 1; i < iLen; i++) {
79 | prop = props[i];
80 |
81 | const candidate = clonedOriginal.modules?.[prop];
82 | if (candidate !== undefined) {
83 | clonedOriginal = candidate;
84 | } else {
85 | break;
86 | }
87 | }
88 |
89 | return clonedOriginal;
90 | }
91 |
92 | export const getStoreStateWithModules = (store: VuexStoreType, result: Record = {}): InheritedStateType => {
93 | Object.assign(result, store?.state || {});
94 |
95 | const childModules = Object.keys(store.modules ?? {});
96 | if (childModules.length) {
97 | Object.assign(result, { modules: {} });
98 |
99 | childModules.forEach(moduleName => {
100 | Object.assign(result.modules, { [moduleName]: {} });
101 | if (store.modules) getStoreStateWithModules(store.modules[moduleName], result.modules[moduleName]);
102 | })
103 | }
104 |
105 | return result as InheritedStateType;
106 | }
107 |
108 | export const handleStateFillWithLocalValues = ,>(state: T, storageName: string) => {
109 | const storedState = localStorage.getItem(storageName);
110 | if (storedState) {
111 | const storedStateValues: Record = JSON.parse(storedState);
112 | Object.keys(storedStateValues).forEach((key) => {
113 | state[key] = storedStateValues[key];
114 | })
115 | }
116 | }
--------------------------------------------------------------------------------
/src/lib/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useContext, useEffect } from "react";
2 |
3 | import { actionsContext, globalGettersContext, globalStoreContext, mutationsContext } from "./storeContext";
4 | import { filterObjectModuleKeys } from "./helpers";
5 | import { ExternalStoreType, useStore } from './externalStore';
6 |
7 | export const useAction = (actionName: string) => {
8 | const globalStore = useContext(globalStoreContext);
9 | const actions = useContext(actionsContext);
10 | const mutations = useContext(mutationsContext);
11 | const action = actions[actionName];
12 |
13 | if (!action) {
14 | throw new Error(`Cannot find action: ${actionName}`)
15 | }
16 |
17 | const actionWithStoreParams = useCallback<(...args: any) => Promise>((...args: any[]) => {
18 | if (!globalStore) throw new Error('No store found');
19 |
20 | const moduleNames = actionName.split('/');
21 | let filteredActions = actions;
22 | let filteredMutations = mutations;
23 |
24 | if (moduleNames.length > 1) {
25 | filteredActions = filterObjectModuleKeys(actions, actionName);
26 | filteredMutations = filterObjectModuleKeys(mutations, actionName);
27 | }
28 |
29 | return action({ actions: filteredActions, mutations: filteredMutations, state: globalStore.getState() }, ...args);
30 | }, [actions, mutations, actionName])
31 |
32 | return actionWithStoreParams;
33 | }
34 |
35 | export const useActions = (values: string[]) => {
36 | const actions = useContext(actionsContext);
37 |
38 | const result: Array<(args?: any) => Promise> = [];
39 |
40 | values.forEach((actionName) => {
41 | if (!actions[actionName]) {
42 | throw new Error(`Cannot find action: ${actionName}`);
43 | } else {
44 | result.push(actions[actionName]);
45 | }
46 | });
47 |
48 | return result;
49 | }
50 |
51 | export const useMutation = (mutationName: string) => {
52 | const mutations = useContext(mutationsContext);
53 |
54 | if (!mutations[mutationName]) {
55 | throw new Error(`Cannot find mutation: ${mutationName}`)
56 | }
57 |
58 | return mutations[mutationName];
59 | }
60 |
61 | export const useMutations = (values: string[]) => {
62 | const mutations = useContext(mutationsContext);
63 |
64 | const result: Array<(...args: any) => void> = [];
65 |
66 | values.forEach((mutationName) => {
67 | if (!mutations[mutationName]) {
68 | throw new Error(`Cannot find mutation: ${mutationName}`);
69 | } else {
70 | result.push(mutations[mutationName]);
71 | }
72 | });
73 |
74 | return result;
75 | }
76 |
77 | export const useGetter = (getterName: string): T => {
78 | const gettersStore = useContext(globalGettersContext);
79 | const value = useStore(gettersStore.current as ExternalStoreType, getterName);
80 |
81 | return value;
82 | }
83 |
84 | export const useActionOnMount = (actionName: string, ...params) => {
85 | const action = useAction(actionName);
86 |
87 | useEffect(() => {
88 | action(...params);
89 | }, [action, ...params]);
90 | }
91 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { default as withStore } from './withStore';
2 |
3 | export { useAction, useActions, useActionOnMount, useMutation, useMutations, useGetter } from './hooks';
4 |
5 | export type { VuexStoreType } from './types';
6 |
--------------------------------------------------------------------------------
/src/lib/storeContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext, memo, MutableRefObject } from 'react';
2 |
3 | import { ActionParamsType } from './types';
4 | import { createStore, ExternalStoreType } from './externalStore';
5 |
6 | export const mutationsContext = createContext void>>({});
7 | export const MutationsProvider = memo(mutationsContext.Provider);
8 |
9 | export const actionsContext = createContext Promise>>({});
10 | export const ActionsProvider = memo(actionsContext.Provider);
11 |
12 | export const globalStoreContext = createContext(createStore({}));
13 | export const GlobalStoreProvider = memo(globalStoreContext.Provider);
14 |
15 | type GettersContextType = MutableRefObject>
16 | export const globalGettersContext = createContext({
17 | current: createStore({})
18 | });
19 | export const GlobalGettersProvider = globalGettersContext.Provider;
20 |
--------------------------------------------------------------------------------
/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { StateType } from './externalStore';
2 |
3 | export interface VuexStoreType {
4 | state: T;
5 | mutations?: Record>;
6 | actions?: Record>;
7 | getters?: Record>;
8 | modules?: Record;
9 | }
10 |
11 | export interface ActionParamsType {
12 | mutations: Record>;
13 | actions: Record>;
14 | state: StateType;
15 | }
16 |
17 | export interface MutationType {
18 | (state: T, ...args: any): void;
19 | }
20 | export interface ActionType {
21 | (context: ActionParamsType, ...args: any): Promise
22 | }
23 | export interface GetterType {
24 | (state: T): any
25 | }
26 |
27 | export interface StoreOptionsType {
28 | localStorageName?: string; // if you wish to have synchronized state with local storage, provide the name
29 | }
--------------------------------------------------------------------------------
/src/lib/withStore.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
2 | import { deepRecreate } from 'object-deep-recreate';
3 |
4 | import { ActionsProvider, MutationsProvider, GlobalStoreProvider, GlobalGettersProvider } from './storeContext';
5 | import {
6 | ActionType,
7 | MutationType,
8 | StoreOptionsType,
9 | VuexStoreType
10 | } from './types';
11 | import {
12 | getStoreKeyModuleValues,
13 | getStoreModule,
14 | getStoreModuleName, getStoreStateWithModules, handleStateFillWithLocalValues,
15 | } from './helpers';
16 | import { calcAndSetGettersValues, getGettersInitialValues } from './getters';
17 | import { createStore, ExternalStoreType, StateType } from './externalStore';
18 |
19 | const withStore = (Component: (props: any) => JSX.Element, store: VuexStoreType, options: StoreOptionsType = {}) => (props: any) => {
20 | const initialValues = useRef(getGettersInitialValues(store));
21 | const globalStoreRef = useRef(createStore(store.state));
22 | const globalGettersRef = useRef(createStore(initialValues.current));
23 | const prevGettersRef = useRef>(JSON.parse(JSON.stringify(initialValues.current)));
24 |
25 | const handleGettersValuesSet = useCallback((newValues: InheritedStateType) => {
26 | calcAndSetGettersValues(store, newValues, globalGettersRef.current, prevGettersRef);
27 | }, [options.localStorageName]);
28 |
29 | useEffect(() => {
30 | const stateInitialValues = getStoreStateWithModules(store);
31 |
32 | if (options.localStorageName && stateInitialValues) {
33 | handleStateFillWithLocalValues(stateInitialValues, options.localStorageName);
34 | }
35 |
36 | globalStoreRef.current.setState(() => stateInitialValues);
37 | }, []);
38 |
39 | const mutations = useMemo(() => {
40 | return getMutations(store, globalStoreRef.current, handleGettersValuesSet, options.localStorageName);
41 | }, [handleGettersValuesSet]);
42 |
43 | const actions = useMemo(() => {
44 | const actionsFns = getStoreKeyModuleValues(store, 'actions');
45 | return actionsFns ?? {};
46 | }, []);
47 |
48 | const MemoizedComponent = useMemo(() => memo(Component), []);
49 |
50 | return (
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | const getMutations = (
64 | storeConfig: VuexStoreType,
65 | globalStore: ExternalStoreType,
66 | handleGettersValuesSet: (newValues: T) => void,
67 | storageName?: string,
68 | ) => {
69 | const mutations = getStoreKeyModuleValues(storeConfig, 'mutations');
70 | const mutationNames = Object.keys(mutations);
71 | if (!mutationNames.length) return {};
72 |
73 | const values: Record void> = {};
74 |
75 | mutationNames.forEach(mutationName => {
76 | const originalFn = mutations[mutationName] as MutationType;
77 | values[mutationName] = (...args) => {
78 | if (!globalStore) {
79 | throw new Error('No store found')
80 | }
81 |
82 | const setter = (state: StateType) => {
83 | const prevStateCloned: T = JSON.parse(JSON.stringify(state));
84 | const moduleNames = mutationName.split('/');
85 |
86 | // alter the state with the logic given in the store config
87 | if (moduleNames.length === 1) {
88 | originalFn(state, ...args)
89 | } else {
90 | const moduleName = getStoreModuleName(mutationName);
91 | const moduleState = getStoreModule(state, moduleName);
92 | originalFn(moduleState as T, ...args)
93 | }
94 |
95 | const newValues: T = deepRecreate(state, prevStateCloned) as T
96 |
97 | handleGettersValuesSet(newValues);
98 | if (storageName) {
99 | localStorage.setItem(storageName, JSON.stringify(newValues))
100 | }
101 | return newValues;
102 | }
103 |
104 | globalStore.setState(setter);
105 | }
106 | })
107 |
108 | return values;
109 | }
110 |
111 | export default withStore;
112 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "lib/esm",
4 | "module": "esnext",
5 | "target": "es5",
6 | "lib": ["es6", "dom", "es2016", "es2017"],
7 | "jsx": "react",
8 | "declaration": true,
9 | "moduleResolution": "node",
10 | "noUnusedLocals": true,
11 | "noUnusedParameters": true,
12 | "esModuleInterop": true,
13 | "noImplicitReturns": true,
14 | "noImplicitThis": true,
15 | "strictNullChecks": true,
16 | "suppressImplicitAnyIndexErrors": true,
17 | "allowSyntheticDefaultImports": true
18 | },
19 | "include": ["src"],
20 | "exclude": ["node_modules", "lib"]
21 | }
22 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "js-tokens@^3.0.0 || ^4.0.0":
6 | version "4.0.0"
7 | resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
8 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
9 |
10 | loose-envify@^1.1.0:
11 | version "1.4.0"
12 | resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
13 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
14 | dependencies:
15 | js-tokens "^3.0.0 || ^4.0.0"
16 |
17 | object-deep-recreate@^1.0.1:
18 | version "1.0.1"
19 | resolved "https://registry.yarnpkg.com/object-deep-recreate/-/object-deep-recreate-1.0.1.tgz#93f548f4dceb94cbf5fe26a9b9426d4d8a321d80"
20 | integrity sha512-CS0znAjOutpuqGWCFizxFoFcqpa7FH3mxqXNa7Tn7w/7SvWjDFJX2JxB7cvI2JATEr8iCKmmLIEipM3fIE/E2w==
21 |
22 | react@^18.2.0:
23 | version "18.2.0"
24 | resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
25 | integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
26 | dependencies:
27 | loose-envify "^1.1.0"
28 |
29 | typescript@^4.1.2:
30 | version "4.3.5"
31 | resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz"
32 | integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
33 |
--------------------------------------------------------------------------------