Skip to content

Commit

Permalink
add support for shouldComponentUpdate (fix #16)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcherny committed Apr 2, 2018
1 parent 2addf3f commit d85fee1
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 44 deletions.
79 changes: 60 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,87 @@ import { Emitter } from 'typed-rx-emitter'
export type Undux<Actions extends object> = {
[K in keyof Actions]: {
key: K
previousValue: Actions[K],
previousValue: Actions[K]
value: Actions[K]
}
}

export class Store<Actions extends object> extends Emitter<Actions> {
private befores = new Emitter<Undux<Actions>>()
private emitter = new Emitter<Actions>()
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<Actions extends object> {
get<K extends keyof Actions>(key: K): Actions[K]
set<K extends keyof Actions>(key: K): (value: Actions[K]) => void
on<K extends keyof Actions>(key: K): RxJS.Observable<Actions[K]>
onAll<K extends keyof Actions>(): RxJS.Observable<Actions[keyof Actions]>
before<K extends keyof Actions>(key: K): RxJS.Observable<Undux<Actions>[K]>
beforeAll<K extends keyof Actions>(): RxJS.Observable<Undux<Actions>[keyof Actions]>
}

export class StoreSnapshot<Actions extends object> implements Store<Actions> {
constructor(
private state: Actions,
private store: StoreProxy<Actions>
) { }
get<K extends keyof Actions>(key: K) {
return this.state[key]
}
set<K extends keyof Actions>(key: K) {
return this.store.set(key)
}
on<K extends keyof Actions>(key: K): RxJS.Observable<Actions[K]> {
return this.store.on(key)
}
onAll<K extends keyof Actions>(): RxJS.Observable<Actions[keyof Actions]> {
return this.store.onAll()
}
before<K extends keyof Actions>(key: K): RxJS.Observable<Undux<Actions>[K]> {
return this.store.before(key)
}
beforeAll<K extends keyof Actions>(): RxJS.Observable<Undux<Actions>[keyof Actions]> {
return this.store.beforeAll()
}

private assign<Actions extends object, K extends keyof Actions>(
key: K, value: Actions[K]) {
return new StoreSnapshot(Object.assign({}, this.state, { [key]: value }), this.store)
}
}

export class StoreProxy<Actions extends object> implements Store<Actions> {
private store: StoreSnapshot<Actions>
private befores: Emitter<Undux<Actions>> = new Emitter
private emitter: Emitter<Actions> = new Emitter
constructor(state: Actions) {
this.store = new StoreSnapshot(state, this)
}
before<K extends keyof Actions>(key: K): RxJS.Observable<Undux<Actions>[K]> {
return this.befores.on(key)
}
beforeAll<K extends keyof Actions>(): RxJS.Observable<Undux<Actions>[keyof Actions]> {
return this.befores.all()
}
on<K extends keyof Actions>(key: K): RxJS.Observable<Actions[K]> {
return this.emitter.on(key)
}
onAll<K extends keyof Actions>(): RxJS.Observable<Actions[keyof Actions]> {
return this.emitter.all()
}
get<K extends keyof Actions>(key: K) {
return this.state[key]
return this.store.get(key)
}
set<K extends keyof Actions>(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<Actions extends object>(initialState: Actions) {
return new Store<Actions>(initialState)
return new StoreProxy<Actions>(initialState)
}

export type Plugin = <Actions extends object>(store: Store<Actions>) => Store<Actions>
export type Plugin = <Actions extends object>(store: StoreProxy<Actions>) => StoreProxy<Actions>

export * from './plugins/logger'
export * from './react'
59 changes: 34 additions & 25 deletions src/react.tsx
Original file line number Diff line number Diff line change
@@ -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<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T]
export type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] }

export function connect<Actions extends object>(store: Store<Actions>) {
export function connect<Actions extends object>(store: StoreProxy<Actions>) {
return (...listenOn: (keyof Actions)[]) => {
return function <
Props,
Expand All @@ -16,38 +16,47 @@ export function connect<Actions extends object>(store: Store<Actions>) {
Component: React.ComponentType<PropsWithStore>
): React.ComponentClass<Omit<PropsWithStore, 'store'>> {

let state: Subscription[][]
type State = {
store: StoreSnapshot<Actions>
subscriptions: Subscription[][]
}

let Class: ComponentClass<Omit<PropsWithStore, 'store'>> = class extends React.Component<Omit<PropsWithStore, 'store'>> {
return class extends React.Component<Omit<PropsWithStore, 'store'>, 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<PropsWithStore, 'store'>, state: State) {
return state.store !== this.state.store
}
render() {
return <Component {...this.props} store={store} />
return <Component {...this.props} store={this.state.store} />
}
}

Class.displayName = `withStore(${getDisplayName(Component)})`

return Class
}
}
}
47 changes: 47 additions & 0 deletions test/stateful.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Actions>({
isTrue: true,
users: []
})
let A = connect(store)('users')(
class extends React.Component<Props> {
shouldComponentUpdate({ store }: Props) {
return store.get('users').length > 3
}
componentDidUpdate() {
updateCount++
}
componentWillReceiveProps() {
willReceivePropsCount++
}
render() {
renderCount++
return <div>
{this.props.store.get('users').length > 3 ? 'FRESH' : 'STALE'}
<button onClick={() =>
this.props.store.set('users')(this.props.store.get('users').concat('x'))
}>Update</button>
</div>
}
}
)

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)
})
})

0 comments on commit d85fee1

Please sign in to comment.