Skip to content

Commit 8860f59

Browse files
committed
patch/improvement: added custom comparator, too
1 parent 5a582cf commit 8860f59

File tree

4 files changed

+303
-166
lines changed

4 files changed

+303
-166
lines changed

README.md

Lines changed: 91 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The result is a elegant API with 2-3x reduction in client code and near total el
2626
1. [ Contribution](#contribution)
2727
1. [ License](#license)
2828
1. [ Produced at GenUI](#produced-at-genui)
29+
2930
## Install
3031

3132
```
@@ -37,27 +38,28 @@ npm install hooks-for-redux
3738
Tiny, complete example. See below for explanations.
3839

3940
```jsx
40-
import React from 'react';
41-
import ReactDOM from 'react-dom';
42-
import {createReduxModule, Provider} from 'hooks-for-redux'
41+
import React from "react";
42+
import ReactDOM from "react-dom";
43+
import { createReduxModule, Provider } from "hooks-for-redux";
4344

44-
const [useCount, {inc, add, reset}] = createReduxModule('count', 0, {
45-
inc: (state) => state + 1,
45+
const [useCount, { inc, add, reset }] = createReduxModule("count", 0, {
46+
inc: state => state + 1,
4647
add: (state, amount) => state + amount,
47-
reset: () => 0
48-
})
48+
reset: () => 0,
49+
});
4950

50-
const App = () =>
51+
const App = () => (
5152
<p>
52-
Count: {useCount()}
53-
{' '}<input type="button" value="+1" onClick={inc} />
54-
{' '}<input type="button" value="+10" onClick={() => add(10)} />
55-
{' '}<input type="button" value="reset" onClick={reset} />
53+
Count: {useCount()} <input type="button" value="+1" onClick={inc} />{" "}
54+
<input type="button" value="+10" onClick={() => add(10)} /> <input type="button" value="reset" onClick={reset} />
5655
</p>
56+
);
5757

5858
ReactDOM.render(
59-
<Provider><App /></Provider>,
60-
document.getElementById('root')
59+
<Provider>
60+
<App />
61+
</Provider>,
62+
document.getElementById("root")
6163
);
6264
```
6365

@@ -156,7 +158,7 @@ import { createReduxModule } from "hooks-for-redux";
156158
export const [useCount, { inc, add, reset }] = createReduxModule("count", 0, {
157159
inc: state => state + 1,
158160
add: (state, amount) => state + amount,
159-
reset: () => 0
161+
reset: () => 0,
160162
});
161163
```
162164

@@ -170,8 +172,7 @@ import { useCount, inc, add, reset } from "./NameReduxState.js";
170172
export default () => (
171173
<p>
172174
Count: {useCount()} <input type="button" onClick={inc} value="+1" />{" "}
173-
<input type="button" onClick={() => add(10)} value="+10" />{" "}
174-
<input type="button" onClick={reset} value="reset" />
175+
<input type="button" onClick={() => add(10)} value="+10" /> <input type="button" onClick={reset} value="reset" />
175176
</p>
176177
);
177178
```
@@ -219,7 +220,7 @@ ReactDOM.render(
219220

220221
If you are interested in seeing a more complicated example in TypeScript with asynchronous remote requests, please see:
221222

222-
* [ H4R vs Redux-Toolkit Advanced TypeScript Tutorial ](#h4r-vs-redux-toolkit-advanced-typescript-tutorial)
223+
- [ H4R vs Redux-Toolkit Advanced TypeScript Tutorial ](#h4r-vs-redux-toolkit-advanced-typescript-tutorial)
223224

224225
## API
225226

@@ -256,16 +257,24 @@ Define a top-level property of the redux state including its initial value, all
256257
```jsx
257258
const [useMyStore] = createReduxModule(reduxStorePropertyName, initialState)
258259
const MyComponent = () => { // must be used in render function
259-
useMyStore() => current state
260+
useMyStore(selector = undefined) => current state
260261
// ...
261262
}
262263
```
263264

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.
264273
- **OUT**: current state
265274
- **REQUIRED**: must be called within a Component's render function
266275
- **EFFECT**:
267276
- 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.
268-
- Internally, useMyStore is simply:<br>`useSelector(state => state[reduxStorePropertyName])`<br>see: https://react-redux.js.org/next/api/hooks for details.
277+
- Note, this hook will only trigger re-renders if the `comparator` function returns `false`.
269278

270279
#### myDispatchers
271280

@@ -443,26 +452,22 @@ const Provider = ({ store = getStore(), context, children }) =>
443452

444453
- source: [src/createReduxModule.js](src/createReduxModule.js)
445454

446-
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.
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.
447456

448457
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.
449458

450459
The implementation of createReduxModule is surprisingly brief. Details are explained below:
451460

452461
```jsx
453462
const createReduxModule = (storeKey, initialState, reducers, store = getStore()) => {
454-
/* 1 */ store.injectReducer(
455-
storeKey,
456-
(state = initialState, { type, payload }) =>
457-
reducers[type] ? reducers[type](state, payload) : state
463+
/* 1 */ store.injectReducer(storeKey, (state = initialState, { type, payload }) =>
464+
reducers[type] ? reducers[type](state, payload) : state
458465
);
459466

460467
return [
461468
/* 2 */ () => useSelector(storeState => storeState[storeKey]),
462-
/* 3 */ mapKeys(reducers, type => payload =>
463-
store.dispatch({ type, payload })
464-
),
465-
/* 4 */ createVirtualStore(store, storeKey)
469+
/* 3 */ mapKeys(reducers, type => payload => store.dispatch({ type, payload })),
470+
/* 4 */ createVirtualStore(store, storeKey),
466471
];
467472
};
468473
```
@@ -485,10 +490,8 @@ const createVirtualStore = (store, storeKey) => {
485490
getState,
486491
/* 2 */ subscribe: f => {
487492
let lastState = getState();
488-
return store.subscribe(
489-
() => lastState !== getState() && f((lastState = getState()))
490-
);
491-
}
493+
return store.subscribe(() => lastState !== getState() && f((lastState = getState())));
494+
},
492495
};
493496
};
494497
```
@@ -524,48 +527,51 @@ Several people have attempted to simplify Redux and/or make it act more like Rea
524527
Redux-Toolkit claims to be efficient, but when compared to H4R it still falls far short. I'll give an example.
525528

526529
#### H4R vs Redux-Toolkit Intermediate-Example
530+
527531
> 58% less code
528532
529533
Taking from the intermediate code-example provided in the Redux-Toolkit Package:
530534

531535
Redux-Toolkit's implementation:
532-
* tutorial: [redux-toolkit.js.org](https://redux-toolkit.js.org/tutorials/intermediate-tutorial)
533-
* interactive: [codesandbox.io](https://codesandbox.io/s/rtk-convert-todos-example-uqqy3)
534-
* ~390 lines of JavaScript
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
535540

536541
I reduced the code by about 2x using H4R - including eliminating several files. Even the tests got simpler.
537542

538543
H4R solution
539-
* interactive: [codesandbox.io](https://codesandbox.io/s/github/shanebdavis/rtk-convert-todos-example-h4r-conversion)
540-
* source: [github](https://github.com/shanebdavis/rtk-convert-todos-example-h4r-conversion)
541-
* ~160 lines of JavaScript
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
542548

543549
Here is an apples-to-apples comparison of some of the main files from each project:
544550

545-
* [Redux Toolkit gist - 104 lines](https://gist.github.com/shanebdavis/9e67be8a0874a4c295001ba6e91f79e2)
546-
* [Hooks-for-redux gist - 52 lines](https://gist.github.com/shanebdavis/ce02b4495f1bc0afa830796f58124604)
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)
547553

548554
Perhaps the most dramatic difference is how H4R simplifies the interdependencies between files. Boxes are files, lines are imports:
549555

550556
![](https://github.com/generalui/hooks-for-redux/raw/master/assets/h4r-vs-reduxToolkit-intermediate-example.png)
551557

552-
553558
Part of the key is how well H4R links into React. Redux-toolkit takes 50 lines of code just to do this.
554559

555560
```javascript
556-
import React from 'react'
557-
import Todo from './Todo'
558-
import { useFilters } from '../filters/filtersSlice'
559-
import { useTodos } from './todosSlice'
561+
import React from "react";
562+
import Todo from "./Todo";
563+
import { useFilters } from "../filters/filtersSlice";
564+
import { useTodos } from "./todosSlice";
560565

561-
export const VisibleTodoList = () =>
566+
export const VisibleTodoList = () => (
562567
<ul>
563568
{useTodos()
564569
.filter(useFilters())
565570
.map(todo => (
566571
<Todo key={todo.id} {...todo} />
567572
))}
568573
</ul>
574+
);
569575
```
570576

571577
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.
@@ -579,78 +585,84 @@ Now to take on a bigger challenge. The advanced tutorial is a capable github iss
579585
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.
580586
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.
581587

582-
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.
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.
583589

584590
For example, compare the `IssueListPage.tsx` from each project:
585591

586592
```typescript
587-
import React from 'react'
588-
import { useIssues } from 'redux/issues'
589-
import { RepoSearchForm } from './IssuesListLib/RepoSearchForm'
590-
import { IssuesPageHeader } from './IssuesListLib/IssuesPageHeader'
591-
import { IssuesList } from './IssuesListLib/IssuesList'
592-
import { IssuePagination } from './IssuesListLib/IssuePagination'
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";
593599

594600
export const IssuesListPage = () => {
595-
const { loading, error } = useIssues()
596-
return error
597-
? <div>
601+
const { loading, error } = useIssues();
602+
return error ? (
603+
<div>
598604
<h1>Something went wrong...</h1>
599605
<div>{error.toString()}</div>
600606
</div>
601-
: <div id="issue-list-page">
607+
) : (
608+
<div id="issue-list-page">
602609
<RepoSearchForm />
603610
<IssuesPageHeader />
604611
{loading ? <h3>Loading...</h3> : <IssuesList />}
605612
<IssuePagination />
606613
</div>
607-
}
614+
);
615+
};
608616
```
609617

610-
* [github/h4r/IssuesListPage](https://github.com/shanebdavis/rtk-github-issues-example-h4r-conversion/blob/master/src/components/pages/IssuesListPage.tsx)
611-
* 21 lines, 693 bytes
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
612620

613621
to this:
614622

615-
* [github/redux-toolkit/IssuesListPage](https://github.com/reduxjs/rtk-github-issues-example/blob/master/src/features/issuesList/IssuesListPage.tsx)
616-
* 87 lines, 2000 bytes
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
617625

618626
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.
619627

620628
Compare the full source of each project below:
621629

622630
Redux-Toolkit solution:
623-
* tutorial: [redux-toolkit.js.org](https://redux-toolkit.js.org/tutorials/advanced-tutorial)
624-
* interactive: [codesandbox.io](https://codesandbox.io/s/rtk-github-issues-example-02-issues-display-tdx2w)
625-
* source: [github](https://github.com/reduxjs/rtk-github-issues-example)
626-
* ~1170 lines of TypeScript
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
627636

628637
H4R solution:
629-
* interactive: [codesandbox.io](https://codesandbox.io/s/github/shanebdavis/rtk-github-issues-example-h4r-conversion)
630-
* source: [github](https://github.com/shanebdavis/rtk-github-issues-example-h4r-conversion)
631-
* ~613 lines of TypeScript
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
632642

633643
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:
634644

635645
![](https://github.com/generalui/hooks-for-redux/raw/master/assets/h4r-vs-reduxToolkit-advanced-example.png)
636646

637647
## Additional Resources
648+
638649
Blog Posts:
639650

640-
* [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
641-
* [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)
642-
* [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
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
643654

644655
Included Examples:
645656

646-
* [tiny](./examples/tiny) - the simplest working example
647-
* [tiny-todo](./examples/tiny-todo) - a slightly more detailed example
648-
* [middleware](./examples/middleware) - an example of how to use Redux middleware with H4R
649-
* comparison [plain-redux](./examples/comparison-plain-redux) vs [hooks-for-redux](./examples/hooks-for-redux) - compare two, tiny working examples back-to-back
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
650661

651662
Advanced Examples:
652-
* [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))
653-
* [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))
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))
654666

655667
## Contribution
656668

index.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ type RestParams<TFunction> = TFunction extends (arg: any, ...args: infer A) => v
1616
* @param selector optional function to select-from or transform the Redux Model's state. See https://react-redux.js.org/api/hooks#useselector
1717
*/
1818
type ReactReduxHookWithOptionalSelector<TState> =
19-
| (<TFunction extends (TState) => any>(f: TFunction) => ReturnType<TFunction>)
19+
| (<TFunction extends (TState) => any>(
20+
selector: TFunction,
21+
comparator?: (a: ReturnType<TFunction>, b: ReturnType<TFunction>) => boolean
22+
) => ReturnType<TFunction>)
2023
| (() => TState);
2124

2225
/****************************

src/createReduxModule.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,31 @@ const mapKeys = (o, f) => {
1010

1111
const createSimpleReduxModule = (storeKey, initialState) => {
1212
const [hook, dispatchers, virtualStore] = createReduxModule(storeKey, initialState, {
13-
update: (state, payload) => payload
13+
update: (state, payload) => payload,
1414
});
1515
return [hook, dispatchers.update, virtualStore];
1616
};
1717

1818
const createReduxModule = (storeKey, initialState, reducers, store = getStore()) => {
1919
if (!reducers) return createSimpleReduxModule(storeKey, initialState);
2020

21-
let getQualifiedActionType = (type) => `${storeKey}-${type}`;
22-
let qualifiedReducers = {}
23-
Object.keys(reducers).map(key => { return qualifiedReducers[getQualifiedActionType(key)] = reducers[key] });
21+
let getQualifiedActionType = type => `${storeKey}-${type}`;
22+
let qualifiedReducers = {};
23+
Object.keys(reducers).map(key => {
24+
return (qualifiedReducers[getQualifiedActionType(key)] = reducers[key]);
25+
});
2426

2527
store.injectReducer(storeKey, (state = initialState, { type, payload }) =>
2628
qualifiedReducers[type] ? qualifiedReducers[type](state, payload) : state
2729
);
2830

2931
return [
30-
(fn = null) => useSelector(storeState => { return typeof fn === 'function' ? fn(storeState[storeKey]) : storeState[storeKey] }),
32+
(fn = undefined, comparator = undefined) =>
33+
useSelector(storeState => {
34+
return typeof fn === "function" ? fn(storeState[storeKey]) : storeState[storeKey];
35+
}, comparator),
3136
mapKeys(reducers, type => payload => store.dispatch({ type: getQualifiedActionType(type), payload })),
32-
createVirtualStore(store, storeKey)
37+
createVirtualStore(store, storeKey),
3338
];
3439
};
3540
module.exports = createReduxModule;

0 commit comments

Comments
 (0)