Skip to content

Commit fa905be

Browse files
feat: add composite-reducer (ENG-737) (#6)
1 parent 02e41c3 commit fa905be

File tree

12 files changed

+232
-5
lines changed

12 files changed

+232
-5
lines changed

libs/bidirectional-adapter/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Bi-Directional Adapter
22

3-
[![npm version](https://img.shields.io/npm/v/bidirectional-adapter.svg?style=flat-square)](https://www.npmjs.com/package/bidirectional-adapter)
4-
[![npm downloads](https://img.shields.io/npm/dm/bidirectional-adapter.svg?style=flat-square)](https://www.npmjs.com/package/bidirectional-adapter)
3+
[![npm version](https://img.shields.io/npm/v/@voiceflow/bidirectional-adapter.svg?style=flat-square)](https://www.npmjs.com/package/@voiceflow/bidirectional-adapter)
4+
[![npm downloads](https://img.shields.io/npm/dm/@voiceflow/bidirectional-adapter.svg?style=flat-square)](https://www.npmjs.com/package/@voiceflow/bidirectional-adapter)
55

66
Factory to create bi-directional adapters that can convert between two distinct data structures.
77
Using adapters helps to decouple systems which share common data structures and may need to alter them
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[
2+
{
3+
"type": "module",
4+
"from": "src/index.ts",
5+
"to": "src/index.ts",
6+
"rule": {
7+
"severity": "error",
8+
"name": "no-orphans"
9+
}
10+
}
11+
]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createConfig } from '@voiceflow/dependency-cruiser-config';
2+
3+
export default createConfig();

libs/composite-reducer/README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Composite Reducer
2+
3+
[![npm version](https://img.shields.io/npm/v/@voiceflow/composite-reducer.svg?style=flat-square)](https://www.npmjs.com/package/@voiceflow/composite-reducer)
4+
[![npm downloads](https://img.shields.io/npm/dm/@voiceflow/composite-reducer.svg?style=flat-square)](https://www.npmjs.com/package/@voiceflow/composite-reducer)
5+
6+
[What are Reducers?](https://css-tricks.com/understanding-how-reducers-are-used-in-redux/)
7+
8+
Allows reducers for specific properties of a state - better organization for reducers of complex or deeply nested objects.
9+
10+
> Works well with state management solutions such as [Redux](https://redux.js.org/) or [React Context](https://reactjs.org/docs/context.html) + [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer) hook.
11+
12+
Similar to the [`combineReducers`](https://redux.js.org/api/combinereducers) function in redux. But instead of combining many equal top level reducers, has a main reducer and attaches other reducers for properties of the main state.
13+
14+
## Why?
15+
16+
Say there is a state that looks like this:
17+
18+
```
19+
{
20+
name: "voiceflow",
21+
type: "startup",
22+
settings: {
23+
website: "voiceflow.com"
24+
}
25+
}
26+
```
27+
28+
If a reducer is created for this state, to change the `website`, I would need a dedicated action to update it, and construct a new state with something messier like this:
29+
30+
```
31+
{
32+
...state,
33+
settings: {
34+
...state.settings,
35+
website: action.payload
36+
}
37+
}
38+
```
39+
40+
With `composite-reducer`, the `settings` sub-state can be abstracted into it's own dedicated reducer, separate from the main one.
41+
42+
```
43+
const reducer = compositeReducer(mainReducer, {
44+
settings: settingsReducer
45+
});
46+
```
47+
48+
The dedicated reducer updates/works with a smaller, more concise state.
49+
50+
The main reducer can still act on the property if it has to.
51+
52+
Along with [`combinedReducer`](), this encourges the overall reducer to be cleaner/better organized.
53+
54+
## Example
55+
56+
```
57+
import compositeReducer from 'composite-reducer';
58+
59+
const mainReducer = (state, action) => {
60+
// do reducer stuff here
61+
return state;
62+
};
63+
const propertyOneReducer = (state, action) => {
64+
// state is in the shape of propertyOne
65+
// do reducer stuff here
66+
return state;
67+
};
68+
69+
const propertyTwoReducer = (state, action) => {
70+
// state is in the shape of propertyTwo
71+
// do reducer stuff here
72+
return state;
73+
};
74+
75+
const reducer = compositeReducer(mainReducer, {
76+
propertyOne: subpropertyOneReducer,
77+
propertyTwo: subpropertyTwoReducer,
78+
})
79+
```
80+
81+
## Installation
82+
83+
To use `composite-reducer`, install it as a dependency:
84+
85+
```bash
86+
# If you use npm:
87+
npm install @voiceflow/composite-reducer
88+
89+
# Or if you use Yarn:
90+
yarn add @voiceflow/composite-reducer
91+
```
92+
93+
This assumes that you’re using a package manager such as [npm](http://npmjs.com/).
94+
95+
## License
96+
97+
[MIT](LICENSE.md)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "@voiceflow/composite-reducer",
3+
"version": "2.0.0",
4+
"description": "combine reducers based on individual properties",
5+
"keywords": [
6+
"combinedReducer",
7+
"react",
8+
"react-redux",
9+
"reducer",
10+
"redux",
11+
"voiceflow"
12+
],
13+
"homepage": "https://github.com/voiceflow/oss/tree/master/libs/composite-reducer#readme",
14+
"bugs": {
15+
"url": "https://github.com/voiceflow/oss/issues"
16+
},
17+
"repository": {
18+
"type": "git",
19+
"url": "git+https://github.com/voiceflow/oss.git"
20+
},
21+
"license": "ISC",
22+
"author": "Tyler Han, Ben Teichman",
23+
"type": "module",
24+
"main": "build/index.js",
25+
"types": "build/index.d.ts",
26+
"files": [
27+
"build"
28+
],
29+
"scripts": {
30+
"build": "yarn g:turbo run build:cmd --filter=@voiceflow/composite-reducer...",
31+
"build:cmd": "yarn g:build:pkg",
32+
"clean": "yarn g:rimraf build",
33+
"lint": "yarn g:run-p -c lint:eslint lint:prettier",
34+
"lint:eslint": "yarn g:eslint",
35+
"lint:fix": "yarn g:run-p -c \"lint:eslint --fix\" \"lint:prettier --write\"",
36+
"lint:prettier": "yarn g:prettier --check",
37+
"test": "yarn g:run-p -c test:dependencies test:types",
38+
"test:dependencies": "yarn g:depcruise --ignore-known",
39+
"test:types": "yarn g:tsc --noEmit"
40+
},
41+
"engines": {
42+
"node": "20"
43+
},
44+
"volta": {
45+
"extends": "../../package.json"
46+
}
47+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
sonar.projectName=oss-composite-reducer
2+
sonar.sources=src/
3+
sonar.tests=src/
4+
sonar.exclusions=src/**/*.test.ts
5+
sonar.test.inclusions=src/**/*.test.ts
6+
sonar.cpd.exclusions=src/**/*.test.ts
7+
sonar.typescript.tsconfigPath=tsconfig.json
8+
sonar.javascript.lcov.reportPaths=sonar/coverage/lcov.info
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
type RootReducer<S, A = never> = (state: S | undefined, action: A) => S;
2+
3+
type ReducerMap<T extends Record<string, any>, A> = {
4+
[K in keyof T]: RootReducer<T[K], A>;
5+
};
6+
7+
type ReducerMapState<T> = T extends ReducerMap<infer R, any> ? R : never;
8+
type ReducerMapAction<T> = T extends ReducerMap<any, infer R> ? R : never;
9+
10+
const compositeReducer =
11+
<S extends Record<string, any>, A extends Record<string, any>, M extends ReducerMap<Record<string, any>, any>>(
12+
rootReducer: RootReducer<S, A>,
13+
reducers: M
14+
): RootReducer<S & ReducerMapState<M>, A | ReducerMapAction<M>> =>
15+
(state, action) =>
16+
Object.keys(reducers).reduce(
17+
(acc, key) => {
18+
if (acc) {
19+
const subState = reducers[key]!(acc[key], action as ReducerMapAction<M>);
20+
if (subState !== acc[key]) {
21+
return {
22+
...acc,
23+
[key]: subState,
24+
};
25+
}
26+
}
27+
28+
return acc;
29+
},
30+
rootReducer(state, action as A) as S & ReducerMapState<M>
31+
);
32+
33+
export default compositeReducer;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "https://json.schemastore.org/tsconfig",
3+
"extends": "./tsconfig.json",
4+
"compilerOptions": {
5+
"module": "esnext",
6+
"outDir": "build"
7+
},
8+
"include": ["src"],
9+
"exclude": ["**/*.test.ts"],
10+
"tsc-alias": {
11+
"resolveFullPaths": true
12+
}
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "https://json.schemastore.org/tsconfig",
3+
"extends": "@voiceflow/tsconfig",
4+
"compilerOptions": {
5+
"isolatedModules": false,
6+
"experimentalDecorators": true,
7+
"emitDecoratorMetadata": true
8+
}
9+
}

libs/normal-store/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Normal Store
22

3-
[![npm version](https://img.shields.io/npm/v/normal-store.svg?style=flat-square)](https://www.npmjs.com/package/normal-store)
4-
[![npm downloads](https://img.shields.io/npm/dm/normal-store.svg?style=flat-square)](https://www.npmjs.com/package/normal-store)
3+
[![npm version](https://img.shields.io/npm/v/@voiceflow/normal-store.svg?style=flat-square)](https://www.npmjs.com/package/@voiceflow/normal-store)
4+
[![npm downloads](https://img.shields.io/npm/dm/@voiceflow/normal-store.svg?style=flat-square)](https://www.npmjs.com/package/@voiceflow/normal-store)
55

66
Utilities to transform data with unique identifiers to and from a normalized data store.
77
All data is treated as immutable, new data structures are returned when updating.

0 commit comments

Comments
 (0)