-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
40 changed files
with
2,207 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
This is a workaround for https://github.com/eslint/eslint/issues/3458 | ||
TLDR; eslint requires you to know and declare all eslint plugins that are being used in the plugin you | ||
are consuming as peer dependencies. This patch fixes that so you don't have to add to your peer | ||
dependencies and is [recommended](https://github.com/microsoft/rushstack/tree/main/eslint/eslint-config#2-choose-one-profile) by the rush stack eslint-config package. | ||
*/ | ||
require("@rushstack/eslint-config/patch/modern-module-resolution") | ||
|
||
module.exports = { | ||
extends: ["@rushstack/eslint-config/profile/node"], | ||
rules: { | ||
"@typescript-eslint/no-explicit-any": 0, | ||
"@typescript-eslint/consistent-type-definitions": [1, "type"], | ||
"@typescript-eslint/explicit-function-return-type": 0, | ||
"@typescript-eslint/naming-convention": [ | ||
"error", | ||
{ | ||
selector: "parameter", | ||
format: ["camelCase"], | ||
leadingUnderscore: "allow" | ||
} | ||
], | ||
"@typescript-eslint/typedef": 0, | ||
"@rushstack/no-new-null": 0, | ||
"@rushstack/typedef-var": 0 | ||
}, | ||
parserOptions: { tsconfigRootDir: __dirname } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
lerna-debug.log* | ||
|
||
# Diagnostic reports (https://nodejs.org/api/report.html) | ||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | ||
|
||
# Runtime data | ||
pids | ||
*.pid | ||
*.seed | ||
*.pid.lock | ||
|
||
# Directory for instrumented libs generated by jscoverage/JSCover | ||
lib-cov | ||
|
||
# Coverage directory used by tools like istanbul | ||
coverage | ||
*.lcov | ||
|
||
# nyc test coverage | ||
.nyc_output | ||
|
||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | ||
.grunt | ||
|
||
# Bower dependency directory (https://bower.io/) | ||
bower_components | ||
|
||
# node-waf configuration | ||
.lock-wscript | ||
|
||
# Compiled binary addons (https://nodejs.org/api/addons.html) | ||
build/Release | ||
|
||
# Dependency directories | ||
node_modules/ | ||
jspm_packages/ | ||
|
||
# TypeScript v1 declaration files | ||
typings/ | ||
|
||
# TypeScript cache | ||
*.tsbuildinfo | ||
|
||
# Optional npm cache directory | ||
.npm | ||
|
||
# Optional eslint cache | ||
.eslintcache | ||
|
||
# Microbundle cache | ||
.rpt2_cache/ | ||
.rts2_cache_cjs/ | ||
.rts2_cache_es/ | ||
.rts2_cache_umd/ | ||
|
||
# Optional REPL history | ||
.node_repl_history | ||
|
||
# Output of 'npm pack' | ||
*.tgz | ||
|
||
# Yarn Integrity file | ||
.yarn-integrity | ||
|
||
# dotenv environment variables file | ||
.env | ||
.env.test | ||
|
||
# parcel-bundler cache (https://parceljs.org/) | ||
.cache | ||
|
||
# Next.js build output | ||
.next | ||
|
||
# Nuxt.js build / generate output | ||
.nuxt | ||
dist | ||
|
||
# Gatsby files | ||
.cache/ | ||
# Comment in the public line in if your project uses Gatsby and *not* Next.js | ||
# https://nextjs.org/blog/next-9-1#public-directory-support | ||
# public | ||
|
||
# vuepress build output | ||
.vuepress/dist | ||
|
||
# Serverless directories | ||
.serverless/ | ||
|
||
# FuseBox cache | ||
.fusebox/ | ||
|
||
# DynamoDB Local files | ||
.dynamodb/ | ||
|
||
# TernJS port file | ||
.tern-port |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
# rtk-modules | ||
|
||
Library of helper utilities for splitting Redux case reducers, thunks, and selectors | ||
across multiple file modules. Particularly useful in conjunction with Redux Toolkit's | ||
`createSlice` and React Redux's `useSelector` | ||
|
||
## Rationale | ||
|
||
Redux, and in particular Redux Toolkit, encourage breaking redux store's up into feature- | ||
specific "slices." In some code bases, it is desirable to further break up those slices | ||
into modules containing the feature-specific case reducers, selectors, and async thunks. | ||
This can reduce file size, make testing easier, and even allow for isolating individual | ||
redux slices in their own npm packages. | ||
|
||
This library provides some helper functions to make this process easier while preserving | ||
the excellent type safety and type inferences provided by Redux Toolkit. It also provides | ||
some testing utilities to make testing these modularized files easier. | ||
|
||
## Helper Utilities | ||
|
||
### `produceCaseReducer` | ||
|
||
A function to make it easier to create an object full of named case reducers thatcan be passed to the `reducers` property of `createSlice`'s argument. | ||
|
||
```typescript | ||
const createCounterReducer = produceCaseReducer<CounterState>() | ||
|
||
export const counterReducers = { | ||
someReducer: createCounterReducer((state,action) => { | ||
// reducer logic here | ||
}) | ||
} | ||
|
||
//in slice.ts | ||
createSlice({ | ||
... | ||
reducers: createCounterReducers | ||
}) | ||
``` | ||
|
||
### `produceThunkReducerInjector` | ||
|
||
A function to make it easier to create asyncThunk case reducers in the same file | ||
you make the asyncThunks themselves, but following the same `builder` pattern | ||
used by `createSlice` | ||
|
||
```typescript | ||
const createCounterThunkReducerInjector = produceThunkReducerInjector<CounterState>() | ||
|
||
export const injectReducers = createCounterThunkReducerInjector( builder => { | ||
builder.addCase(someAsyncThunk.fulfilled, (state, action) => { | ||
// case reducer code here | ||
}) | ||
} | ||
|
||
//in slice.ts | ||
createSlice({ | ||
... | ||
extraReducers: build => injectReducers(build) | ||
}) | ||
``` | ||
### `produceRootSelectors` | ||
A function that can turn selectors and selector creators written against slice | ||
state into selectors and selector creators, respectively, that operate against | ||
root state. | ||
```typescript | ||
const counterPlainSelectors = { | ||
aSelector: (state:CounterState) => { | ||
//select some state | ||
} | ||
} | ||
|
||
const counterSelectorCreators = { | ||
aSelectorCreator: (x:number) => (state:CounterState) => { | ||
//select some state | ||
} | ||
} | ||
|
||
export const counterSelectors = produceRootSelectors<RootState>()(counterSlice.name)( | ||
counterPlainSelectors, counterSelectorCreators | ||
) | ||
|
||
// in a component | ||
const mySelection = useSelector(counterSelectors.aSelector) | ||
const myOtherSelection = useSelector(counterSelectors.aSelector(4)) | ||
``` | ||
## Testing Utilities | ||
### `produceAsyncThunkTester` | ||
A function to make it easier to test async thunks using an actual store | ||
containing the relevant slice. | ||
```typescript | ||
// creates a mock store containing just the slice and with state configured | ||
// based on an injected initial state or a function that transforms the | ||
// slices initial state | ||
let testAsyncThunk = produceAsyncThunkTester(counterSlice, injectedStartingState) | ||
|
||
it("example test", async () => { | ||
// runs the passed in async thunk through the store and returns the result | ||
// and modified state | ||
const { result, state } = await testAsyncThunk(counterThunks.someAsyncThunk()) | ||
|
||
expect(Object.keys(result)).toContain("expectedString") | ||
expect(state.someState.length).toBe(3) | ||
}) | ||
``` | ||
### `produceMockState` and `produceStateSetter` | ||
Lightweight functional programming utilities to create mock state from an initial | ||
state using a set of transformer functions. `produceStateSetter` generates the | ||
transformer functions, and `produceMockState` composes them together and uses | ||
them to transform an initial state object. | ||
```typescript | ||
const set = produceStateSetter<OrdersState>() | ||
|
||
it("example test", () => { | ||
const mockState = createMockState([ | ||
set("availableProducts", [Product.floppy, Product.fdd]), | ||
set("orderQueue", [ | ||
{ | ||
id: "asdf8", | ||
customerId: 3, | ||
productType: Product.floppy, | ||
datePlaced: "2021-03-24" | ||
}, | ||
{ | ||
id: "lc73s", | ||
customerId: 7, | ||
productType: Product.fdd, | ||
datePlaced: "2021-03-24" | ||
} | ||
]) | ||
])(initialOrdersState) | ||
/// continue with act and assertions of test | ||
}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// Copyright 2022 MFB Technologies, Inc. | ||
|
||
import { ordersSelectors } from "../orders/selectors" | ||
import { ordersSlice, Product } from "../orders/slice" | ||
import { ordersThunks } from "../orders/thunks" | ||
import { RootState, store } from "../store" | ||
|
||
/* | ||
* Redux Store Integration Tests | ||
* | ||
* This is one way to test a redux store, including its actions, thunks, | ||
* and selectors. Note that there's no need to involve React in order to fully | ||
* test the redux logic. But you can mock out versions of useSelector and | ||
* useDispatch to (somewhat) mimic the feel of how you would use things in React, | ||
* as is done in these tests. | ||
*/ | ||
|
||
const useSelector: <R>(selector: (state: RootState) => R) => R = selector => | ||
selector(store.getState()) | ||
const useDispatch = () => store.dispatch | ||
|
||
it("examine initial state", () => { | ||
const orders = useSelector(ordersSelectors.getAllOrders) | ||
expect(orders.length).toBe(0) | ||
}) | ||
|
||
it("get some orders from the server", async () => { | ||
const dispatch = useDispatch() | ||
await dispatch(ordersThunks.getOrder()) | ||
await dispatch(ordersThunks.getOrder()) | ||
const orders = useSelector(ordersSelectors.getAllOrders) | ||
expect(orders.length).toBe(2) | ||
}) | ||
|
||
it("see if any of them are crts (spoiler: they aren't)", () => { | ||
const crts = useSelector(ordersSelectors.getOrdersByType(Product.crt)) | ||
expect(crts.length).toBe(0) | ||
}) | ||
|
||
it("see if any of them are floppies (yes!)", () => { | ||
const crts = useSelector(ordersSelectors.getOrdersByType(Product.floppy)) | ||
expect(crts.length).toBe(2) | ||
}) | ||
|
||
it("remove crts from available products, and add dot matrix printers(!)", () => { | ||
const dispatch = useDispatch() | ||
dispatch(ordersSlice.actions.productRemoved(Product.crt)) | ||
dispatch(ordersSlice.actions.productAdded(Product.dotMatrix)) | ||
expect(store.getState().orders.availableProducts).toContain(Product.dotMatrix) | ||
expect(store.getState().orders.availableProducts).not.toContain(Product.crt) | ||
}) | ||
|
||
it("ship a product", async () => { | ||
const dispatch = useDispatch() | ||
const order = useSelector(ordersSelectors.getOldestOrder) | ||
await dispatch(ordersThunks.shipOrder(order)) | ||
const orders = useSelector(ordersSelectors.getAllOrders) | ||
expect(orders.length).toBe(1) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { createSlice } from "@reduxjs/toolkit" | ||
import { store } from "./store" | ||
// needed for type inference in monorepo | ||
import "immer" | ||
|
||
export const counterSlice = createSlice({ | ||
name: "counter", | ||
initialState: { | ||
value: 0 | ||
}, | ||
reducers: { | ||
increment: state => { | ||
// Redux Toolkit allows us to write "mutating" logic in reducers. It | ||
// doesn't actually mutate the state because it uses the immer library, | ||
// which detects changes to a "draft state" and produces a brand new | ||
// immutable state based off those changes | ||
state.value += 1 | ||
}, | ||
decrement: state => { | ||
state.value -= 1 | ||
}, | ||
incrementByAmount: (state, action) => { | ||
state.value += action.payload | ||
} | ||
} | ||
}) | ||
|
||
export const { increment, decrement, incrementByAmount } = counterSlice.actions | ||
|
||
// The function below is called a thunk and allows us to perform async logic. It | ||
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This | ||
// will call the thunk with the `dispatch` function as the first argument. Async | ||
// code can then be executed and other actions can be dispatched | ||
export const incrementAsync = | ||
(amount: number) => (dispatch: typeof store.dispatch) => { | ||
setTimeout(() => { | ||
dispatch(incrementByAmount(amount)) | ||
}, 1000) | ||
} | ||
|
||
// The function below is called a selector and allows us to select a value from | ||
// the state. Selectors can also be defined inline where they're used instead of | ||
// in the slice file. For example: `useSelector((state) => state.counter.value)` | ||
export const selectCount = (state: ReturnType<typeof store.getState>) => | ||
state.counter.value | ||
|
||
export default counterSlice.reducer |
Oops, something went wrong.