Skip to content
This repository has been archived by the owner on Sep 10, 2022. It is now read-only.

Commit

Permalink
Add class-like state behavior option to withState
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim Kindberg committed Apr 14, 2017
1 parent efbefe9 commit 24657af
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 6 deletions.
22 changes: 18 additions & 4 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,14 @@ const Post = enhance(({ title, content, author }) =>

```js
withState(
stateName: string,
stateUpdaterName: string,
stateName: ?string,
stateUpdaterName: ?string,
initialState: any | (props: Object) => any
): HigherOrderComponent
```
Passes two additional props to the base component: a state value, and a function to update that state value.

Passes two additional props to the base component: a state value, and a function to update that state value. The state updater has the following signature:
If stateName and stateUpdateName are provided then the behavior is as follows. The state updater has the following signature:

```js
stateUpdater<T>((prevValue: T) => T, ?callback: Function): void
Expand All @@ -273,7 +274,20 @@ The second form accepts a single value, which is used as the new state.

Both forms accept an optional second parameter, a callback function that will be executed once `setState()` is completed and the component is re-rendered.

An initial state value is required. It can be either the state value itself, or a function that returns an initial state given the initial props.
Only the initialState param is required. It can be either the state value itself, or a function that returns an initial state given the initial props.

If stateName and stateUpdaterName are not provided then they default to `'state'` and `'setState'`. In this form, state and setState behave exactly like setState in a typical react class. So `state` will be an object and `setState()` will accept a new state object to merge with the existing state. In this form `setState()` also still allows either an object or function as a param for updating state:

```js
const addCounting = compose(
withState({ count: 0 }),
withHandlers({
increment: ({ setState }) => () => setState(({ count }) => count + 1),
decrement: ({ setState }) => () => setState(({ count }) => count - 1),
reset: ({ setState }) => () => setState({ count: 0 })
}))
)
```

### `withReducer()`

Expand Down
67 changes: 66 additions & 1 deletion src/packages/recompose/__tests__/withState-test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import test from 'ava'
import React from 'react'
import { withState } from '../'
import { mount } from 'enzyme'
import { mount, shallow } from 'enzyme'
import sinon from 'sinon'

test('withState adds a stateful value and a function for updating it', t => {
Expand Down Expand Up @@ -69,3 +69,68 @@ test('withState also accepts initialState as function of props', t => {
updateCounter(n => n * 3)
t.is(component.lastCall.args[0].counter, 3)
})

test('withState (1 param) adds state and setState props', t => {
const enhance = withState({ clicked: 'no' })
const Component = enhance(({ state, setState }) =>
<button
onClick={() => setState({ clicked: 'yes' })}
>
{state.clicked}{state.foo}
</button>
)

const wrapper = shallow(<Component />)

t.is(wrapper.text(), 'no')
wrapper.find('button').simulate('click')
t.is(wrapper.text(), 'yes')
})

test('withState (1 param) allows modify and merge in same update', t => {
const enhance = withState({ foo: 'empty' })
const Component = enhance(({ state, setState }) =>
<button
onClick={() => setState({ foo: 'foo', bar: 'bar' })}
>
{state.foo}{state.bar}
</button>
)

const wrapper = shallow(<Component />)

t.is(wrapper.text(), 'empty')
wrapper.find('button').simulate('click')
t.is(wrapper.text(), 'foobar')
})

test('withState (1 param) accepts setState() callback', t => {
const enhance = withState({ foo: 'foo' })
const callback = sinon.stub()
const Component = enhance(({ setState }) =>
<button
onClick={() => setState({ foo: 'bar' }, callback)}
/>
)

const wrapper = shallow(<Component />)
wrapper.find('button').simulate('click')
t.is(callback.called, true)
})

test('withState (1 param) accepts initialState as function of props', t => {
const enhance = withState(({ initialText }) => ({ text: initialText }))
const Component = enhance(({ state, setState }) =>
<button
onClick={() => setState({ text: 'new text' })}
>
{state.text}
</button>
)

const wrapper = shallow(<Component initialText="old text" />)

t.is(wrapper.text(), 'old text')
wrapper.find('button').simulate('click')
t.is(wrapper.text(), 'new text')
})
32 changes: 31 additions & 1 deletion src/packages/recompose/withState.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Component } from 'react'
import createHelper from './createHelper'
import createEagerFactory from './createEagerFactory'

const withState = (stateName, stateUpdaterName, initialState) =>
const withStateOriginal = (stateName, stateUpdaterName, initialState) =>
BaseComponent => {
const factory = createEagerFactory(BaseComponent)
return class extends Component {
Expand Down Expand Up @@ -30,4 +30,34 @@ const withState = (stateName, stateUpdaterName, initialState) =>
}
}

const withClasslikeState = (initialState) =>
BaseComponent => {
const factory = createEagerFactory(BaseComponent)
return class extends Component {
state = typeof initialState === 'function'
? initialState(this.props)
: initialState

setStateFn = (...args) => {
this.setState(...args)
}

render() {
return factory({
...this.props,
state: this.state,
setState: this.setStateFn
})
}
}
}

const withState = (...args) => {
if (args.length === 1) {
return withClasslikeState(...args)
}

return withStateOriginal(...args)
}

export default createHelper(withState, 'withState')

0 comments on commit 24657af

Please sign in to comment.