diff --git a/src/index.ts b/src/index.ts index bc45047..1e0b915 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,25 +4,56 @@ import { Emitter } from 'typed-rx-emitter' export type Undux = { [K in keyof Actions]: { key: K - previousValue: Actions[K], + previousValue: Actions[K] value: Actions[K] } } -export class Store extends Emitter { - private befores = new Emitter>() - private emitter = new Emitter() - constructor(private state: Actions) { - super() - - for (let key in state) { - this.emitter.on(key).subscribe(value => { - let previousValue = state[key] - this.befores.emit(key, { key, previousValue, value }) - state[key] = value - this.emit(key, value) - }) - } +export interface Store { + get(key: K): Actions[K] + set(key: K): (value: Actions[K]) => void + on(key: K): RxJS.Observable + onAll(): RxJS.Observable + before(key: K): RxJS.Observable[K]> + beforeAll(): RxJS.Observable[keyof Actions]> +} + +export class StoreSnapshot implements Store { + constructor( + private state: Actions, + private store: StoreProxy + ) { } + get(key: K) { + return this.state[key] + } + set(key: K) { + return this.store.set(key) + } + on(key: K): RxJS.Observable { + return this.store.on(key) + } + onAll(): RxJS.Observable { + return this.store.onAll() + } + before(key: K): RxJS.Observable[K]> { + return this.store.before(key) + } + beforeAll(): RxJS.Observable[keyof Actions]> { + return this.store.beforeAll() + } + + private assign( + key: K, value: Actions[K]) { + return new StoreSnapshot(Object.assign({}, this.state, { [key]: value }), this.store) + } +} + +export class StoreProxy implements Store { + private store: StoreSnapshot + private befores: Emitter> = new Emitter + private emitter: Emitter = new Emitter + constructor(state: Actions) { + this.store = new StoreSnapshot(state, this) } before(key: K): RxJS.Observable[K]> { return this.befores.on(key) @@ -30,20 +61,30 @@ export class Store extends Emitter { beforeAll(): RxJS.Observable[keyof Actions]> { return this.befores.all() } + on(key: K): RxJS.Observable { + return this.emitter.on(key) + } + onAll(): RxJS.Observable { + return this.emitter.all() + } get(key: K) { - return this.state[key] + return this.store.get(key) } set(key: K) { - return (value: Actions[K]) => + return (value: Actions[K]) => { + let previousValue = this.store.get(key) + this.befores.emit(key, { key, previousValue, value }) + this.store = this.store['assign'](key, value) this.emitter.emit(key, value) + } } } export function createStore(initialState: Actions) { - return new Store(initialState) + return new StoreProxy(initialState) } -export type Plugin = (store: Store) => Store +export type Plugin = (store: StoreProxy) => StoreProxy export * from './plugins/logger' export * from './react' diff --git a/src/react.tsx b/src/react.tsx index d2ee7a1..6745b8e 100644 --- a/src/react.tsx +++ b/src/react.tsx @@ -1,13 +1,13 @@ import * as React from 'react' import { ComponentClass } from 'react' import { Subscription } from 'rxjs' -import { Store } from './' +import { Store, StoreProxy, StoreSnapshot } from './' import { equals, getDisplayName } from './utils' export type Diff = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T] export type Omit = { [P in Diff]: T[P] } -export function connect(store: Store) { +export function connect(store: StoreProxy) { return (...listenOn: (keyof Actions)[]) => { return function < Props, @@ -16,38 +16,47 @@ export function connect(store: Store) { Component: React.ComponentType ): React.ComponentClass> { - let state: Subscription[][] + type State = { + store: StoreSnapshot + subscriptions: Subscription[][] + } - let Class: ComponentClass> = class extends React.Component> { + return class extends React.Component, State> { + static displayName = `withStore(${getDisplayName(Component)})` + state: State = { + store: store['store'], + subscriptions: [] + } componentDidMount() { - state = listenOn.map(key => { - let ignore = false - return [ - store.before(key).subscribe(({ previousValue, value }) => { - if (equals(previousValue, value)) { - return ignore = true - } - }), - store.on(key).subscribe(() => { - if (ignore) { - return ignore = false - } - this.forceUpdate() - }) - ] + this.setState({ + subscriptions: listenOn.map(key => { + let ignore = false + return [ + store.before(key).subscribe(({ previousValue, value }) => { + if (equals(previousValue, value)) { + return ignore = true + } + }), + store.on(key).subscribe(() => { + if (ignore) { + return ignore = false + } + this.setState({ store: store['store'] }) + }) + ] + }) }) } componentWillUnmount() { - state.forEach(_ => _.forEach(_ => _.unsubscribe())) + this.state.subscriptions.forEach(_ => _.forEach(_ => _.unsubscribe())) + } + shouldComponentUpdate(_: Omit, state: State) { + return state.store !== this.state.store } render() { - return + return } } - - Class.displayName = `withStore(${getDisplayName(Component)})` - - return Class } } } diff --git a/test/stateful.tsx b/test/stateful.tsx index e2d091a..954e7b9 100644 --- a/test/stateful.tsx +++ b/test/stateful.tsx @@ -158,3 +158,50 @@ test('[stateful] it should typecheck with additional props', t => { t.pass() }) + +test('[stateful] it should support lifecycle methods', t => { + + let renderCount = 0 + let updateCount = 0 + let willReceivePropsCount = 0 + let store = createStore({ + isTrue: true, + users: [] + }) + let A = connect(store)('users')( + class extends React.Component { + shouldComponentUpdate({ store }: Props) { + return store.get('users').length > 3 + } + componentDidUpdate() { + updateCount++ + } + componentWillReceiveProps() { + willReceivePropsCount++ + } + render() { + renderCount++ + return
+ {this.props.store.get('users').length > 3 ? 'FRESH' : 'STALE'} + +
+ } + } + ) + + withElement(A, _ => { + Simulate.click(_.querySelector('button')!) + t.regex(_.innerHTML, /STALE/) + Simulate.click(_.querySelector('button')!) + t.regex(_.innerHTML, /STALE/) + Simulate.click(_.querySelector('button')!) + t.regex(_.innerHTML, /STALE/) + Simulate.click(_.querySelector('button')!) + t.regex(_.innerHTML, /FRESH/) + t.is(renderCount, 2) + t.is(updateCount, 1) + t.is(willReceivePropsCount, 4) + }) +})