├── .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 Logo
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 |

53 | Count: {useCount()} {" "} 54 | add(10)} /> 55 |

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 | ![hooks-for-redux vs plain-redux comparison](https://raw.githubusercontent.com/wiki/generalui/hooks-for-redux/hooks-for-redux-comparison.png) 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 |

174 | Count: {useCount()} {" "} 175 | add(10)} value="+10" /> 176 |

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 | ![](https://github.com/generalui/hooks-for-redux/raw/master/assets/h4r-vs-reduxToolkit-intermediate-example.png) 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 |
    568 | {useTodos() 569 | .filter(useFilters()) 570 | .map(todo => ( 571 | 572 | ))} 573 |
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 | ![](https://github.com/generalui/hooks-for-redux/raw/master/assets/h4r-vs-reduxToolkit-advanced-example.png) 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 |
34 |

{ useGreeting() }

35 | setGreeting("こんにちは, hooks-for-redux")}> 36 | japanese 37 | resetGreeting()}>reset 38 |
39 | 40 | export default App 41 | 42 | // index.js 43 | import React from 'react' 44 | import ReactDOM from 'react-dom' 45 | import { Provider } from 'hooks-for-redux' 46 | import './index.css' 47 | import App from './App' 48 | 49 | ReactDOM.render(, document.getElementById('root')); 50 | 51 | ``` -------------------------------------------------------------------------------- /examples/comparison-hooks-for-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "private": true, 4 | "dependencies": { 5 | "react": "^16.10.2", 6 | "react-dom": "^16.10.2", 7 | "react-scripts": "^3.2.0", 8 | "hooks-for-redux": "file:../../" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/comparison-hooks-for-redux/public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/examples/comparison-hooks-for-redux/public/.DS_Store -------------------------------------------------------------------------------- /examples/comparison-hooks-for-redux/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/comparison-hooks-for-redux/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useGreeting, setGreeting, resetGreeting } from "./Greeting"; 3 | 4 | const App = () => ( 5 |
6 |

{useGreeting()}

7 | setGreeting("こんにちは, hooks-for-redux")}> 8 | japanese 9 | {" "} 10 | resetGreeting()}> 11 | reset 12 | 13 |
14 | ); 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /examples/comparison-hooks-for-redux/src/Greeting.js: -------------------------------------------------------------------------------- 1 | import { createReduxModule } from "hooks-for-redux"; 2 | 3 | const DEFAULT_GREETING = "hello, hooks-for-redux!"; 4 | 5 | export const [useGreeting, { resetGreeting, setGreeting }] = createReduxModule( 6 | "greeting", 7 | DEFAULT_GREETING, 8 | { 9 | resetGreeting: () => DEFAULT_GREETING, 10 | setGreeting: (store, greeting) => greeting 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /examples/comparison-hooks-for-redux/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 3 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 4 | sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | background-color: #282c34; 8 | color: white; 9 | } 10 | 11 | .h1 { 12 | font-size: 16px; 13 | } 14 | 15 | a { 16 | font-size: 14px; 17 | color: rgba(224, 112, 0, 1.000); 18 | } -------------------------------------------------------------------------------- /examples/comparison-hooks-for-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "hooks-for-redux"; 4 | import "./index.css"; 5 | import App from "./App"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById("root") 12 | ); 13 | -------------------------------------------------------------------------------- /examples/comparison-plain-redux/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/examples/comparison-plain-redux/.DS_Store -------------------------------------------------------------------------------- /examples/comparison-plain-redux/README.md: -------------------------------------------------------------------------------- 1 | # Comparison-Vanilla-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 vanilla redux, you have to write dramatically **more** code. Here's all the JavaScript source in one place: 13 | 14 | ```javascript 15 | // actions.js 16 | const SET_GREETING = 'SET_GREETING'; 17 | const RESET_GREETING = 'RESET_GREETING'; 18 | export { 19 | SET_GREETING, 20 | RESET_GREETING 21 | } 22 | 23 | // greetingReducer.js 24 | import {SET_GREETING, RESET_GREETING} from './actions' 25 | const DEFAULT_GREETING = "hello, vanilla redux"; 26 | 27 | const greeting = (state = DEFAULT_GREETING, action) => { 28 | switch (action.type) { 29 | case SET_GREETING: 30 | return action.payload; 31 | case RESET_GREETING: 32 | return DEFAULT_GREETING; 33 | default: 34 | return state; 35 | } 36 | } 37 | export default greeting 38 | 39 | // store.js 40 | import {createStore, combineReducers} from 'redux' 41 | import greeting from './greetingReducer' 42 | 43 | const store = createStore(combineReducers({ 44 | greeting 45 | })); 46 | 47 | export default store 48 | 49 | // App.js 50 | import React from 'react' 51 | import {useSelector} from 'react-redux' 52 | import store from './store' 53 | import {SET_GREETING, RESET_GREETING} from './actions' 54 | 55 | const resetGreeting = () => store.dispatch({ 56 | type: RESET_GREETING 57 | }) 58 | 59 | const setGreeting = (greeting) => store.dispatch({ 60 | type: SET_GREETING, 61 | payload: greeting 62 | }) 63 | 64 | const App = () => 65 |
66 |

{ useSelector(({greeting}) => greeting) }

67 | setGreeting("こんにちは, バニラ redux")}> 68 | japanese 69 | resetGreeting()}>reset 70 |
71 | 72 | export default App 73 | 74 | // index.js 75 | import React from 'react' 76 | import ReactDOM from 'react-dom' 77 | import {Provider} from 'react-redux' 78 | 79 | import './index.css' 80 | import App from './App' 81 | import store from './store' 82 | 83 | ReactDOM.render( 84 | , 85 | document.getElementById('root') 86 | ); 87 | ``` 88 | -------------------------------------------------------------------------------- /examples/comparison-plain-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "private": true, 4 | "dependencies": { 5 | "react": "^16.10.2", 6 | "react-dom": "^16.10.2", 7 | "react-scripts": "^3.2.0" 8 | }, 9 | "scripts": { 10 | "start": "react-scripts start", 11 | "build": "react-scripts build", 12 | "test": "react-scripts test", 13 | "eject": "react-scripts eject" 14 | }, 15 | "eslintConfig": { 16 | "extends": "react-app" 17 | }, 18 | "browserslist": { 19 | "production": [ 20 | ">0.2%", 21 | "not dead", 22 | "not op_mini all" 23 | ], 24 | "development": [ 25 | "last 1 chrome version", 26 | "last 1 firefox version", 27 | "last 1 safari version" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/comparison-plain-redux/public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/examples/comparison-plain-redux/public/.DS_Store -------------------------------------------------------------------------------- /examples/comparison-plain-redux/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/comparison-plain-redux/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {useSelector} from 'react-redux' 3 | import store from './store' 4 | import {SET_GREETING, RESET_GREETING} from './actions' 5 | 6 | const resetGreeting = () => store.dispatch({ 7 | type: RESET_GREETING 8 | }) 9 | 10 | const setGreeting = (greeting) => store.dispatch({ 11 | type: SET_GREETING, 12 | payload: greeting 13 | }) 14 | 15 | const App = () => 16 |
17 |

{ useSelector(({greeting}) => greeting) }

18 | setGreeting("こんにちは, バニラ redux")}> 19 | japanese 20 | resetGreeting()}>reset 21 |
22 | 23 | export default App -------------------------------------------------------------------------------- /examples/comparison-plain-redux/src/actions.js: -------------------------------------------------------------------------------- 1 | const SET_GREETING = 'SET_GREETING'; 2 | const RESET_GREETING = 'RESET_GREETING'; 3 | 4 | export { 5 | SET_GREETING, 6 | RESET_GREETING 7 | } -------------------------------------------------------------------------------- /examples/comparison-plain-redux/src/greetingReducer.js: -------------------------------------------------------------------------------- 1 | import {SET_GREETING, RESET_GREETING} from './actions' 2 | const DEFAULT_GREETING = "hello, vanilla redux"; 3 | 4 | const greeting = (state = DEFAULT_GREETING, action) => { 5 | switch (action.type) { 6 | case SET_GREETING: 7 | return action.payload; 8 | case RESET_GREETING: 9 | return DEFAULT_GREETING; 10 | default: 11 | return state; 12 | } 13 | } 14 | export default greeting -------------------------------------------------------------------------------- /examples/comparison-plain-redux/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 3 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 4 | sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | background-color: #282c34; 8 | color: white; 9 | } 10 | 11 | .h1 { 12 | font-size: 16px; 13 | } 14 | 15 | a { 16 | font-size: 14px; 17 | color: rgba(224, 112, 0, 1.000); 18 | } -------------------------------------------------------------------------------- /examples/comparison-plain-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import "./index.css"; 5 | import App from "./App"; 6 | import store from "./store"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | -------------------------------------------------------------------------------- /examples/comparison-plain-redux/src/store.js: -------------------------------------------------------------------------------- 1 | import {createStore, combineReducers} from 'redux' 2 | import greeting from './greetingReducer' 3 | 4 | const store = createStore(combineReducers({ 5 | greeting 6 | })); 7 | 8 | export default store -------------------------------------------------------------------------------- /examples/hooks-for-redux-comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/examples/hooks-for-redux-comparison.png -------------------------------------------------------------------------------- /examples/middleware/README.md: -------------------------------------------------------------------------------- 1 | # Simple Middleware Example 2 | 3 | This will log every dispatched event to the console. 4 | 5 | You can install the Redux devtools for chrome for even more awesomeness! 6 | 7 | * https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd -------------------------------------------------------------------------------- /examples/middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "private": true, 4 | "dependencies": { 5 | "hooks-for-redux": "file:../../", 6 | "react": "^16.13.1", 7 | "react-dom": "^16.13.1", 8 | "react-scripts": "^3.4.1", 9 | "redux-devtools-extension": "^2.13.8" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": { 21 | "production": [ 22 | ">0.2%", 23 | "not dead", 24 | "not op_mini all" 25 | ], 26 | "development": [ 27 | "last 1 chrome version", 28 | "last 1 firefox version", 29 | "last 1 safari version" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/middleware/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | H4R Todo 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/middleware/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { applyMiddleware } from "redux"; 4 | import { composeWithDevTools } from "redux-devtools-extension"; 5 | import { Provider, createReduxModule, createStore, setStore } from "hooks-for-redux"; 6 | 7 | // example middleware 8 | const logDispatch = store => next => action => { 9 | console.log("dispatching", action); 10 | return next(action); 11 | }; 12 | 13 | // store.js 14 | export default setStore(createStore({}, composeWithDevTools(applyMiddleware(logDispatch)))); 15 | 16 | // toggleState.js 17 | const [useToggle, { toggleSwitch }] = createReduxModule("toggle", false, { 18 | toggleSwitch: state => !state 19 | }); 20 | 21 | // Toggle.js 22 | const Toggle = () => { 23 | const toggle = useToggle(); 24 | return ( 25 |
26 |
{JSON.stringify(toggle)}
27 | toggleSwitch()} /> 28 |
29 | ); 30 | }; 31 | 32 | // index.js 33 | const App = () => ( 34 | 35 | 36 | 37 | ); 38 | 39 | const rootElement = document.getElementById("root"); 40 | ReactDOM.render(, rootElement); 41 | -------------------------------------------------------------------------------- /examples/tiny-todo/README.md: -------------------------------------------------------------------------------- 1 | # Tiny Counter 2 | 3 | The simplest possible example of hooks-for-redux, as a counter. -------------------------------------------------------------------------------- /examples/tiny-todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "private": true, 4 | "dependencies": { 5 | "hooks-for-redux": "file:../../", 6 | "react": "^16.13.1", 7 | "react-dom": "^16.13.1", 8 | "react-scripts": "^3.4.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/tiny-todo/public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/examples/tiny-todo/public/.DS_Store -------------------------------------------------------------------------------- /examples/tiny-todo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | H4R Todo 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/tiny-todo/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider, createReduxModule } from "hooks-for-redux"; 4 | 5 | // redux/list.js 6 | const getUniqueId = list => 7 | list.length > 0 ? Math.max(...list.map(t => t.id)) + 1 : 1; 8 | 9 | const [useList, { addItem, deleteItem }] = createReduxModule( 10 | "list", 11 | [ 12 | { id: 1, text: "clean the house" }, 13 | { id: 2, text: "buy milk" } 14 | ], 15 | { 16 | addItem: (list, item) => [...list, { ...item, id: getUniqueId(list) }], 17 | deleteItem: (list, item) => list.filter(todo => todo.id !== item.id) 18 | } 19 | ); 20 | 21 | // components/ToDoItem.js 22 | const ToDoItem = ({ item }) => 23 |
  • 24 | {item.text + " "} 25 | 26 |
  • 27 | 28 | // components/ToDo.js 29 | const ToDo = () => { 30 | const [text, setText] = useState(""); 31 | 32 | const createNewToDoItem = () => { 33 | if (!text) return alert("Please enter a text!"); 34 | addItem({ text }); 35 | setText(""); 36 | }; 37 | 38 | return
    39 |

    Todo with hooks-for-redux

    40 |
      41 | {useList().map(item => ( 42 | 43 | ))} 44 |
    45 | 46 | setText(e.target.value)} 50 | onKeyPress={e => e.key === "Enter" && createNewToDoItem()} 51 | /> 52 |   53 | 54 |
    55 | }; 56 | 57 | // index.js 58 | ReactDOM.render(, document.getElementById("root")); -------------------------------------------------------------------------------- /examples/tiny/README.md: -------------------------------------------------------------------------------- 1 | # Tiny Counter 2 | 3 | The simplest possible example of hooks-for-redux, as a counter. -------------------------------------------------------------------------------- /examples/tiny/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "private": true, 4 | "dependencies": { 5 | "hooks-for-redux": "file:../../", 6 | "react": "^16.13.1", 7 | "react-dom": "^16.13.1", 8 | "react-scripts": "^5.0.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/tiny/public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/examples/tiny/public/.DS_Store -------------------------------------------------------------------------------- /examples/tiny/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | H4R Tiny App 7 | 8 | 9 |
    10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/tiny/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {createReduxModule, Provider} from 'hooks-for-redux' 4 | 5 | const [useCount, {inc, add, reset}] = createReduxModule('count', 0, { 6 | inc: (state) => state + 1, 7 | add: (state, amount) => state + amount, 8 | reset: () => 0 9 | }) 10 | 11 | const App = () => 12 |

    13 | Count: {useCount()} 14 | {' '} 15 | {' '} add(10)} /> 16 | {' '} 17 |

    18 | 19 | ReactDOM.render( 20 | , 21 | document.getElementById('root') 22 | ); -------------------------------------------------------------------------------- /examples/typescript-example/.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/typescript-example/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /examples/typescript-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.11.59", 11 | "@types/react": "^18.0.20", 12 | "@types/react-dom": "^18.0.6", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-scripts": "5.0.1", 16 | "typescript": "^4.8.3", 17 | "web-vitals": "^2.1.4", 18 | "hooks-for-redux": "file:../../" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/typescript-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/examples/typescript-example/public/favicon.ico -------------------------------------------------------------------------------- /examples/typescript-example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/typescript-example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/examples/typescript-example/public/logo192.png -------------------------------------------------------------------------------- /examples/typescript-example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/generalui/hooks-for-redux/75d0310781eff23b988446be2316ea06336a89f0/examples/typescript-example/public/logo512.png -------------------------------------------------------------------------------- /examples/typescript-example/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/typescript-example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/typescript-example/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/typescript-example/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/typescript-example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logo from "./logo.svg"; 3 | import "./App.css"; 4 | import * as MyModel from "./MyModel"; 5 | 6 | function App() { 7 | const myModel = MyModel.use(); // myModel should be MyModelState 8 | const isOn = MyModel.use(({ isOn }) => isOn); // isOn should be boolean 9 | 10 | return ( 11 |
    12 |
    13 | logo 14 |

    15 | Edit src/App.tsx and save to reload. 16 |

    17 | 18 | Learn React 19 | 20 |
    21 |
    22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /examples/typescript-example/src/MyModel.tsx: -------------------------------------------------------------------------------- 1 | import { createReduxModule } from "hooks-for-redux"; 2 | 3 | export interface MyModelState { 4 | isOn: boolean; 5 | } 6 | 7 | const initialState: MyModelState = { 8 | isOn: false, 9 | }; 10 | 11 | export const [use, { toggleIsOn }, store] = createReduxModule("myModel", initialState, { 12 | toggleIsOn: (state: MyModelState) => ({ isOn: !state.isOn }), 13 | }); 14 | -------------------------------------------------------------------------------- /examples/typescript-example/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 | -------------------------------------------------------------------------------- /examples/typescript-example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /examples/typescript-example/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/typescript-example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/typescript-example/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /examples/typescript-example/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /examples/typescript-example/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 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | import { Action, Unsubscribe, AnyAction, Store } from "redux"; 4 | import { Component } from "react"; 5 | 6 | /**************************** 7 | util 8 | ****************************/ 9 | type RestParams = TFunction extends (arg: any, ...args: infer A) => void ? A : never; 10 | 11 | /**************************** 12 | hook 13 | ****************************/ 14 | /** 15 | * use(), a React hook for the Redux Model's state 16 | * @param selector optional function to select-from or transform the Redux Model's state. 17 | * @param equalityFn optional function to compare two results from your selector; return true if they are different enough to justify trigging a re-render 18 | * 19 | * `selector` and `equalityFn` work the same as https://react-redux.js.org/api/hooks#useselector. The main difference here is that the selector is optional since 20 | * H4R already applies a pre-selector to select your module's "slice" out of the whole redux state. Note, the pre-selector is *always* applied to the entire redux state, and then your optional selector is applied to your module's state. 21 | */ 22 | interface ReactReduxHookWithOptionalSelector { 23 | ( 24 | selector: (state: TState) => TSelected, 25 | equalityFn?: (left: TSelected, right: TSelected) => boolean 26 | ): TSelected; 27 | } 28 | 29 | interface ReactReduxHookWithOptionalSelector { 30 | (): TState; 31 | } 32 | 33 | /**************************** 34 | reducers 35 | ****************************/ 36 | type Reducer = (state: TState, payload: any) => TState; 37 | 38 | interface Reducers { 39 | [reducerName: string]: Reducer; 40 | } 41 | 42 | /**************************** 43 | virtualStores 44 | ****************************/ 45 | type VirtualStoreWithReducers = { 46 | getState: () => TState; 47 | getReducers: () => TReducers; 48 | subscribe: (callback: (state: TState) => void) => Unsubscribe; 49 | }; 50 | 51 | type VirtualStore = { 52 | getState: () => TState; 53 | subscribe: (callback: (state: TState) => void) => Unsubscribe; 54 | }; 55 | 56 | /**************************** 57 | dispatchers 58 | ****************************/ 59 | interface PayloadAction extends Action { 60 | payload: TPayload; 61 | } 62 | 63 | type SetterDispatcher = (state: TState) => PayloadAction; 64 | 65 | type Dispatcher = (...args: RestParams) => PayloadAction[0]>; 66 | 67 | type Dispatchers = { 68 | [K in keyof TReducers]: Dispatcher; 69 | }; 70 | 71 | /**************************** 72 | createReduxModule function 73 | ****************************/ 74 | 75 | /** 76 | * Defines a top-level property of the redux state including its inital value, all related reducers, and returns a react-hook, dispatchers and virtualStore. 77 | * 78 | * @param reduxStorePropertyName is the name of the property off the root redux store you are declaring and initializing 79 | * 80 | * @param initialState is the initial value for your redux state 81 | * 82 | * @param [reducers] is a map from reducer-name to reducer functions which take the currentState plus optional payload and return a new state. 83 | * 84 | * @returns a 3-element array: [reactHook, updateDispatcher or dispatcherMap, virtualStore] 85 | */ 86 | export function createReduxModule( 87 | reduxStorePropertyName: string, 88 | initialState: TState 89 | ): [ReactReduxHookWithOptionalSelector, SetterDispatcher, VirtualStore]; 90 | export function createReduxModule>( 91 | reduxStorePropertyName: string, 92 | initialState: TState, 93 | reducers: TReducers 94 | ): [ 95 | ReactReduxHookWithOptionalSelector, 96 | Readonly>, 97 | VirtualStoreWithReducers 98 | ]; 99 | 100 | /** 101 | * Defines a top-level property of the redux state including its inital value, all related reducers, and returns a react-hook, dispatchers and virtualStore. 102 | * 103 | * @param reduxStorePropertyName is the name of the property off the root redux store you are declaring and initializing 104 | * 105 | * @param initialState is the initial value for your redux state 106 | * 107 | * @param [reducers] is a map from reducer-name to reducer functions which take the currentState plus optional payload and return a new state. 108 | * 109 | * @returns a 3-element array: [reactHook, updateDispatcher or dispatcherMap, virtualStore] 110 | * 111 | * @deprecated Use createReduxModule instead. Same API, new name. 112 | */ 113 | export function useRedux( 114 | reduxStorePropertyName: string, 115 | initialState: TState 116 | ): [ReactReduxHookWithOptionalSelector, SetterDispatcher, VirtualStore]; 117 | export function useRedux>( 118 | reduxStorePropertyName: string, 119 | initialState: TState, 120 | reducers: TReducers 121 | ): [ 122 | ReactReduxHookWithOptionalSelector, 123 | Readonly>, 124 | VirtualStoreWithReducers 125 | ]; 126 | 127 | /**************************** 128 | other hooks-for-redux functions 129 | ****************************/ 130 | export interface ReduxStoreWithInjectReducers extends Store { 131 | injectReducer(key: string, reducer: Reducer): void; 132 | } 133 | 134 | /** 135 | * Auto-vivifies a store if setStore has not been called. Otherwise, it returns the store passed to setStore. 136 | * 137 | * @returns current or newly crated store 138 | */ 139 | export function getStore(): ReduxStoreWithInjectReducers; 140 | 141 | /** 142 | * 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. 143 | * 144 | * @param [store] store to set in the store-registry 145 | * 146 | * @returns store 147 | */ 148 | export function setStore(store: ReduxStoreWithInjectReducers): ReduxStoreWithInjectReducers; 149 | 150 | /** 151 | * Creates a Redux store that holds the state tree. 152 | * The only way to change the data in the store is to call `dispatch()` on it. 153 | * 154 | * There should only be a single store in your app. To specify how different 155 | * parts of the state tree respond to actions, you may combine several 156 | * reducers 157 | * into a single reducer function by using `combineReducers`. 158 | * 159 | * @template TState State object type. 160 | * 161 | * @param reducers An object whose values correspond to different reducer 162 | * functions that need to be combined into one. 163 | * 164 | * @param [initialState] The initial state. 165 | * 166 | * @param [enhancer] The store enhancer. You may optionally specify it to 167 | * enhance the store with third-party capabilities such as middleware, time 168 | * travel, persistence, etc. The only store enhancer that ships with Redux 169 | * is `applyMiddleware()`. 170 | * 171 | * @returns A Redux store that lets you read the state, dispatch actions and 172 | * subscribe to changes. 173 | */ 174 | export function createStore( 175 | reducers: Reducers, 176 | initialState?: TState, 177 | enhancer?: any 178 | ): ReduxStoreWithInjectReducers; 179 | 180 | export interface ProviderProps { 181 | /** 182 | * The single Redux store in your application. 183 | */ 184 | store?: Store; 185 | /** 186 | * Optional context to be used internally in react-redux. Use React.createContext() to create a context to be used. 187 | * If this is used, generate own connect HOC by using connectAdvanced, supplying the same context provided to the 188 | * Provider. Initial value doesn't matter, as it is overwritten with the internal state of Provider. 189 | */ 190 | context?: Context; 191 | } 192 | 193 | /** 194 | * Makes the Redux store available to the connect() calls in the component hierarchy below. 195 | */ 196 | export class Provider extends Component> {} 197 | 198 | /**************************** 199 | type testing 200 | ****************************/ 201 | 202 | // type Unsubscribe = () => void 203 | // export interface Action { 204 | // type: T 205 | // } 206 | 207 | // const [getBar2, setBar2] = useRedux('bar', 0) 208 | // getBar2() 209 | // setBar2(1) 210 | 211 | // const [getBar, {addSevenToBar, addToBar, addString}] = useRedux('bar', 0, { 212 | // addSevenToBar: (state) => state + 7, 213 | // addToBar: (state, payload) => state + payload, 214 | // addString:(state, myThing:string) => state + +myThing 215 | // }) 216 | 217 | // getBar().toPrecision() 218 | 219 | // addSevenToBar() 220 | // addToBar(8) 221 | // addString("9") 222 | 223 | // TEST: https://www.typescriptlang.org/play/index.html#code/C4TwDgpgBAggxsAlgewHYB4AqA+KBeKAb1EgC4pMBfAWACgToBVVAZwFcAjFuAJ0Q+gEAFAEp8uAG7JEAEzp0GUAEoQWwAAoBDHpoC2LLLgKYoEAB7AIqGSyhDtAc3KbUIADRQAdN8ctyiVAAzCB5YMTxJaRkoAH5YKHJUCAkQ+XpwaABxCGBLHkN8O3DcTDSAvMDNOGgtEAAbZE0ZeCQ0AvNLa1sWlAw1PlQHXEI6KCgwTXrGmXJS2hpaBQyoAGUcvIARRBYJ4DgACxCC4TVNS1niqFqGpp62nDTFFRk2avycQqFT84oPCamms5XJc5ktIMoIC83gZMB5GEYIXBkDwZOgANYQEDIQJQRgeZ6vI44bCPZZbHZnA5EglvBFCbyeXzkFRqLQ6fRYGkhbDFUZXSY3ZoIXroFkabR6GFcnjYADaAAYALokxbpcHk3ZUnhSyGE7UIka0MaygDSUACUAxWJxmGlLEV5A1lMO7ztpuVdAWYOgADVEDxgGxNHUVsBkRAAOqIYD7O1YUNnCAeW266EGvkOHIJn6icQUbNJjM5O3kXMRCh2tx89hcXj8CCluDBuocKpo0vfBuYAuXKSyS7MGvcPgCT2k8F+gNBkNhngQePARPpo1QTPAAulkEFqsrod1gSN5utuDtuyd0jdxeWXtRAesTjD+tj1UyCBwOraaCBNioYVoKBsCwEAEmYC5LkIsjkP0AQOB4ATRogwYbvmV4QCI5Cyny2S5ESBbYDuYxrDhPBOnsLpgZY+F8pOgZIbO86XkudCKnQr7vp+UDfr+rSoABQEgRRSYVqmIS2B0Vg2BCUKiYJyZ2tg2BCHyYyQVA0GDARYzmqgCF0YmszbspUBztJ2qzJWdDoVAmErmM2F5IJVG2RCTRoHUIDoKRWo6qZLAKZpYw0dOobhlGMZxoxlhySJ+o7ixqpIqwwDWWuABC2gAEweEBwDpTwGWKoUgHAbqZhCAA5K2PDlR48oiHQaWZaIdA5XlGVCAAjPVCVoGoKU5HlHiEFATQyJgyCDSNMgyGsKSoONeVQJQhUEMVIEVVVNVQPKQ1GaNs1WAt2gdqhlydlAADUUAAOwBVNY0TcdZ6oX8ArTIkbC6AIPBnahl3jG9TQ7pQYgAPSg1AHhpI1PCiJ4YbqHOcDbL0zWqvtySHY9sPdaNR2wwAHN1QA 224 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./src") -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hooks-for-redux", 3 | "dependencies": { 4 | "@types/react-redux": "^7.1.5", 5 | "react-redux": "^7.1.1", 6 | "redux": "^4.0.4" 7 | }, 8 | "scripts": { 9 | "test": "react-scripts test" 10 | }, 11 | "devDependencies": { 12 | "hooks-for-redux": "file:.", 13 | "react": "^17.0.2", 14 | "react-dom": "^17.0.2", 15 | "react-scripts": "^5.0.1", 16 | "react-test-renderer": "^17.0.2" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "h4r", 21 | "redux", 22 | "hooks", 23 | "DRY", 24 | "boilerplate" 25 | ], 26 | "author": "Shane Brinkman-Davis Delamore, GenUI", 27 | "bugs": "https://github.com/generalui/hooks-for-redux/issues", 28 | "homepage": "https://github.com/generalui/hooks-for-redux", 29 | "description": "All the power of Redux in half the code.", 30 | "license": "MIT", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/generalui/hooks-for-redux.git" 34 | }, 35 | "version": "2.2.1" 36 | } 37 | -------------------------------------------------------------------------------- /src/Provider.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Provider: ReactReduxProvider } = require("react-redux"); 3 | const { getStore } = require("./storeRegistry"); 4 | 5 | const Provider = ({ store = getStore(), context, children }) => 6 | React.createElement(ReactReduxProvider, { store, context }, children); 7 | 8 | module.exports = Provider; 9 | -------------------------------------------------------------------------------- /src/VirtualStore.js: -------------------------------------------------------------------------------- 1 | module.exports.createVirtualStore = (store, storeKey) => { 2 | const getState = () => store.getState()[storeKey]; 3 | return { 4 | getState, 5 | subscribe: f => { 6 | let lastState = getState(); 7 | return store.subscribe( 8 | () => lastState !== getState() && f((lastState = getState())) 9 | ); 10 | } 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/createReduxModule.js: -------------------------------------------------------------------------------- 1 | const { getStore } = require("./storeRegistry"); 2 | const { useSelector } = require("react-redux"); 3 | const { createVirtualStore } = require("./VirtualStore"); 4 | 5 | const mapKeys = (o, f) => { 6 | const r = {}; 7 | for (let k in o) r[k] = f(k); 8 | return r; 9 | }; 10 | 11 | const createSimpleReduxModule = (storeKey, initialState) => { 12 | const [hook, dispatchers, virtualStore] = createReduxModule(storeKey, initialState, { 13 | update: (state, payload) => payload, 14 | }); 15 | return [hook, dispatchers.update, virtualStore]; 16 | }; 17 | 18 | const createReduxModule = (storeKey, initialState, reducers, store = getStore()) => { 19 | if (!reducers) return createSimpleReduxModule(storeKey, initialState); 20 | 21 | let getQualifiedActionType = type => `${storeKey}-${type}`; 22 | let qualifiedReducers = {}; 23 | Object.keys(reducers).map(key => { 24 | return (qualifiedReducers[getQualifiedActionType(key)] = reducers[key]); 25 | }); 26 | 27 | store.injectReducer(storeKey, (state = initialState, { type, payload }) => 28 | qualifiedReducers[type] ? qualifiedReducers[type](state, payload) : state 29 | ); 30 | 31 | return [ 32 | (fn = undefined, comparator = undefined) => 33 | useSelector(storeState => { 34 | return typeof fn === "function" ? fn(storeState[storeKey]) : storeState[storeKey]; 35 | }, comparator), 36 | mapKeys(reducers, type => payload => store.dispatch({ type: getQualifiedActionType(type), payload })), 37 | createVirtualStore(store, storeKey), 38 | ]; 39 | }; 40 | module.exports = createReduxModule; 41 | -------------------------------------------------------------------------------- /src/createStore.js: -------------------------------------------------------------------------------- 1 | const { createStore: reduxCreateStore, combineReducers } = require('redux') 2 | 3 | module.exports = (initialReducers = {}, ...args) => { 4 | if (typeof initialReducers !== "object") { 5 | console.error({initialReducers, args}) 6 | throw new Error("initialReducers should be an object suitable to be passed to combineReducers") 7 | } 8 | 9 | const reducers = {...initialReducers, _stub_: (s) => s || 0} 10 | const store = reduxCreateStore(combineReducers(reducers), ...args) 11 | 12 | store.injectReducer = (key, reducer) => { 13 | if (reducers[key]) console.warn(`injectReducer: replacing reducer for key '${key}'`); 14 | reducers[key] = reducer 15 | store.replaceReducer(combineReducers(reducers)) 16 | } 17 | 18 | return store 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const {getStore, setStore} = require('./storeRegistry'); 2 | 3 | module.exports = { 4 | getStore, 5 | setStore, 6 | createStore: require("./createStore"), 7 | Provider: require("./Provider"), 8 | useRedux: require("./useRedux"), 9 | createReduxModule: require("./createReduxModule") 10 | } 11 | -------------------------------------------------------------------------------- /src/storeRegistry.js: -------------------------------------------------------------------------------- 1 | const createStore = require('./createStore'); 2 | 3 | let store = null; 4 | module.exports = { 5 | getStore: () => store ? store : store = createStore(), 6 | 7 | setStore: (initialStore) => { 8 | if (store != null) { 9 | console.warn("Store is already initialized. Call setStore before the first getStore. This call will be ignored."); 10 | return; 11 | } 12 | if (typeof initialStore.injectReducer != "function") 13 | throw new Error("Store must support .injectReducer"); 14 | 15 | return store = initialStore; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/tests/__snapshots__/createReduxModule.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`basic mode subscribeToName works 1`] = ` 4 |

    5 | Hello there, 6 | Alice 7 | ! Click to change me. 8 |

    9 | `; 10 | 11 | exports[`basic mode subscribeToName works 2`] = ` 12 |

    13 | Hello there, 14 | Shane 15 | ! Click to change me. 16 |

    17 | `; 18 | 19 | exports[`reducers mode subscribeToName works 1`] = ` 20 |

    21 | Hello there, 22 | Alice 23 | ! Click to change me. 24 |

    25 | `; 26 | 27 | exports[`reducers mode subscribeToName works 2`] = ` 28 |

    29 | Hello there, 30 | Shane 31 | ! Click to change me. 32 |

    33 | `; 34 | -------------------------------------------------------------------------------- /src/tests/createReduxModule.test.js: -------------------------------------------------------------------------------- 1 | // index.js 2 | import React from "react"; 3 | import renderer from "react-test-renderer"; 4 | import { Provider, createReduxModule, getStore } from "../../index"; 5 | 6 | describe("basic mode", () => { 7 | const STORE_KEY = "basicModeName"; 8 | const [subscribeToName, updateName, { getState }] = createReduxModule(STORE_KEY, "Alice"); 9 | 10 | const expectNameToEqual = expected => expect(getState()).toEqual(expected); 11 | 12 | it("updateName(newState) works", () => { 13 | updateName("Al"); 14 | expectNameToEqual("Al"); 15 | }); 16 | 17 | const App = () =>

    Hello there, {subscribeToName()}! Click to change me.

    ; 18 | 19 | it("subscribeToName works", () => { 20 | updateName("Alice"); 21 | const component = renderer.create( 22 | 23 | 24 | 25 | ); 26 | expect(component.toJSON()).toMatchSnapshot(); 27 | 28 | renderer.act(() => { 29 | updateName("Shane"); 30 | }); 31 | expect(component.toJSON()).toMatchSnapshot(); 32 | component.unmount(); 33 | }); 34 | 35 | it("getState", () => { 36 | expect(typeof getState()).toEqual("string"); 37 | expect(getState()).toEqual(getStore().getState()[STORE_KEY]); 38 | }); 39 | }); 40 | 41 | describe("reducers mode", () => { 42 | const STORE_KEY = "reducerModeName"; 43 | const [subscribeToName, { toggleName, mrName, updateName }, { getState, getReducers }] = createReduxModule( 44 | STORE_KEY, 45 | "Alice", 46 | { 47 | toggleName: name => (name == "Alice" ? "Bob" : "Alice"), 48 | mrName: name => (name == "Alice" ? "Mrs Alice" : "Mr Bob"), 49 | updateName: (name, newName) => newName, 50 | } 51 | ); 52 | 53 | const expectNameToEqual = expected => expect(getState()).toEqual(expected); 54 | 55 | const App = () =>

    Hello there, {subscribeToName()}! Click to change me.

    ; 56 | 57 | it("subscribeToName works", () => { 58 | updateName("Alice"); 59 | const component = renderer.create( 60 | 61 | 62 | 63 | ); 64 | expect(component.toJSON()).toMatchSnapshot(); 65 | 66 | renderer.act(() => { 67 | updateName("Shane"); 68 | }); 69 | expect(component.toJSON()).toMatchSnapshot(); 70 | component.unmount(); 71 | }); 72 | 73 | it("updateName(newState) works", () => { 74 | updateName("Al"); 75 | expectNameToEqual("Al"); 76 | }); 77 | 78 | it("toggleName from addReducers works", () => { 79 | updateName("Alice"); 80 | toggleName(); 81 | expectNameToEqual("Bob"); 82 | }); 83 | 84 | it("mrName from addReducers works", () => { 85 | updateName("Alice"); 86 | mrName(); 87 | expectNameToEqual("Mrs Alice"); 88 | }); 89 | 90 | it("getState", () => { 91 | expect(typeof getState()).toEqual("string"); 92 | expect(getState()).toEqual(getStore().getState()[STORE_KEY]); 93 | }); 94 | 95 | it("getStore", () => { 96 | expect(typeof getStore().injectReducer).toEqual("function"); 97 | }); 98 | }); 99 | 100 | describe("subcriptions outside react", () => { 101 | const STORE_KEY = "createReduxModule testCounter"; 102 | const [useName, { increment }, { getState, getReducers, subscribe }] = createReduxModule(STORE_KEY, 0, { 103 | increment: v => v + 1, 104 | }); 105 | 106 | it("subscribe", () => { 107 | let lastValueSeen = getState(); 108 | let firstValueSeen = lastValueSeen; 109 | const unsubscribe = subscribe(newValue => { 110 | expect(newValue).toEqual(lastValueSeen + 1); 111 | lastValueSeen = newValue; 112 | }); 113 | increment(); 114 | expect(lastValueSeen).toEqual(firstValueSeen + 1); 115 | unsubscribe(); 116 | increment(); 117 | expect(lastValueSeen).toEqual(firstValueSeen + 1); 118 | }); 119 | }); 120 | 121 | describe("duplicate action names", () => { 122 | const [useCountA, { increment: incrementCountA }, { getState: getCountAState }] = createReduxModule("count-a", 0, { 123 | increment: v => v + 1, 124 | }); 125 | const [useCountB, { increment: incrementCountB }, { getState: getCountBState }] = createReduxModule("count-b", 0, { 126 | increment: v => v + 1, 127 | }); 128 | 129 | it("doesn't interfere", () => { 130 | expect(getCountAState()).toEqual(0); 131 | expect(getCountBState()).toEqual(0); 132 | incrementCountA(); 133 | expect(getCountAState()).toEqual(1); 134 | expect(getCountBState()).toEqual(0); 135 | }); 136 | }); 137 | 138 | describe("use state subset", () => { 139 | const STORE_KEY = "subsetModeName"; 140 | const [useName, { setFirstName, setLastName }] = createReduxModule( 141 | STORE_KEY, 142 | { 143 | firstName: "Marty", 144 | lastName: "McFly", 145 | }, 146 | { 147 | setFirstName: (state, firstName) => ({ ...state, firstName }), 148 | setLastName: (state, lastName) => ({ ...state, lastName }), 149 | } 150 | ); 151 | 152 | const App = () => { 153 | renderCount++; 154 | return

    Hello {useName(({ firstName }) => firstName)}!

    ; 155 | }; 156 | 157 | let renderCount = 0; 158 | 159 | it("initial render", () => { 160 | renderer.create( 161 | 162 | 163 | 164 | ); 165 | expect(renderCount).toBe(1); 166 | }); 167 | 168 | // Changing first name should trigger a render 169 | it("re-render on change", () => { 170 | renderer.act(() => { 171 | setFirstName("Emmett"); 172 | }); 173 | expect(renderCount).toBe(2); 174 | }); 175 | 176 | // Changing last name should not trigger a render 177 | it("does not re-render on change", () => { 178 | renderer.act(() => { 179 | setLastName("Brown"); 180 | }); 181 | expect(renderCount).toBe(2); 182 | }); 183 | }); 184 | 185 | describe("use state subset with custom comparator", () => { 186 | const STORE_KEY = "subsetModeName2"; 187 | const [useName, { setFirstName, setLastName }] = createReduxModule( 188 | STORE_KEY, 189 | { 190 | firstName: "Marty", 191 | lastName: "McFly", 192 | }, 193 | { 194 | setFirstName: (state, firstName) => ({ ...state, firstName }), 195 | setLastName: (state, lastName) => ({ ...state, lastName }), 196 | } 197 | ); 198 | 199 | describe("App1 does not have the custom comparator and always re-renders", () => { 200 | const App1 = () => { 201 | renderCount++; 202 | return

    Hello {useName(({ firstName }) => [firstName])[0]}!

    ; 203 | }; 204 | 205 | let renderCount = 0; 206 | 207 | it("initial render App1", () => { 208 | renderer.create( 209 | 210 | 211 | 212 | ); 213 | expect(renderCount).toBe(1); 214 | }); 215 | 216 | // Changing first name should trigger a render 217 | it("App1 re-renders on change", () => { 218 | renderer.act(() => { 219 | setFirstName("Emmett"); 220 | }); 221 | expect(renderCount).toBe(2); 222 | }); 223 | 224 | // Changing last name should not trigger a render 225 | it("App1 re-renders all the time without the custom comparator", () => { 226 | renderer.act(() => { 227 | setLastName("Brown"); 228 | }); 229 | expect(renderCount).toBe(3); 230 | }); 231 | }); 232 | 233 | describe("App2 has a custom comparator and only re-renders when firstName changes", () => { 234 | const App2 = () => { 235 | renderCount++; 236 | return ( 237 |

    238 | Hello{" "} 239 | { 240 | useName( 241 | ({ firstName }) => [firstName], 242 | (a, b) => a[0] === b[0] 243 | )[0] 244 | } 245 | ! 246 |

    247 | ); 248 | }; 249 | 250 | let renderCount = 0; 251 | 252 | it("initial render App2", () => { 253 | renderer.act(() => { 254 | setFirstName("Marty"); 255 | }); 256 | renderer.create( 257 | 258 | 259 | 260 | ); 261 | expect(renderCount).toBe(1); 262 | }); 263 | 264 | // Changing first name should trigger a render 265 | it("App2 re-renders on change", () => { 266 | renderer.act(() => { 267 | setFirstName("Emmett"); 268 | }); 269 | expect(renderCount).toBe(2); 270 | }); 271 | 272 | // Changing last name should not trigger a render 273 | it("App2 re-renders all the time without the custom comparator", () => { 274 | renderer.act(() => { 275 | setLastName("Brown"); 276 | }); 277 | expect(renderCount).toBe(2); 278 | }); 279 | }); 280 | }); 281 | -------------------------------------------------------------------------------- /src/tests/createStore.test.js: -------------------------------------------------------------------------------- 1 | import { getStore } from "../index"; 2 | 3 | it("injectReducer key must be unique", () => { 4 | getStore().injectReducer("myKey", () => 1); 5 | getStore().injectReducer("myKey", () => 2); // second call OK 6 | }); 7 | -------------------------------------------------------------------------------- /src/tests/storeRegistry.test.js: -------------------------------------------------------------------------------- 1 | import { getStore, setStore, createStore } from "../index"; 2 | 3 | it("works", () => { 4 | const store1 = getStore() 5 | const store2 = setStore(createStore({})) 6 | expect(store1 === store2).toEqual(false) 7 | }); 8 | -------------------------------------------------------------------------------- /src/useRedux.js: -------------------------------------------------------------------------------- 1 | const { getStore } = require("./storeRegistry"); 2 | const createReduxModule = require('./createReduxModule'); 3 | 4 | let warnedOnce = false; 5 | const useRedux = (storeKey, initialState, reducers, store = getStore()) => { 6 | if (!warnedOnce) { 7 | console.warn("DEPRECATED: hooks-for-redux - `useRedux` is deprecated. Use `createReduxModule` instead.\n(same API, new name that doesn't erroneously trigger the React warning: https://reactjs.org/warnings/invalid-hook-call-warning.html)"); 8 | warnedOnce = true; 9 | } 10 | return createReduxModule(storeKey, initialState, reducers, store); 11 | }; 12 | module.exports = useRedux; 13 | --------------------------------------------------------------------------------