diff --git a/README.md b/README.md index f307157..35a8aed 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ Returns **[store](#store)** An observable state container, returned from [createStore](#createstore) -##### action +##### dispatch Create a bound copy of the given action function. The bound returned function invokes action() and persists the result back to the store. diff --git a/devtools.js b/devtools.js index 6c6b2f1..e39e54d 100644 --- a/devtools.js +++ b/devtools.js @@ -18,11 +18,16 @@ module.exports = function unistoreDevTools(store) { } }); store.devtools.init(store.getState()); - store.subscribe(function (state, action) { - var actionName = action && action.name || 'setState'; + store.subscribe(function (state, action, update) { + update = update || {}; + var actionName = action + ? action.type || + action.name || + 'N/A (' + (Object.keys(update).join(', ') || 'none') + ')' + : 'setState (' + (Object.keys(update).join(', ') || 'none') + ')'; if (!ignoreState) { - store.devtools.send(actionName, state); + store.devtools.send({ type: actionName, update: update }, state); } else { ignoreState = false; } diff --git a/index.d.ts b/index.d.ts index 98f9353..a81ede6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,13 +3,40 @@ // K - Store state // I - Injected props to wrapped component -export type Listener = (state: K, action?: Action) => void; +export type Listener = (state: K, action?: Action, update?: Partial) => void; export type Unsubscribe = () => void; -export type Action = (state: K, ...args: any[]) => void; -export type BoundAction = (...args: any[]) => void; + +export type AsyncActionFn = (getState: () => K, action: (action: Action) => Promise | void) => Promise | void>; +export type SyncActionFn = (getState: () => K, action: (action: Action) => Promise | void) => Partial | void; +export type ActionFn = AsyncActionFn | SyncActionFn; + +export type AsyncActionObject = { + type: string; + action: AsyncActionFn; +} +export type SyncActionObject = { + type: string; + action: SyncActionFn; +} +export type ActionObject = AsyncActionObject | SyncActionObject; + +export type Action = ActionObject | ActionFn; + +export type AsyncActionCreator = (...args: any[]) => AsyncActionFn | AsyncActionObject; +export type SyncActionCreator = (...args: any[]) => SyncActionFn | SyncActionObject; +export type ActionCreator = AsyncActionCreator | SyncActionCreator; + +export type ActionCreatorsObject = { + [actionCreator: string]: ActionCreator +} + +export type MappedActionCreators = { + [P in keyof A]: A[P] extends AsyncActionCreator ? (...args: any[]) => Promise : (...args: any[]) => void +} + export interface Store { - action(action: Action): BoundAction; + dispatch(action: Action): Promise | void; setState(update: Pick, overwrite?: boolean, action?: Action): void; subscribe(f: Listener): Unsubscribe; unsubscribe(f: Listener): void; @@ -18,12 +45,4 @@ export interface Store { export default function createStore(state?: K): Store; -export type ActionFn = (state: K, ...args: any[]) => Promise> | Partial | void; - -export interface ActionMap { - [actionName: string]: ActionFn; -} - -export type ActionCreator = (store: Store) => ActionMap; - export type StateMapper = (state: K, props: T) => I; diff --git a/package.json b/package.json index 60f688e..c5d27b0 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "eslintConfig": { "extends": "eslint-config-developit", "rules": { - "prefer-rest-params": 0 + "prefer-rest-params": 0, + "prefer-spread": 0 } }, "bundlesize": [ diff --git a/preact.d.ts b/preact.d.ts index 6fc65f0..cf3bd5b 100644 --- a/preact.d.ts +++ b/preact.d.ts @@ -5,20 +5,20 @@ declare module 'unistore/preact' { import * as Preact from 'preact'; - import { ActionCreator, StateMapper, Store } from 'unistore'; + import { StateMapper, Store, ActionCreatorsObject, MappedActionCreators } from 'unistore'; - export function connect( - mapStateToProps: string | Array | StateMapper, - actions?: ActionCreator | object + export function connect>( + mapStateToProps: string | Array | StateMapper | null, + actions?: A, ): ( - Child: Preact.ComponentConstructor | Preact.AnyComponent + Child: Preact.ComponentConstructor, S> | Preact.AnyComponent, S> ) => Preact.ComponentConstructor; - export interface ProviderProps { - store: Store; + export interface ProviderProps { + store: Store; } - export class Provider extends Preact.Component> { - render(props: ProviderProps): Preact.JSX.Element; + export class Provider extends Preact.Component> { + render(props: ProviderProps): Preact.JSX.Element; } } diff --git a/react.d.ts b/react.d.ts index 1c95355..41137f2 100644 --- a/react.d.ts +++ b/react.d.ts @@ -2,23 +2,24 @@ // S - Wrapped component state // K - Store state // I - Injected props to wrapped component +// A - actions declare module 'unistore/react' { import * as React from 'react'; - import { ActionCreator, StateMapper, Store } from 'unistore'; + import { StateMapper, Store, ActionCreatorsObject, MappedActionCreators } from 'unistore'; - export function connect( - mapStateToProps: string | Array | StateMapper, - actions?: ActionCreator | object + export function connect>( + mapStateToProps: string | Array | StateMapper | null, + actions?: A, ): ( - Child: ((props: T & I) => React.ReactNode) | React.ComponentClass | React.FC + Child: ((props: T & I & MappedActionCreators) => React.ReactNode) | React.ComponentClass, S> | React.FC> ) => React.ComponentClass | React.FC; - export interface ProviderProps { - store: Store; + export interface ProviderProps { + store: Store; } - export class Provider extends React.Component, {}> { + export class Provider extends React.Component, {}> { render(): React.ReactNode; } diff --git a/src/index.js b/src/index.js index 4df2006..f6976b4 100644 --- a/src/index.js +++ b/src/index.js @@ -28,10 +28,25 @@ export default function createStore(state) { listeners = out; } + function dispatch(action) { + function apply(result) { + setState(result, false, action); + } + let ret = (action.action || action)(getState, dispatch); + if (ret != null) { + if (ret.then) return ret.then(apply); + return apply(ret); + } + } + + function getState() { + return state; + } + function setState(update, overwrite, action) { state = overwrite ? update : assign(assign({}, state), update); let currentListeners = listeners; - for (let i=0; i stateUpdate` * @returns {Function} boundAction() */ - action(action) { - function apply(result) { - setState(result, false, action); - } - - // Note: perf tests verifying this implementation: https://esbench.com/bench/5a295e6299634800a0349500 - return function() { - let args = [state]; - for (let i=0; i { describe('smoke test (preact)', () => { it('should render', done => { const { Provider, connect } = preact; - const actions = ({ getState, setState }) => ({ - incrementTwice(state) { - setState({ count: state.count + 1 }); + const actions = { + incrementTwice: () => (getState, dispatch) => { + dispatch(() => ({ count: getState().count + 1 })); return new Promise(r => setTimeout(() => { r({ count: getState().count + 1 }); }, 20) ); } - }); + }; const App = connect('count', actions)(({ count, incrementTwice }) => ( )); diff --git a/test/react/builds.test.js b/test/react/builds.test.js index 099bd68..1a2e7ae 100644 --- a/test/react/builds.test.js +++ b/test/react/builds.test.js @@ -33,16 +33,16 @@ describe('build: default', () => { describe('smoke test (react)', () => { it('should render', done => { const { Provider, connect } = react; - const actions = ({ getState, setState }) => ({ - incrementTwice(state) { - setState({ count: state.count + 1 }); + const actions = { + incrementTwice: () => (getState, dispatch) => { + dispatch(() => ({ count: getState().count + 1 })) return new Promise(r => setTimeout(() => { r({ count: getState().count + 1 }); }, 20) ); } - }); + }; const App = connect('count', actions)(({ count, incrementTwice }) => (