Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port ngrx/entity and add createAsyncThunk #352

Merged
merged 80 commits into from
Feb 19, 2020
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
d0f4621
Initial port of `@ngrx/entity` implementation
markerikson Feb 9, 2020
fc3e33b
Remove deprecated addAll method
markerikson Feb 9, 2020
8bfdaf7
Port `@ngrx/entity` tests
markerikson Feb 10, 2020
1602a36
Simplify immutable entity operations by wrapping with Immer
markerikson Feb 10, 2020
46088ec
Don't overwrite state.ids if sorting order hasn't changed
markerikson Feb 10, 2020
d499052
Simplify state adapter logic using Immer
markerikson Feb 10, 2020
293b0d1
Add `isFSA` helper to createAction
markerikson Feb 11, 2020
2ccdbd4
Swap state operator order to `(state, arg)` and support FSAs
markerikson Feb 11, 2020
c088899
Add a test to verify adapter usage with createSlice
markerikson Feb 11, 2020
8c53d58
Document unexpected Immer behavior with nested produce calls
markerikson Feb 11, 2020
ff69954
Quiet lint warnings in tests
markerikson Feb 12, 2020
00a8685
Export Entity types as part of the public API
markerikson Feb 12, 2020
12351d7
Add createAsyncThunk
markerikson Feb 12, 2020
d72b051
Export createAsyncThunk as part of the public API
markerikson Feb 12, 2020
08a256b
Ignore VS Code folder
markerikson Feb 12, 2020
6918d66
Mark new types as alpha
markerikson Feb 12, 2020
edc481a
1.3.0-alpha.0
markerikson Feb 12, 2020
58df106
Remove `removeMany(predicate)` overload
markerikson Feb 13, 2020
a1d4091
Rework dispatched thunk action contents
markerikson Feb 13, 2020
71240c9
Merge branch 'master' into feature/entities
markerikson Feb 13, 2020
2774dd7
Update public API types
markerikson Feb 13, 2020
4940605
typings experiment
phryneas Feb 13, 2020
11ed168
Update createAsyncThunk tests to match API changes
markerikson Feb 14, 2020
24863a1
Simplify entity ID type definitions
markerikson Feb 14, 2020
113038c
Add a basic request ID counter to createAsyncThunk
markerikson Feb 14, 2020
fa3c86b
Add nanoid
markerikson Feb 14, 2020
6054a27
Include requestId in payload creator args, and use nanoid
markerikson Feb 14, 2020
d13ffde
Hopefully fix type definitions for empty thunk action params
markerikson Feb 14, 2020
da2fad9
Add overloads to make EntityAdapter methods createSlice-compatible
markerikson Feb 14, 2020
6e73961
Add a test that combines slices, async thunks, and entities
markerikson Feb 14, 2020
95d2bcd
Remove TS 3.3 and 3.4 from the Travis setup
markerikson Feb 14, 2020
a40e060
Update public API
markerikson Feb 14, 2020
9d57d62
1.3.0-alpha.1
markerikson Feb 14, 2020
d0f44b8
Rework createAsyncThunk error handling behavior
markerikson Feb 15, 2020
d28d779
Update public API
markerikson Feb 15, 2020
fbba32d
1.3.0-alpha.2
markerikson Feb 15, 2020
d13d26a
createAsyncThunk return fulfilled/rejected action instead of re-… (#361)
phryneas Feb 15, 2020
1076ab2
add .abort() to the createAsyncThunk thunkAction (#362)
phryneas Feb 15, 2020
5286c08
Add initial `getAsyncThunk` API docs and usage guide
markerikson Feb 15, 2020
a11b380
Merge branch 'master' into feature/entities
markerikson Feb 15, 2020
a53849b
Rename thunk types and fields and export SerializedError
markerikson Feb 15, 2020
6ce3e43
Update public API
markerikson Feb 15, 2020
fedd56c
1.3.0-alpha.3
markerikson Feb 15, 2020
cad5bf8
Initial fix for createAsyncThunk thunk types
markerikson Feb 16, 2020
a54b9ff
Rework `createAsyncThunk` types to enable specifying getState type
markerikson Feb 16, 2020
ab7cac9
Fix thunk test types
markerikson Feb 16, 2020
2f4ca1f
Update public API
markerikson Feb 16, 2020
6f19daf
1.3.0-alpha.4
markerikson Feb 16, 2020
39c5b31
manually import types to prevent a bundling issue
phryneas Feb 16, 2020
762f161
strongly type slice name (#354)
tgouala Feb 16, 2020
cd8329c
use ThunkApiConfig for optional type arguments (#364)
phryneas Feb 16, 2020
fc46763
Merge branch 'master' into feature/entities
markerikson Feb 16, 2020
3b1e203
1.3.0-alpha.5
markerikson Feb 16, 2020
5691d88
Modify createStateOperator to detect and handle Immer drafts
markerikson Feb 17, 2020
5170cd9
Update link styling to match main Redux site
markerikson Feb 17, 2020
d834671
Update blockquote styling to match main Redux site
markerikson Feb 17, 2020
e8ad717
Update side category menu styling to match main Redux site
markerikson Feb 17, 2020
d708ea1
Consolidate Update generic type and remove unused overload
markerikson Feb 17, 2020
bf23105
Update `combinedTest` based on `createStateOperator` fixes
markerikson Feb 17, 2020
dcb4fc1
Add API docs for `createEntityAdapter`
markerikson Feb 17, 2020
df69c19
guess what time it is again - it's public API time!
markerikson Feb 17, 2020
bfa0aac
1.3.0-alpha.6
markerikson Feb 17, 2020
63130ca
Remove accidental yarn.lock
markerikson Feb 17, 2020
da54c98
Merge branch 'master' into feature/entities
markerikson Feb 17, 2020
84d23fc
Try fixing Netlify deploys: 1
markerikson Feb 17, 2020
4296e74
Update DS to fix sidebar bug
markerikson Feb 17, 2020
3269da8
Merge branch 'master' into feature/entities
markerikson Feb 17, 2020
4c7c92a
Try forcing node version
markerikson Feb 17, 2020
8c6c792
createAsyncThunk improvements (#367)
phryneas Feb 18, 2020
491a122
Remove extraneous period from abort message
markerikson Feb 18, 2020
b9616df
Reorder cancellation content and improve wording
markerikson Feb 18, 2020
0f8a631
Fix code padding color busted from DS alpha.41
markerikson Feb 18, 2020
a0aeace
1.3.0-alpha.7
markerikson Feb 18, 2020
717658b
Update Docusaurus and add lockfile to 43 version (#369)
lex111 Feb 19, 2020
7415f46
Merge branch 'master' into feature/entities
markerikson Feb 19, 2020
69758eb
Try adding the compressed-size-action (#372)
markerikson Feb 19, 2020
34508e0
Merge branch 'master' into feature/entities
markerikson Feb 19, 2020
9eddab9
Fix potential entity bugs identified by code review
markerikson Feb 19, 2020
7b46be4
do that public API thing
markerikson Feb 19, 2020
0252d80
Document caveats with update operations
markerikson Feb 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ es


.idea/
.vscode/
temp/

.rts2*
65 changes: 65 additions & 0 deletions etc/redux-toolkit.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
prepare: PrepareAction<Action['payload']>;
};

// @alpha (undocumented)
export type Comparer<T> = ComparerNum<T> | ComparerStr<T>;

// @public
export type ConfigureEnhancersCallback = (defaultEnhancers: StoreEnhancer[]) => StoreEnhancer[];

Expand All @@ -98,6 +101,29 @@ export function createAction<P = void, T extends string = string>(type: T): Payl
// @public
export function createAction<PA extends PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;

// @alpha (undocumented)
export function createAsyncThunk<ActionType extends string, PayloadCreator extends AsyncActionCreator<unknown, Dispatch, unknown, undefined>>(type: ActionType, payloadCreator: PayloadCreator): {
(args?: Parameters<PayloadCreator>[0]["args"] | undefined): (dispatch: any, getState: any, extra: any) => Promise<any>;
pending: import("./createAction").ActionCreatorWithPreparedPayload<[Parameters<PayloadCreator>[0]["args"]], undefined, string, never, {
args: Parameters<PayloadCreator>[0]["args"];
}>;
rejected: import("./createAction").ActionCreatorWithPreparedPayload<[Error, Parameters<PayloadCreator>[0]["args"]], undefined, string, Error, {
args: Parameters<PayloadCreator>[0]["args"];
}>;
fulfilled: import("./createAction").ActionCreatorWithPreparedPayload<[Await<ReturnType<PayloadCreator>>, Parameters<PayloadCreator>[0]["args"]], Await<ReturnType<PayloadCreator>>, string, never, {
args: Parameters<PayloadCreator>[0]["args"];
}>;
finished: import("./createAction").ActionCreatorWithPreparedPayload<[Parameters<PayloadCreator>[0]["args"]], undefined, string, never, {
args: Parameters<PayloadCreator>[0]["args"];
}>;
};

// @alpha (undocumented)
export function createEntityAdapter<T>(options?: {
selectId?: IdSelector<T>;
sortComparer?: false | Comparer<T>;
}): EntityAdapter<T>;

export { createNextState }

// @public
Expand All @@ -122,13 +148,46 @@ export interface CreateSliceOptions<State = any, CR extends SliceCaseReducers<St
reducers: ValidateSliceCaseReducers<State, CR>;
}

// @alpha (undocumented)
export abstract class Dictionary<T> implements DictionaryNum<T> {
// (undocumented)
[id: string]: T | undefined;
}

export { Draft }

// @public
export interface EnhancedStore<S = any, A extends Action = AnyAction, M extends Middlewares<S> = Middlewares<S>> extends Store<S, A> {
dispatch: DispatchForMiddlewares<M> & Dispatch<A>;
}

// @alpha (undocumented)
export interface EntityAdapter<T> extends EntityStateAdapter<T> {
// (undocumented)
getInitialState(): EntityState<T>;
// (undocumented)
getInitialState<S extends object>(state: S): EntityState<T> & S;
// (undocumented)
getSelectors(): EntitySelectors<T, EntityState<T>>;
// (undocumented)
getSelectors<V>(selectState: (state: V) => EntityState<T>): EntitySelectors<T, V>;
// (undocumented)
selectId: IdSelector<T>;
// (undocumented)
sortComparer: false | Comparer<T>;
}

// @alpha (undocumented)
export type EntityMap<T> = (entity: T) => T;

// @alpha (undocumented)
export interface EntityState<T> {
// (undocumented)
entities: Dictionary<T>;
// (undocumented)
ids: string[] | number[];
}

// @public (undocumented)
export function findNonSerializableValue(value: unknown, path?: ReadonlyArray<string>, isSerializable?: (value: unknown) => boolean, getEntries?: (value: unknown) => [string, any][], ignoredPaths?: string[]): NonSerializableValue | false;

Expand All @@ -142,6 +201,9 @@ export function getDefaultMiddleware<S = any, O extends Partial<GetDefaultMiddle
// @public
export function getType<T extends string>(actionCreator: PayloadActionCreator<any, T>): T;

// @alpha (undocumented)
export type IdSelector<T> = IdSelectorStr<T> | IdSelectorNum<T>;

// @public
export function isPlain(val: any): boolean;

Expand Down Expand Up @@ -199,6 +261,9 @@ export type SliceCaseReducers<State> = {

export { ThunkAction }

// @alpha (undocumented)
export type Update<T> = UpdateStr<T> | UpdateNum<T>;

// @public
export type ValidateSliceCaseReducers<S, ACR extends SliceCaseReducers<S>> = ACR & {
[T in keyof ACR]: ACR[T] extends {
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reduxjs/toolkit",
"version": "1.2.4",
"version": "1.3.0-alpha.0",
"description": "The official, opinionated, batteries-included toolset for efficient Redux development",
"repository": "https://github.com/reduxjs/redux-toolkit",
"keywords": [
Expand Down
17 changes: 17 additions & 0 deletions src/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
IfVoid,
IsAny
} from './tsHelpers'
import isPlainObject from './isPlainObject'

/**
* An action with a string type and an associated payload. This is the
Expand Down Expand Up @@ -295,6 +296,22 @@ export function createAction(type: string, prepareAction?: Function): any {
return actionCreator
}

export function isFSA<
Payload = undefined,
Type extends string = string,
Meta = undefined
>(action: any): action is PayloadAction<Payload, Type, Meta> {
return (
isPlainObject(action) &&
typeof (action as any).type === 'string' &&
Object.keys(action).every(isValidKey)
)
}

function isValidKey(key: string) {
return ['type', 'payload', 'error', 'meta'].indexOf(key) > -1
}

/**
* Returns the action type of the actions created by the passed
* `createAction()`-generated action creator (arbitrary action creators
Expand Down
82 changes: 82 additions & 0 deletions src/createAsyncThunk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createAsyncThunk } from './createAsyncThunk'

describe('createAsyncThunk', () => {
it('creates the action types', () => {
const thunkActionCreator = createAsyncThunk('testType', async () => 42)

expect(thunkActionCreator.fulfilled.type).toBe('testType/fulfilled')
expect(thunkActionCreator.pending.type).toBe('testType/pending')
expect(thunkActionCreator.finished.type).toBe('testType/finished')
expect(thunkActionCreator.rejected.type).toBe('testType/rejected')
})

it('accepts arguments and dispatches the actions on resolve', async () => {
const dispatch = jest.fn()

let passedArgs: any

const result = 42
const args = 123

const thunkActionCreator = createAsyncThunk(
'testType',
async ({ args }) => {
passedArgs = args
return result
}
)

const thunkFunction = thunkActionCreator(args)

await thunkFunction(dispatch, undefined, undefined)

expect(passedArgs).toBe(args)

expect(dispatch).toHaveBeenNthCalledWith(
1,
thunkActionCreator.pending(args)
)

expect(dispatch).toHaveBeenNthCalledWith(
2,
thunkActionCreator.fulfilled(result, args)
)

expect(dispatch).toHaveBeenNthCalledWith(
3,
thunkActionCreator.finished(args)
)
})

it('accepts arguments and dispatches the actions on reject', async () => {
const dispatch = jest.fn()

let passedArgs: any
const args = 123

const error = new Error('Panic!')

const thunkActionCreator = createAsyncThunk('testType', async () => {
throw error
})

const thunkFunction = thunkActionCreator(args)

await thunkFunction(dispatch, undefined, undefined)

expect(dispatch).toHaveBeenNthCalledWith(
1,
thunkActionCreator.pending(args)
)

expect(dispatch).toHaveBeenNthCalledWith(
2,
thunkActionCreator.rejected(error, args)
)

expect(dispatch).toHaveBeenNthCalledWith(
3,
thunkActionCreator.finished(args)
)
})
})
113 changes: 113 additions & 0 deletions src/createAsyncThunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Dispatch } from 'redux'
import { createAction } from './createAction'

export type Await<T> = T extends {
then(onfulfilled?: (value: infer U) => unknown): unknown
}
? U
: T

export interface AsyncThunkParams<
A,
D extends Dispatch,
S extends unknown,
E extends unknown
> {
args: A
dispatch: D
getState: () => S
extra: E
}

export type AsyncActionCreator<
A,
D extends Dispatch,
S extends unknown,
E extends unknown
> = (params: AsyncThunkParams<A, D, S, E>) => any

/**
*
* @param type
* @param payloadCreator
*
* @alpha
*/
export function createAsyncThunk<
markerikson marked this conversation as resolved.
Show resolved Hide resolved
ActionType extends string,
PayloadCreator extends AsyncActionCreator<
unknown,
Dispatch,
unknown,
undefined
>
>(type: ActionType, payloadCreator: PayloadCreator) {
// TODO This results in some hideous-looking inferred types for the actions
type ActionParams = Parameters<PayloadCreator>[0]['args']

const fulfilled = createAction(
type + '/fulfilled',
(result: Await<ReturnType<PayloadCreator>>, args: ActionParams) => {
return {
payload: result,
meta: { args }
}
}
)

const pending = createAction(type + '/pending', (args: ActionParams) => {
return {
payload: undefined,
meta: { args }
}
})

const finished = createAction(type + '/finished', (args: ActionParams) => {
return {
payload: undefined,
meta: { args }
}
})

const rejected = createAction(
type + '/rejected',
(error: Error, args: ActionParams) => {
return {
payload: undefined,
error,
meta: { args }
}
}
)

function actionCreator(args?: ActionParams) {
return async (dispatch: any, getState: any, extra: any) => {
try {
dispatch(pending(args))
// TODO Also ugly types
const result: Await<ReturnType<PayloadCreator>> = await payloadCreator({
args,
dispatch,
getState,
extra
})

// TODO How do we avoid errors in here from hitting the catch clause?
return dispatch(fulfilled(result, args))
} catch (err) {
// TODO Errors aren't serializable
dispatch(rejected(err, args))
} finally {
// TODO IS there really a benefit from a "finished" action?
dispatch(finished(args))
}
}
}

actionCreator.pending = pending
actionCreator.rejected = rejected
actionCreator.fulfilled = fulfilled
actionCreator.finished = finished

return actionCreator
}
38 changes: 38 additions & 0 deletions src/entities/create_adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { EntityDefinition, Comparer, IdSelector, EntityAdapter } from './models'
import { createInitialStateFactory } from './entity_state'
import { createSelectorsFactory } from './state_selectors'
import { createSortedStateAdapter } from './sorted_state_adapter'
import { createUnsortedStateAdapter } from './unsorted_state_adapter'

/**
*
* @param options
*
* @alpha
*/
export function createEntityAdapter<T>(
options: {
selectId?: IdSelector<T>
sortComparer?: false | Comparer<T>
} = {}
): EntityAdapter<T> {
const { selectId, sortComparer }: EntityDefinition<T> = {
sortComparer: false,
selectId: (instance: any) => instance.id,
...options
}

const stateFactory = createInitialStateFactory<T>()
const selectorsFactory = createSelectorsFactory<T>()
const stateAdapter = sortComparer
? createSortedStateAdapter(selectId, sortComparer)
: createUnsortedStateAdapter(selectId)

return {
selectId,
sortComparer,
...stateFactory,
...selectorsFactory,
...stateAdapter
}
}
Loading