Skip to content

Commit

Permalink
Merge pull request #8 from whizkydee/dev
Browse files Browse the repository at this point in the history
Type definition bug fixed & setState now accepts a callback function containing prevState
  • Loading branch information
whizkydee committed Apr 7, 2020
2 parents 36b6980 b3aad23 commit bce27c0
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 39 deletions.
8 changes: 4 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 8,17 @@ import { Dispatch, SetStateAction } from 'react'
*
* `setState` is a multi-action dispatcher function that takes in a new state object.
*
* `setters` is an array that contains composed dispatchAction functions for each state property.
* `setters` is an object that contains composed dispatchAction functions for each state property.
*
* @see https://github.com/whizkydee/react-multi-state#-usage
*/
export default function useMultiState<
T extends { [key: string]: any },
U extends T[keyof T]
U extends Partial<T>
>(
initialState: T
): [
T,
(newState: Partial<T>) => void,
{ [key: string]: Dispatch<SetStateAction<U>> }
((newState: (prevState: T) => U) => void) & ((newState: U) => void),
{ [key: string]: Dispatch<SetStateAction<unknown>> }
]
12 changes: 8 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 17,13 @@ export default function useMultiState(initialState) {

return [
state,
newState => {
for (const prop in newState) {
internalDispatchers[prop](newState[prop])
newStateOrCb => {
if (typeof newStateOrCb == 'function') {
// pass state to the updater function as prevState
newStateOrCb = newStateOrCb.call(this, state)
}
for (const prop in newStateOrCb) {
internalDispatchers[prop](newStateOrCb[prop])
}
},
setters,
Expand All @@ -28,7 32,7 @@ export default function useMultiState(initialState) {

function once(fn) {
let called = false
return function(...args) {
return function (...args) {
if (called) return
called = true
return fn.apply(this, args)
Expand Down
37 changes: 22 additions & 15 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 6,7 @@ export function TestComponent() {
const [state, setState, setters] = useMultiState({
age: 25,
name: 'Chris',
interests: ['Biking', 'Sky diving'],
interests: ['Biking', 'Skydiving'],
})

expectType<number>(state.age)
Expand All @@ -17,20 17,27 @@ export function TestComponent() {
expectError(setState({ age: '20' }))
expectType<void>(setState({ age: 20 }))

// Test that the value of the `prevState` parameter in `setState`
// has the same signature as that of `state`.
setState(prevState => {
expectType<{
age: number
name: string
interests: string[]
}>(prevState)
return {
age: prevState.age 2,
}
})

expectType<void>(setters.setName('Olaolu'))

// TypeScript doesn't have an API that allows us dynamically augment property names.
// See https://github.com/microsoft/TypeScript/issues/12754.
//
// So, as a half-baked workaround to assert the type of each function in `setters`,
// we check the type of the value we pass to each dispatcher function and then assert
// that it contains at least one of the property value types specified in the local
// `state` object signature...
expectType<Dispatch<SetStateAction<string | number | string[]>>>(
setters.setName
)
// ...This is why we expect an error in the next LOC -- because going by our local
// `state` signature, none of the property values is an object. Pretty clever, eh?
expectError(setters.setAge({ a: 1 }))
}, [state.age, state.name, state.interests])
// TypeScript doesn't have an API that allows us dynamically augment property names
// which is quite unfortunate because we rely on that feature to provide an optimal
// TypeScript DX.
// See https://github.com/microsoft/TypeScript/issues/12754

// We expect `unknown` here becuase of https://github.com/whizkydee/react-multi-state/issues/7
expectType<Dispatch<SetStateAction<unknown>>>(setters.setName)
}, [setState, setters])
}
43 changes: 37 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 2,6 @@

🦍Declarative, simplified way to handle complex local state with hooks.

<!-- useState, but simplified for complex local states in React apps. -->

## ✨ Features

- 📦 ~286b (gzipped)
Expand Down Expand Up @@ -53,12 51,12 @@ export default function Users() {
})

useEffect(() => {
;(async function() {
;(async function () {
const usersData = await getUsers()
setState({ isFetched: true, users: usersData })
setCleanupAfter(true)
})()
}, [])
}, [setState, setCleanupAfter])

return (
<ul>
Expand All @@ -70,6 68,39 @@ export default function Users() {
}
```

## ↩ Accessing previous state

Currently, there are two ways to access previous state values before update, and
they do not require spreading the old state object at all. See the example
below.

```jsx
import { Fragment } from 'react'
function Counter() {
const [state, setState, { setCount }] = useMultiState({
count: 0,
secondCount: 10,
})

return (
<Fragment>
<button onClick={() => setCount(c => c 1)}>Update count</button>

<button
onClick={() => {
setState(prevState => ({
secondCount: prevState.secondCount 10,
// use as many `prevState` property values as you wish
}))
}}
>
Update second count
</button>
</Fragment>
)
}
```

## 👀 Comparison with `React.useState` (examples)

With `React.useState`, you'd have to call `useState` and the individual
Expand All @@ -86,12 117,12 @@ export default function Users() {
const [isFetched, setIsFetched] = useState(false)

useEffect(() => {
;(async function() {
;(async function () {
const usersData = await getUsers()
setUsers(usersData)
setIsFetched(true)
})()
}, [])
}, [setUsers, setIsFetched])
}
```

Expand Down
46 changes: 36 additions & 10 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 8,39 @@ describe('useMultiState assertions', () => {
it('returns an array containing a state object', () => {
const { state } = mockComponent()
expect(state).not.toBe(undefined)
expect(state).toStrictEqual({ count: 0, name: 'Olaolu' })
expect(state).toStrictEqual({ age: 10, count: 0, name: 'Olaolu' })
})

it('returns an array containing a setters object', () => {
const { setters } = mockComponent()

expect(setters).not.toBe(undefined)
expect(setters).toHaveProperty('setCount')
expect(typeof setters.setCount).toBe('function')
expect(setters).toHaveProperty('setName')
expect(typeof setters.setName).toBe('function')
expect(setters).toHaveProperty('setAge')
expect(typeof setters.setAge).toBe('function')
})

it('contains the right amount of properties in the state object', () => {
const { state } = mockComponent()
expect(Object.keys(state)).toHaveLength(2)
expect(Object.keys(state)).toHaveLength(3)
})

it('contains the right amount of properties in the setters object', () => {
const { setters } = mockComponent()
expect(Object.keys(setters)).toHaveLength(2)
expect(Object.keys(setters)).toHaveLength(3)
})

it('truly updates the values of items in the state object', () => {
const { getByTestId } = mockComponent()

fireEvent.click(getByTestId('btn'))
expect(getByTestId('text').textContent).toEqual('1')
fireEvent.click(getByTestId('count-btn'))
expect(getByTestId('count-text').textContent).toEqual('1')

fireEvent.click(getByTestId('btn'))
expect(getByTestId('text').textContent).toEqual('2')
fireEvent.click(getByTestId('count-btn'))
expect(getByTestId('count-text').textContent).toEqual('2')
})

it('truly updates the values of items in the state object via setState', () => {
Expand All @@ -51,25 54,36 @@ describe('useMultiState assertions', () => {
expect(getByTestId('input').value).toBe('Chris')
expect(getByTestId('currentName').textContent).toEqual('Chris')
})

it('updates state with the correct value via a prevState parameter passed to setState', () => {
const { getByTestId } = mockComponent()

fireEvent.click(getByTestId('age-btn'))
expect(getByTestId('age-text').textContent).toEqual('20') // uses `setState`

fireEvent.click(getByTestId('age-btn'))
expect(getByTestId('age-text').textContent).toEqual('30') // uses `setState`
})
})

function mockComponent() {
let payload = {}

function ReactComponent() {
const [state, setState, setters] = useMultiState({
age: 10,
count: 0,
name: 'Olaolu',
})
const { count, name } = state
const { age, count, name } = state
const { setCount } = setters

payload = { state, setState, setters }

return (
<div>
<p data-testid="text">{count}</p>
<button data-testid="btn" onClick={() => setCount(c => c 1)}>
<p data-testid="count-text">{count}</p>
<button data-testid="count-btn" onClick={() => setCount(c => c 1)}>
Update count
</button>

Expand All @@ -79,6 93,18 @@ function mockComponent() {
data-testid="input"
onChange={event => setState({ name: event.target.value })}
/>

<p data-testid="age-text">{age}</p>
<button
data-testid="age-btn"
onClick={() => {
setState(prevState => ({
age: prevState.age 10,
}))
}}
>
Update age
</button>
</div>
)
}
Expand Down

0 comments on commit bce27c0

Please sign in to comment.