This repository has been archived by the owner on Sep 10, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
withStateHandlers (new withState) (#421)
* Move mapValues into utils * Add withStateHandlers
- Loading branch information
Showing
7 changed files
with
301 additions
and
12 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
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
193 changes: 193 additions & 0 deletions
193
src/packages/recompose/__tests__/withStateHandlers-test.js
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,193 @@ | ||
import React from 'react' | ||
import { mount } from 'enzyme' | ||
import sinon from 'sinon' | ||
|
||
import { compose, withStateHandlers } from '../' | ||
|
||
test('withStateHandlers adds a stateful value and a function for updating it', () => { | ||
const component = sinon.spy(() => null) | ||
component.displayName = 'component' | ||
|
||
const Counter = withStateHandlers( | ||
{ counter: 0 }, | ||
{ | ||
updateCounter: ({ counter }) => increment => ({ | ||
counter: counter + increment, | ||
}), | ||
} | ||
)(component) | ||
expect(Counter.displayName).toBe('withStateHandlers(component)') | ||
|
||
mount(<Counter pass="through" />) | ||
const { updateCounter } = component.firstCall.args[0] | ||
|
||
expect(component.lastCall.args[0].counter).toBe(0) | ||
expect(component.lastCall.args[0].pass).toBe('through') | ||
|
||
updateCounter(9) | ||
expect(component.lastCall.args[0].counter).toBe(9) | ||
updateCounter(1) | ||
updateCounter(10) | ||
|
||
expect(component.lastCall.args[0].counter).toBe(20) | ||
expect(component.lastCall.args[0].pass).toBe('through') | ||
}) | ||
|
||
test('withStateHandlers accepts initialState as function of props', () => { | ||
const component = sinon.spy(() => null) | ||
component.displayName = 'component' | ||
|
||
const Counter = withStateHandlers( | ||
({ initialCounter }) => ({ | ||
counter: initialCounter, | ||
}), | ||
{ | ||
updateCounter: ({ counter }) => increment => ({ | ||
counter: counter + increment, | ||
}), | ||
} | ||
)(component) | ||
|
||
const initialCounter = 101 | ||
|
||
mount(<Counter initialCounter={initialCounter} />) | ||
expect(component.lastCall.args[0].counter).toBe(initialCounter) | ||
}) | ||
|
||
test('withStateHandlers initial state must be function or object or null or undefined', () => { | ||
const component = sinon.spy(() => null) | ||
component.displayName = 'component' | ||
|
||
const Counter = withStateHandlers(1, {})(component) | ||
// React throws an error | ||
expect(() => mount(<Counter />)).toThrow() | ||
}) | ||
|
||
test('withStateHandlers have access to props', () => { | ||
const component = sinon.spy(() => null) | ||
component.displayName = 'component' | ||
|
||
const Counter = withStateHandlers( | ||
({ initialCounter }) => ({ | ||
counter: initialCounter, | ||
}), | ||
{ | ||
increment: ({ counter }, { incrementValue }) => () => ({ | ||
counter: counter + incrementValue, | ||
}), | ||
} | ||
)(component) | ||
|
||
const initialCounter = 101 | ||
const incrementValue = 37 | ||
|
||
mount( | ||
<Counter initialCounter={initialCounter} incrementValue={incrementValue} /> | ||
) | ||
|
||
const { increment } = component.firstCall.args[0] | ||
|
||
increment() | ||
expect(component.lastCall.args[0].counter).toBe( | ||
initialCounter + incrementValue | ||
) | ||
}) | ||
|
||
test('withStateHandlers passes immutable state updaters', () => { | ||
const component = sinon.spy(() => null) | ||
component.displayName = 'component' | ||
|
||
const Counter = withStateHandlers( | ||
({ initialCounter }) => ({ | ||
counter: initialCounter, | ||
}), | ||
{ | ||
increment: ({ counter }, { incrementValue }) => () => ({ | ||
counter: counter + incrementValue, | ||
}), | ||
} | ||
)(component) | ||
|
||
const initialCounter = 101 | ||
const incrementValue = 37 | ||
|
||
mount( | ||
<Counter initialCounter={initialCounter} incrementValue={incrementValue} /> | ||
) | ||
|
||
const { increment } = component.firstCall.args[0] | ||
|
||
increment() | ||
expect(component.lastCall.args[0].counter).toBe( | ||
initialCounter + incrementValue | ||
) | ||
}) | ||
|
||
test('withStateHandlers does not rerender if state updater returns undefined', () => { | ||
const component = sinon.spy(() => null) | ||
component.displayName = 'component' | ||
|
||
const Counter = withStateHandlers( | ||
({ initialCounter }) => ({ | ||
counter: initialCounter, | ||
}), | ||
{ | ||
updateCounter: ({ counter }) => increment => | ||
increment === 0 | ||
? undefined | ||
: { | ||
counter: counter + increment, | ||
}, | ||
} | ||
)(component) | ||
|
||
const initialCounter = 101 | ||
|
||
mount(<Counter initialCounter={initialCounter} />) | ||
expect(component.callCount).toBe(1) | ||
|
||
const { updateCounter } = component.firstCall.args[0] | ||
|
||
updateCounter(1) | ||
expect(component.callCount).toBe(2) | ||
|
||
updateCounter(0) | ||
expect(component.callCount).toBe(2) | ||
}) | ||
|
||
test('withStateHandlers rerenders if parent props changed', () => { | ||
const component = sinon.spy(() => null) | ||
component.displayName = 'component' | ||
|
||
const Counter = compose( | ||
withStateHandlers( | ||
({ initialCounter }) => ({ | ||
counter: initialCounter, | ||
}), | ||
{ | ||
increment: ({ counter }) => incrementValue => ({ | ||
counter: counter + incrementValue, | ||
}), | ||
} | ||
), | ||
withStateHandlers( | ||
{ incrementValue: 1 }, | ||
{ | ||
// updates parent state and return undefined | ||
updateParentIncrement: ({ incrementValue }, { increment }) => () => { | ||
increment(incrementValue) | ||
return undefined | ||
}, | ||
} | ||
) | ||
)(component) | ||
|
||
const initialCounter = 101 | ||
|
||
mount(<Counter initialCounter={initialCounter} />) | ||
|
||
const { updateParentIncrement } = component.firstCall.args[0] | ||
|
||
updateParentIncrement() | ||
expect(component.lastCall.args[0].counter).toBe(initialCounter + 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
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,13 @@ | ||
const mapValues = (obj, func) => { | ||
const result = {} | ||
/* eslint-disable no-restricted-syntax */ | ||
for (const key in obj) { | ||
if (obj.hasOwnProperty(key)) { | ||
result[key] = func(obj[key], key) | ||
} | ||
} | ||
/* eslint-enable no-restricted-syntax */ | ||
return result | ||
} | ||
|
||
export default mapValues |
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
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,45 @@ | ||
import { Component } from 'react' | ||
import setDisplayName from './setDisplayName' | ||
import wrapDisplayName from './wrapDisplayName' | ||
import createEagerFactory from './createEagerFactory' | ||
import shallowEqual from './shallowEqual' | ||
import mapValues from './utils/mapValues' | ||
|
||
const withStateHandlers = (initialState, stateUpdaters) => BaseComponent => { | ||
const factory = createEagerFactory(BaseComponent) | ||
|
||
class WithStateHandlers extends Component { | ||
state = typeof initialState === 'function' | ||
? initialState(this.props) | ||
: initialState | ||
|
||
stateUpdaters = mapValues(stateUpdaters, handler => (...args) => | ||
this.setState((state, props) => handler(state, props)(...args)) | ||
) | ||
|
||
shouldComponentUpdate(nextProps, nextState) { | ||
const propsChanged = nextProps !== this.props | ||
// the idea is to skip render if stateUpdater handler return undefined | ||
// this allows to create no state update handlers with access to state and props | ||
const stateChanged = !shallowEqual(nextState, this.state) | ||
return propsChanged || stateChanged | ||
} | ||
|
||
render() { | ||
return factory({ | ||
...this.props, | ||
...this.state, | ||
...this.stateUpdaters, | ||
}) | ||
} | ||
} | ||
|
||
if (process.env.NODE_ENV !== 'production') { | ||
return setDisplayName(wrapDisplayName(BaseComponent, 'withStateHandlers'))( | ||
WithStateHandlers | ||
) | ||
} | ||
return WithStateHandlers | ||
} | ||
|
||
export default withStateHandlers |