├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── TODO.md
├── assets
├── h4r-logo-with-full-name.afdesign
├── h4r-vs-reduxToolkit-advanced-example.png
├── h4r-vs-reduxToolkit-intermediate-example.png
└── hooks-for-redux-logo.afdesign
├── examples
├── comparison-hooks-for-redux
│ ├── .DS_Store
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── .DS_Store
│ │ └── index.html
│ └── src
│ │ ├── App.js
│ │ ├── Greeting.js
│ │ ├── index.css
│ │ └── index.js
├── comparison-plain-redux
│ ├── .DS_Store
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── .DS_Store
│ │ └── index.html
│ └── src
│ │ ├── App.js
│ │ ├── actions.js
│ │ ├── greetingReducer.js
│ │ ├── index.css
│ │ ├── index.js
│ │ └── store.js
├── hooks-for-redux-comparison.png
├── middleware
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ └── index.js
├── tiny-todo
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── .DS_Store
│ │ └── index.html
│ └── src
│ │ └── index.js
├── tiny
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── .DS_Store
│ │ └── index.html
│ └── src
│ │ └── index.js
└── typescript-example
│ ├── .gitignore
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ ├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── MyModel.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ └── setupTests.ts
│ └── tsconfig.json
├── index.d.ts
├── index.js
├── package-lock.json
├── package.json
└── src
├── Provider.js
├── VirtualStore.js
├── createReduxModule.js
├── createStore.js
├── index.js
├── storeRegistry.js
├── tests
├── __snapshots__
│ └── createReduxModule.test.js.snap
├── createReduxModule.test.js
├── createStore.test.js
└── storeRegistry.test.js
└── useRedux.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | WIP/
3 | .DS_Store
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | examples/
2 | WIP/
3 | assets/
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v2.0.0
2 |
3 | Dispatched action types are now qualified by their store key. Most people should see no difference. If you were previously relying on the implicit match across redux modules, your code may break.
4 |
5 | # v1.3.0
6 |
7 | * NEW: `createReduxModule` - same API as useRedux, but new name.
8 |
9 | > We changed the name so as to not erroneously trigger the React warning: https://reactjs.org/warnings/invalid-hook-call-warning.html. It's also a better name.
10 |
11 | * DEPRECATED: `useRedux` - use `createReduxModule` instead (same API!)
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) General UI LLC and its affiliates.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hooks-for-redux (H4R)
2 |
3 | > still redux, half the code, built to scale
4 |
5 | [Redux](https://www.npmjs.com/package/redux) has many wonderful traits, but brevity isn't one of them. Verbose code is not only tedious to write, but it increases the chance of bugs.
6 |
7 | Hooks-for-redux's goal is to reduce the amount of boilerplate code required to define and manage Redux state while maximizing capability and compatibility with the Redux ecosystem.
8 |
9 | The primary strategy is to [DRY](https://www.essenceandartifact.com/2016_06_01_archive.html#dry) up the API and use reasonable defaults, with overrides, wherever possible. H4R streamlines reducers, actions, dispatchers, store-creation and hooks for React. In the same way that React added "hooks" to clean up Component state management, hooks-for-redux uses a similar, hooks-style API to clean up Redux state management.
10 |
11 | The result is a elegant API with 2-3x reduction in client code and near total elimination of all the boilerplate code needed to use plain Redux.
12 |
13 | > H4R implements the [Modular Redux Design Pattern](https://medium.com/@shanebdavis/modular-redux-a-design-pattern-for-mastering-scalable-shared-state-82d4abc0d7b3).
14 |
15 | ## Contents
16 |
17 | 1. [ Install](#install)
18 | 1. [ Usage](#usage)
19 | 1. [ Comparison](#comparison)
20 | 1. [ Tutorial](#tutorial)
21 | 1. [ API](#api)
22 | 1. [ How it Works](#how-it-works)
23 | 1. [ TypeScript](#typescript)
24 | 1. [ Prior Work](#prior-work) (Redux Toolkit and others)
25 | 1. [ Additional Resources](#additional-resources)
26 | 1. [ Contribution](#contribution)
27 | 1. [ License](#license)
28 | 1. [ Produced at GenUI](#produced-at-genui)
29 |
30 | ## Install
31 |
32 | ```
33 | npm install hooks-for-redux
34 | ```
35 |
36 | ## Usage
37 |
38 | Tiny, complete example. See below for explanations.
39 |
40 | ```jsx
41 | import React from "react";
42 | import ReactDOM from "react-dom";
43 | import { createReduxModule, Provider } from "hooks-for-redux";
44 |
45 | const [useCount, { inc, add, reset }] = createReduxModule("count", 0, {
46 | inc: state => state + 1,
47 | add: (state, amount) => state + amount,
48 | reset: () => 0,
49 | });
50 |
51 | const App = () => (
52 |
56 | );
57 |
58 | ReactDOM.render(
59 |
60 |
61 | ,
62 | document.getElementById("root")
63 | );
64 | ```
65 |
66 | - source: [examples/tiny](examples/tiny)
67 |
68 | ## Comparison
69 |
70 | This is a quick comparison of a simple app implemented with both plain Redux and hooks-for-redux. In this example, 66% of redux-specific code was eliminated.
71 |
72 | View the source:
73 |
74 | - [comparison-plain-redux](examples/comparison-plain-redux)
75 | - [comparison-hooks-for-redux](examples/comparison-hooks-for-redux)
76 |
77 | This example is primarily intended to give a visual feel for how much code can be saved. Scroll down to learn more about what's going on.
78 |
79 | 
80 |
81 | ## Tutorial
82 |
83 | #### Tutorial A: Use and Set
84 |
85 | The core of hooks-for-redux is the `createReduxModule` method. There are two ways to call createReduxModule - with and without custom reducers. This first tutorial shows the first, easiest way to use hooks-for-redux.
86 |
87 | > Concept: `createReduxModule` initializes redux state under the property-name you provide and returns an array, containing three things:
88 | >
89 | > 1. react-hook to access named-state
90 | > 2. dispatcher-function to update that state
91 | > 3. virtual store
92 |
93 | First, you'll need to define your redux state.
94 |
95 | ```jsx
96 | // NameReduxState.js
97 | import { createReduxModule } from "hooks-for-redux";
98 |
99 | // - initialize redux state.count = 0
100 | // - export useCount hook for use in components
101 | // - export setCount to update state.count
102 | export const [useCount, setCount] = createReduxModule("count", 0);
103 | ```
104 |
105 | Use your redux state:
106 |
107 | - add a "+" button that adds 1 to count
108 | - useCount()
109 | - returns the current count
110 | - re-renders when count changes
111 |
112 | ```jsx
113 | // App.jsx
114 | import React from 'react';
115 | import ReactDOM from 'react-dom';
116 | import {useCount, setCount} from './NameReduxState.js'
117 |
118 | export default () => {
119 | const count = useCount()
120 | const inc = () => setCount(count + 1)
121 |
122 | Count: {count}
123 | {' '}
124 |
125 | }
126 | ```
127 |
128 | The last step is to wrap your root component with a Provider. H4R provides a streamlined version of the Provider component from [react-redux](https://react-redux.js.org/) to make your redux store available to the rest of your app. H4R's Provider automatically connects to the default store:
129 |
130 | ```jsx
131 | // index.jsx
132 | import React from "react";
133 | import { Provider } from "hooks-for-redux";
134 | import App from "./App";
135 |
136 | ReactDOM.render(
137 |
138 |
139 | ,
140 | document.getElementById("root")
141 | );
142 | ```
143 |
144 | And that's all you need to do! Now, let's look at a fuller tutorial with custom reducers.
145 |
146 | #### Tutorial B: Custom Reducers
147 |
148 | Instead of returning the raw update reducer, you can build your own reducers. Your code will be less brittle and more testable the more specific you can make your transactional redux update functions ('reducers').
149 |
150 | > Concept: When you pass a reducer-map as the 3rd argument, createReduxModule returns set of matching map of dispatchers, one for each of your reducers.
151 |
152 | This example adds three reducer/dispatcher pairs: `inc`, `dec` and `reset`.
153 |
154 | ```jsx
155 | // NameReduxState.js
156 | import { createReduxModule } from "hooks-for-redux";
157 |
158 | export const [useCount, { inc, add, reset }] = createReduxModule("count", 0, {
159 | inc: state => state + 1,
160 | add: (state, amount) => state + amount,
161 | reset: () => 0,
162 | });
163 | ```
164 |
165 | Now the interface supports adding 1, adding 10 and resetting the count.
166 |
167 | ```jsx
168 | // App.jsx
169 | import React from "react";
170 | import { useCount, inc, add, reset } from "./NameReduxState.js";
171 |
172 | export default () => (
173 |
177 | );
178 | ```
179 |
180 | > Use `index.js` from Example-A to complete this app.
181 |
182 | #### Tutorial: Custom Middleware
183 |
184 | You may have noticed none of the code above actually calls Redux.createStore(). H4R introduces the concept of a default store accessible via the included `getStore()` and `setStore()` functions. The first time `getStore()` is called, a new redux store is automatically created for you. However, if you want to control how the store is created, call `setStore()` and pass in your custom store before calling `getStore` or any other function which calls it indirectly including `createReduxModule` and `Provider`.
185 |
186 | Below is an example of creating your own store with some custom middleware. It uses H4R's own createStore method which extends Redux's create store as required for H4R. More on that below.
187 |
188 | ```jsx
189 | // store.js
190 | import { setStore, createStore } from "hooks-for-redux";
191 | import { applyMiddleware } from "redux";
192 |
193 | // example middle-ware
194 | const logDispatch = store => next => action => {
195 | console.log("dispatching", action);
196 | return next(action);
197 | };
198 |
199 | export default setStore(createStore({}, applyMiddleware(logDispatch)));
200 | ```
201 |
202 | ```jsx
203 | // index.jsx
204 | import React from "react";
205 | import "./store"; // <<< import before calling createReduxModule or Provider
206 | import { Provider } from "hooks-for-redux";
207 | import App from "./App";
208 |
209 | ReactDOM.render(
210 |
211 |
212 | ,
213 | document.getElementById("root")
214 | );
215 | ```
216 |
217 | > NOTE: You don't have to use hooks-for-redux's createStore, but setStore must be passed a store that supports the injectReducer method as described here: https://redux.js.org/api/combinereducers
218 |
219 | #### Advanced Examples
220 |
221 | If you are interested in seeing a more complicated example in TypeScript with asynchronous remote requests, please see:
222 |
223 | - [ H4R vs Redux-Toolkit Advanced TypeScript Tutorial ](#h4r-vs-redux-toolkit-advanced-typescript-tutorial)
224 |
225 | ## API
226 |
227 | ### createReduxModule
228 |
229 | ```jsx
230 | import {createReduxModule} from 'hooks-for-redux'
231 | createReduxModule(reduxStorePropertyName, initialState) =>
232 | [useMyStore, setMyStore, virtualStore]
233 |
234 | createReduxModule(reduxStorePropertyName, initialState, reducers) =>
235 | [useMyStore, myDispatchers, virtualStore]
236 | ```
237 |
238 | Define a top-level property of the redux state including its initial value, all related reducers, and returns a react-hook, dispatchers and virtualStore.
239 |
240 | - **IN**: (reduxStorePropertyName, initialState, reducers)
241 |
242 | - reduxStorePropertyName: string
243 | - initialState: non-null, non-undefined
244 | - reducers: object mapping action names to reducers
245 | - e.g. `{myAction: (state, payload) => newState}`
246 |
247 | - **OUT**: [useMyStore, setMyStore -or- myDispatchers, virtualStore]
248 | - useMyStore: react hook returning current state
249 | - One of the following:
250 | - setMyStore: `(newState) => dispatcher-results`
251 | - myDispatchers: object mapping action names to dispatchers
252 | - `{myAction: (payload) => dispatcher-results}`}
253 | - virtualStore: object with API similar to a redux store, but just for the state defined in this createReduxModule call
254 |
255 | #### useMyStore
256 |
257 | ```jsx
258 | const [useMyStore] = createReduxModule(reduxStorePropertyName, initialState)
259 | const MyComponent = () => { // must be used in render function
260 | useMyStore(selector = undefined) => current state
261 | // ...
262 | }
263 | ```
264 |
265 | - **IN**: (selector?, comparator?) =>
266 | - selector (optional): `(state) => selectorResult` default: `(state) => state`
267 | - Optionally, you can provide a selector function taking the current state as input and returning anything.
268 | - Typically, one returns a sub-slice of the current state, but one can return anything.
269 | - The selector function should be deterministic and "pure" (i.e. it ONLY depends on its inputs).
270 | - comparator (optional): `(selectorResultA, selectorResultB) => boolean` default: `(a, b) => a === b`
271 | - Compares the current and previous return values of the selector function and tests if they are the same.
272 | - If comparator returns `false`, the enclosing component will re-render.
273 | - **OUT**: current state
274 | - **REQUIRED**: must be called within a Component's render function
275 | - **EFFECT**:
276 | - Establishes a subscription for any component that uses it. The component will re-render whenever `update` is called, and `useMyStore` will return the latest, updated value within that render.
277 | - Note, this hook will only trigger re-renders if the `comparator` function returns `false`.
278 |
279 | #### myDispatchers
280 |
281 | ```jsx
282 | const [__, {myAction}] = createReduxModule(reduxStorePropertyName, initialState, {
283 | myAction: (state, payload) => state
284 | })
285 | myAction(payload) => {type: MyAction, payload}
286 | ```
287 |
288 | - **IN**: payload - after dispatching, will arrive as the payload for the matching reducer
289 | - **OUT**: {type, payload}
290 | - type: the key string for the matching reducer
291 | - payload: the payload that was passed in
292 | - i.e. same as plain redux's store.dispatch()
293 |
294 | ### virtualStore API
295 |
296 | The virtual store is an object similar to the redux store, except it is only for the redux-state you created with createReduxModule. It supports a similar, but importantly different API from the redux store:
297 |
298 | #### virtualStore.getState
299 |
300 | ```jsx
301 | import {createReduxModule, getStore} from 'hooks-for-redux'
302 | const [,, myVirtualStore] = createReduxModule("myStateName", myInitialState)
303 | myVirtualStore.getState() =>
304 | getStore().getState()["myStateName"]
305 | ```
306 |
307 | The getState method works exactly like a redux store except instead of returning the state of the entire redux store, it returns only the sub portion of that redux state defined by the createReduxModule call.
308 |
309 | - **IN**: (nothing)
310 | - **OUT**: your current state
311 |
312 | #### virtualStore.subscribe
313 |
314 | ```jsx
315 | import {createReduxModule, getStore} from 'hooks-for-redux'
316 | const [,, myVirtualStore] = createReduxModule("myStateName", myInitialState)
317 | myVirtualStore.subscribe(callback) => unsubscribe
318 | ```
319 |
320 | - **IN**: callback(currentState => ...)
321 | - **OUT**: unsubscribe()
322 |
323 | The subscribe method works a little differently from a redux store. Like reduxStore.subscribe, it too returns a function you can use to unsubscribe. Unlike reduxStore.subscribe, the callback passed to virtualStore.subscribe has two differences:
324 |
325 | 1. callback is passed the current value of the virtualStore directly (same value returned by virtualStore.getState())
326 | 2. callback is _only_ called when virtualStore's currentState !== its previous value.
327 |
328 | ### Provider
329 |
330 | ```jsx
331 | import {Provider} from 'hooks-for-redux'
332 | {/* render your App's root here*/}
333 | ```
334 |
335 | hooks-for-redux includes its own `Provider` component shortcut. It is equivalent to:
336 |
337 | ```jsx
338 | import {Provider} from 'react-redux'
339 | import {getState} from 'hooks-for-redux'
340 |
341 |
342 | {/* render your App's root here*/}
343 |
344 | ```
345 |
346 | ### Store Registry API
347 |
348 | Getting started, you can ignore the store registry. Its goal is to automatically manage creating your store and making sure all your code has access. However, if you want to customize your redux store, it's easy to do (see the [custom middleware example](#example-custom-middleware) above).
349 |
350 | #### getStore
351 |
352 | ```jsx
353 | import {getStore} from 'hooks-for-redux'
354 | getStore() => store
355 | ```
356 |
357 | Auto-vivifies a store if setStore has not been called. Otherwise, it returns the store passed to setStore.
358 |
359 | - **IN**: nothing
360 | - **OUT** : redux store
361 |
362 | #### setStore
363 |
364 | ```jsx
365 | import {setStore} from 'hooks-for-redux'
366 | setStore(store) => store
367 | ```
368 |
369 | Call setStore to provide your own store for hooks-for-redux to use. You'll need to use this if you want to use middleware.
370 |
371 | - **IN**: any redux store supporting .injectReducer
372 | - **OUT**: the store passed in
373 | - **REQUIRED**:
374 | - can only be called once
375 | - must be called before getStore or createReduxModule
376 |
377 | #### createStore
378 |
379 | ```jsx
380 | import {createStore} from 'hooks-for-redux'
381 | createStore(reducersMap, [preloadedState], [enhancer]) => store
382 | ```
383 |
384 | Create a basic redux store with injectReducer support. Use this to configure your store's middleware.
385 |
386 | - **IN**
387 |
388 | - reducersMap: object suitable for Redux.combineReducers https://redux.js.org/api/combinereducers
389 | - **OPTIONAL**: preloadedState & enhancer: see Redux.createStore https://redux.js.org/api/createstore
390 |
391 | - **OUT**: redux store supporting .injectReducer
392 |
393 | #### store.injectReducer
394 |
395 | ```jsx
396 | store.injectReducer(reducerName, reducer) => ignored
397 | ```
398 |
399 | If you just want to use Redux's createStore with custom parameters, see the [Custom Middleware Example](#example-custom-middleware). However, if you want to go further and provide your own redux store, you'll need to implement `injectReducer`.
400 |
401 | - **IN**:
402 |
403 | - reducerName: String
404 | - reducer: (current-reducer-named-state) => nextState
405 |
406 | - **EFFECT**: adds reducer to the reducersMaps passed in at creation time.
407 | - **REQUIRED**:
408 | - {[reducerName]: reducer} should be suitable for React.combineReducers https://redux.js.org/api/combinereducers
409 |
410 | Hooks-for-redux requires a store that supports the injectReducer. You only need to worry about this if you are using setState to manually set your store _and_ you are note using hooks-for-redux's own createStore function.
411 |
412 | The injectReducer method is described here https://redux.js.org/recipes/code-splitting. Its signature looks like:
413 |
414 | > NOTE: Just as any other reducer passed to React.combineReducers, the reducer passed to injectReducer doesn't get passed the store's entire state. It only gets passed, and should return, its own state data which is stored in the top-level state under the provided reducerName.
415 |
416 | ## How it Works
417 |
418 | Curious what's happening behind the scenes? This is a tiny library for all the capabilities it gives you. Below is a quick overview of what's going on.
419 |
420 | > Note: All code-examples in this section are approximations of the actual code. Minor simplifications were applied for the purpose of instruction and clarity. See the latest [source](src/) for complete, up-to-date implementations.
421 |
422 | #### Dependencies
423 |
424 | To keep things simple, this library has only two dependencies: [redux](https://www.npmjs.com/package/redux) and [react-redux](https://www.npmjs.com/package/react-redux). In some ways, H4R is just a set of elegant wrappers for these two packages.
425 |
426 | #### Store Registry
427 |
428 | - source: [src/storeRegistry.js](src/storeRegistry.js)
429 |
430 | You might notice when using hooks-for-redux, you don't have to manually create your store, nor do you need to reference your store explicitly anywhere in your application. [Redux recommends](https://redux.js.org/faq/store-setup#can-or-should-i-create-multiple-stores-can-i-import-my-store-directly-and-use-it-in-components-myself) only using one store per application. H4R codifies that recommendation and defines a central registry to eliminate the need to explicitly pass the store around.
431 |
432 | The implementation is straight forward:
433 |
434 | ```jsx
435 | let store = null;
436 | const getStore = () => (store ? store : (store = createStore()));
437 | const setStore = initialStore => (store = initialStore);
438 | ```
439 |
440 | #### Provider
441 |
442 | - source: [src/Provider.js](src/Provider.js)
443 |
444 | H4R wraps the react-redux [Provider](https://react-redux.js.org/api/provider#provider), combining it with a default store from the store registry. It reduces a small, but significant amount of boilerplate.
445 |
446 | ```jsx
447 | const Provider = ({ store = getStore(), context, children }) =>
448 | React.createElement(ReactReduxProvider, { store, context }, children);
449 | ```
450 |
451 | #### createReduxModule
452 |
453 | - source: [src/createReduxModule.js](src/createReduxModule.js)
454 |
455 | H4R's biggest win comes from one key observation: _if you are writing your own routing, you are doing it wrong._ The same can be said for dispatching and subscriptions.
456 |
457 | The `createReduxModule` function automates all the manual routing required to make plain Redux work. It inputs only the essential data and functions necessary to define a redux model, and it returns all the tools you need to use it.
458 |
459 | The implementation of createReduxModule is surprisingly brief. Details are explained below:
460 |
461 | ```jsx
462 | const createReduxModule = (storeKey, initialState, reducers, store = getStore()) => {
463 | /* 1 */ store.injectReducer(storeKey, (state = initialState, { type, payload }) =>
464 | reducers[type] ? reducers[type](state, payload) : state
465 | );
466 |
467 | return [
468 | /* 2 */ () => useSelector(storeState => storeState[storeKey]),
469 | /* 3 */ mapKeys(reducers, type => payload => store.dispatch({ type, payload })),
470 | /* 4 */ createVirtualStore(store, storeKey),
471 | ];
472 | };
473 | ```
474 |
475 | 1. H4R's redux store uses the [injectReducer pattern recommended by Redux](https://redux.js.org/api/combinereducers) to add your reducers to the store. Because the reducers are defined as an object, routing is dramatically simplified. Instead of a huge switch-statement, reducer routing can be expressed as one line no matter how many reducers there are.
476 | 2. The returned React Hook wraps react-redux's [useSelector](https://react-redux.js.org/next/api/hooks#useselector), selecting your state.
477 | 3. The returned dispatchers object is generated from the reducers passed in. The `type` value is set from each key in reducers. The dispatchers themselves take a payload as input and return the standard result of Redux's [dispatch](https://redux.js.org/api/store#dispatchaction) function.
478 | 4. Last, a new virtual-store is created for your redux model. See below for details.
479 |
480 | #### VirtualStore
481 |
482 | - source: [src/VirtualStore.js](src/VirtualStore.js)
483 |
484 | The VirtualStore object allows you to access your state, a value bound to the Redux store via your storeKey, as-if it were a Redux store. It is implemented, again, as simple wrappers binding the virtual store to the state defined in createReduxModule.
485 |
486 | ```jsx
487 | const createVirtualStore = (store, storeKey) => {
488 | const /* 1 */ getState = () => store.getState()[storeKey];
489 | return {
490 | getState,
491 | /* 2 */ subscribe: f => {
492 | let lastState = getState();
493 | return store.subscribe(() => lastState !== getState() && f((lastState = getState())));
494 | },
495 | };
496 | };
497 | ```
498 |
499 | 1. `getState` wraps Redux's [getState](https://redux.js.org/api/store#getstate) and returns the state of your storeKey.
500 | 2. `subscribe` wraps Redux's [subscribe](https://redux.js.org/api/store#subscribelistener), but it provides some additional functionality:
501 | - It only calls `f` if your state changed (using a `!==` test). In Redux's subscribe, `f` is "called any time an action is dispatched" - which is extremely wasteful.
502 | - `f` is passed your current state, so you don't have to manually call getState.
503 |
504 | ## TypeScript
505 |
506 | TypeScript support is provided in the library. Configuring the generics for H4R was tricky, particularly for the createReduxModule method. Please send feedback on how we can improve the typing.
507 |
508 | - [hooks-for-redux type definition](index.d.ts)
509 |
510 | ## Prior Work
511 |
512 | Several people have attempted to simplify Redux and/or make it act more like React hooks, but none have succeeded in providing a general-purpose, fully DRY solution.
513 |
514 | - https://www.npmjs.com/package/edux
515 | - https://www.npmjs.com/package/no-boilerplate-redux
516 | - https://www.npmjs.com/package/reduxless
517 | - https://www.npmjs.com/package/redux-actions
518 | - https://www.npmjs.com/package/redux-arc
519 | - https://www.npmjs.com/package/@finn-no/redux-actions
520 | - https://www.npmjs.com/package/@mollycule/redux-hook
521 | - https://www.npmjs.com/package/easy-peasy
522 |
523 | ### What about Redux Toolkit?
524 |
525 | > Redux Toolkit: The official, opinionated, batteries-included tool set for efficient Redux development - https://redux-toolkit.js.org
526 |
527 | Redux-Toolkit claims to be efficient, but when compared to H4R it still falls far short. I'll give an example.
528 |
529 | #### H4R vs Redux-Toolkit Intermediate-Example
530 |
531 | > 58% less code
532 |
533 | Taking from the intermediate code-example provided in the Redux-Toolkit Package:
534 |
535 | Redux-Toolkit's implementation:
536 |
537 | - tutorial: [redux-toolkit.js.org](https://redux-toolkit.js.org/tutorials/intermediate-tutorial)
538 | - interactive: [codesandbox.io](https://codesandbox.io/s/rtk-convert-todos-example-uqqy3)
539 | - ~390 lines of JavaScript
540 |
541 | I reduced the code by about 2x using H4R - including eliminating several files. Even the tests got simpler.
542 |
543 | H4R solution
544 |
545 | - interactive: [codesandbox.io](https://codesandbox.io/s/github/shanebdavis/rtk-convert-todos-example-h4r-conversion)
546 | - source: [github](https://github.com/shanebdavis/rtk-convert-todos-example-h4r-conversion)
547 | - ~160 lines of JavaScript
548 |
549 | Here is an apples-to-apples comparison of some of the main files from each project:
550 |
551 | - [Redux Toolkit gist - 104 lines](https://gist.github.com/shanebdavis/9e67be8a0874a4c295001ba6e91f79e2)
552 | - [Hooks-for-redux gist - 52 lines](https://gist.github.com/shanebdavis/ce02b4495f1bc0afa830796f58124604)
553 |
554 | Perhaps the most dramatic difference is how H4R simplifies the interdependencies between files. Boxes are files, lines are imports:
555 |
556 | 
557 |
558 | Part of the key is how well H4R links into React. Redux-toolkit takes 50 lines of code just to do this.
559 |
560 | ```javascript
561 | import React from "react";
562 | import Todo from "./Todo";
563 | import { useFilters } from "../filters/filtersSlice";
564 | import { useTodos } from "./todosSlice";
565 |
566 | export const VisibleTodoList = () => (
567 |
574 | );
575 | ```
576 |
577 | NOTE: The normal use of H4R is React-specific while Redux-Toolkit is agnostic to the rendering engine. Let me know if there is interest in non-react H4R support. It shouldn't be hard to do.
578 |
579 | #### H4R vs Redux-Toolkit Advanced TypeScript Tutorial
580 |
581 | > 48% less code
582 |
583 | Now to take on a bigger challenge. The advanced tutorial is a capable github issue and issue-comment-browser. Even here, H4R shines. Redux-Toolkit has two main problems:
584 |
585 | 1. It still makes you manually dispatch your updates. H4R avoids making you manually create and dispatch your actions entirely by returning ready-to-use dispatchers. They just look like normal functions you can call to start your updates.
586 | 2. Redux-Toolkit's pattern mixes business-logic with view-logic. Redux-related code, particularly updates, should never be in the same files as view and view-logic files like components.
587 |
588 | Blending UX-logic with business-logic creates excessive dependencies between modules. This dependency hell literally took me hours to unwind before I could convert it to H4R. Once I was done, though, it all simplified and became clear and easy to edit. If you open the code you will see that all the business logic in the H4R solution resides in the src/redux folder in _4 files and 100 lines of code - total_. All the components are clean and have zero business logic.
589 |
590 | For example, compare the `IssueListPage.tsx` from each project:
591 |
592 | ```typescript
593 | import React from "react";
594 | import { useIssues } from "redux/issues";
595 | import { RepoSearchForm } from "./IssuesListLib/RepoSearchForm";
596 | import { IssuesPageHeader } from "./IssuesListLib/IssuesPageHeader";
597 | import { IssuesList } from "./IssuesListLib/IssuesList";
598 | import { IssuePagination } from "./IssuesListLib/IssuePagination";
599 |
600 | export const IssuesListPage = () => {
601 | const { loading, error } = useIssues();
602 | return error ? (
603 |
604 |
Something went wrong...
605 |
{error.toString()}
606 |
607 | ) : (
608 |
609 |
610 |
611 | {loading ?
Loading...
: }
612 |
613 |
614 | );
615 | };
616 | ```
617 |
618 | - [github/h4r/IssuesListPage](https://github.com/shanebdavis/rtk-github-issues-example-h4r-conversion/blob/master/src/components/pages/IssuesListPage.tsx)
619 | - 21 lines, 693 bytes
620 |
621 | to this:
622 |
623 | - [github/redux-toolkit/IssuesListPage](https://github.com/reduxjs/rtk-github-issues-example/blob/master/src/features/issuesList/IssuesListPage.tsx)
624 | - 87 lines, 2000 bytes
625 |
626 | Redux-toolkit's solution mixes in the business logic of fetching the remote data. This is all handled by H4R's createReduxModule slices. Further, RT makes IssuesListPage dependent on many things such that it only passes to child-components but never uses itself - a false dependency. For example, the pagination details (currentPage, pageCount, etc...) should only be a dependency of IssuePagination.
627 |
628 | Compare the full source of each project below:
629 |
630 | Redux-Toolkit solution:
631 |
632 | - tutorial: [redux-toolkit.js.org](https://redux-toolkit.js.org/tutorials/advanced-tutorial)
633 | - interactive: [codesandbox.io](https://codesandbox.io/s/rtk-github-issues-example-02-issues-display-tdx2w)
634 | - source: [github](https://github.com/reduxjs/rtk-github-issues-example)
635 | - ~1170 lines of TypeScript
636 |
637 | H4R solution:
638 |
639 | - interactive: [codesandbox.io](https://codesandbox.io/s/github/shanebdavis/rtk-github-issues-example-h4r-conversion)
640 | - source: [github](https://github.com/shanebdavis/rtk-github-issues-example-h4r-conversion)
641 | - ~613 lines of TypeScript
642 |
643 | The file and inter-file dependency reduction is dramatic. With H4R your code will be significantly more agile and easier to adapt to new changes. Boxes are files, lines are imports:
644 |
645 | 
646 |
647 | ## Additional Resources
648 |
649 | Blog Posts:
650 |
651 | - [How I Eliminated Redux Boilerplate with Hooks-for-Redux](https://medium.com/@shanebdavis/how-i-eliminated-redux-boilerplate-with-hooks-for-redux-bd308d5abbdd) - an introduction and explanation of H4R with examples
652 | - [The 5 Essential Elements of Modular Software Design](https://medium.com/@shanebdavis/the-5-essential-elements-of-modular-software-design-6b333918e543) - how and why to write modular code - a precursor to why you should use Modular Redux (e.g. H4R)
653 | - [Modular Redux — a Design Pattern for Mastering Scalable, Shared State in React](https://medium.com/@shanebdavis/modular-redux-a-design-pattern-for-mastering-scalable-shared-state-82d4abc0d7b3) - the Modular Redux design pattern H4R is based on and detailed comparison with Redux Toolkit
654 |
655 | Included Examples:
656 |
657 | - [tiny](./examples/tiny) - the simplest working example
658 | - [tiny-todo](./examples/tiny-todo) - a slightly more detailed example
659 | - [middleware](./examples/middleware) - an example of how to use Redux middleware with H4R
660 | - comparison [plain-redux](./examples/comparison-plain-redux) vs [hooks-for-redux](./examples/hooks-for-redux) - compare two, tiny working examples back-to-back
661 |
662 | Advanced Examples:
663 |
664 | - [todo with filters](https://github.com/shanebdavis/rtk-convert-todos-example-h4r-conversion) (try it now on [codesandbox.io](https://codesandbox.io/s/github/shanebdavis/rtk-convert-todos-example-h4r-conversion))
665 | - [github-issues-browser](https://github.com/shanebdavis/rtk-github-issues-example-h4r-conversion) with typescript and asynchronous requests (try it now on [codesandbox.io](https://codesandbox.io/s/github/shanebdavis/rtk-github-issues-example-h4r-conversion))
666 |
667 | ## Contribution
668 |
669 | If you have suggestions for improvement, please feel free to [start an issue on github](https://github.com/generalui/hooks-for-redux/issues).
670 |
671 | ## License
672 |
673 | hooks-for-redux is [MIT licensed](./LICENSE).
674 |
675 | ## Produced at GenUI
676 |
677 | hooks-for-redux was [developed in JavaScript for React and Redux at GenUI.co](https://www.genui.co).
678 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 |
2 | # "Pure" API: addReducers
3 |
4 | We could create an API that avoided putting functions in the dispatch object. It would also make the dispatch object's 'type' more descriptive (derived from the key of each field passed to addReducers).
5 |
6 | ```js
7 | const [useSubscription, update, addReducers] = useReduxState("items", [])
8 |
9 | const {addItem} = addReducers({
10 | addItem: (items, item) => [...items, item],
11 | })
12 | // addItem = (item) => dispatch-response
13 | ```
14 |
15 | # Simplified Provider
16 |
17 | Instead of:
18 | ```js
19 | import { getStore } from 'hooks-for-redux';
20 | import { Provider } from 'react-redux'
21 |
22 |
23 | ...
24 |
25 | ```
26 |
27 | Why not:
28 | ```js
29 | import { Provider } from 'hooks-for-redux';
30 |
31 |
32 | ...
33 |
34 | ```
35 |
--------------------------------------------------------------------------------
/assets/h4r-logo-with-full-name.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/assets/h4r-logo-with-full-name.afdesign
--------------------------------------------------------------------------------
/assets/h4r-vs-reduxToolkit-advanced-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/assets/h4r-vs-reduxToolkit-advanced-example.png
--------------------------------------------------------------------------------
/assets/h4r-vs-reduxToolkit-intermediate-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/assets/h4r-vs-reduxToolkit-intermediate-example.png
--------------------------------------------------------------------------------
/assets/hooks-for-redux-logo.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/assets/hooks-for-redux-logo.afdesign
--------------------------------------------------------------------------------
/examples/comparison-hooks-for-redux/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/examples/comparison-hooks-for-redux/.DS_Store
--------------------------------------------------------------------------------
/examples/comparison-hooks-for-redux/README.md:
--------------------------------------------------------------------------------
1 | # Comparison-Hooks-For-Redux
2 |
3 | This example is one of two comparing hooks-for-redux with vanilla redux in a very simple app.
4 |
5 | ## Compare
6 |
7 | * comparison-vanilla-redux
8 | * comparison-hooks-for-redux
9 |
10 | ## JavaScript Source
11 |
12 | With hooks-for-redux, you have to write dramatically less code. Here's all the JavaScript source in one place:
13 |
14 | ```jsx
15 |
16 | // greetingReduxState.js
17 | import { createReduxModule } from 'hooks-for-redux'
18 |
19 | const DEFAULT_GREETING = "hello, hooks-for-redux!";
20 |
21 | export const [useGreeting, {resetGreeting, setGreeting}] =
22 | createReduxModule('greeting', DEFAULT_GREETING, {
23 | resetGreeting: () => DEFAULT_GREETING,
24 | setGreeting: (store, greeting) => greeting
25 | });
26 |
27 | // App.js
28 | import React from 'react'
29 | import { useGreeting, resetGreeting, setGreeting }
30 | from './greetingReduxState'
31 |
32 | const App = () =>
33 |