Skip to content

Commit

Permalink
add state initializers back
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Mar 21, 2024
1 parent 9a6aeb3 commit 7b7eb57
Show file tree
Hide file tree
Showing 63 changed files with 407 additions and 0 deletions.
11 changes: 11 additions & 0 deletions exercises/06.state-initializers/01.problem.initial/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Initialize Toggle

👨‍💼 Our toggle component should be able to be customizable for the initial state
and reset to the initial state.

🧝‍♂️ I've updated the toggle component to use a reducer instead of `useState`. If
you'd like to back up, and do that yourself in the playground, by my guest. Or
you can <PrevDiffLink>check my work</PrevDiffLink> instead.

👨‍💼 Please add a case in our reducer for the `reset` logic, and add an option to
our `useToggle` hook for setting the initialOn state.
16 changes: 16 additions & 0 deletions exercises/06.state-initializers/01.problem.initial/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Switch } from '#shared/switch.tsx'
import { useToggle } from './toggle.tsx'

export function App() {
// 🐨 add an initialOn option (set it to true) and get the reset callback from useToggle
const { on, getTogglerProps } = useToggle()
// 💣 delete this reset callback in favor of what you get from useToggle
const reset = () => {}
return (
<div>
<Switch {...getTogglerProps({ on })} />
<hr />
<button onClick={reset}>Reset</button>
</div>
)
}
55 changes: 55 additions & 0 deletions exercises/06.state-initializers/01.problem.initial/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useReducer } from 'react'

function callAll<Args extends Array<unknown>>(
...fns: Array<((...args: Args) => unknown) | undefined>
) {
return (...args: Args) => fns.forEach(fn => fn?.(...args))
}

type ToggleState = { on: boolean }
type ToggleAction = { type: 'toggle' }
// 🦺 add an action type for reset:
// 💰 | { type: 'reset'; initialState: ToggleState }

function toggleReducer(state: ToggleState, action: ToggleAction) {
switch (action.type) {
case 'toggle': {
return { on: !state.on }
}
// 🐨 add a case for 'reset' that simply returns the "initialState"
// which you can get from the action.
}
}

// 🐨 We'll need to add an option for `initialOn` here (default to false)
export function useToggle() {
// 🐨 update the initialState object to use the initialOn option
const initialState = { on: false }
const [state, dispatch] = useReducer(toggleReducer, initialState)
const { on } = state

const toggle = () => dispatch({ type: 'toggle' })

// 🐨 add a reset function here which dispatches a 'reset' type with your
// initialState object and calls `onReset` with the initialState.on value

function getTogglerProps<Props>({
onClick,
...props
}: {
onClick?: React.ComponentProps<'button'>['onClick']
} & Props) {
return {
'aria-checked': on,
onClick: callAll(onClick, toggle),
...props,
}
}

return {
on,
toggle,
// 🐨 add your reset function here.
getTogglerProps,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Initialize Toggle

👨‍💼 Great job! Now we can initilize and reset the toggle component! But we've
discovered a bug. Let's look at that next.
13 changes: 13 additions & 0 deletions exercises/06.state-initializers/01.solution.initial/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Switch } from '#shared/switch.tsx'
import { useToggle } from './toggle.tsx'

export function App() {
const { on, getTogglerProps, reset } = useToggle({ initialOn: true })
return (
<div>
<Switch {...getTogglerProps({ on })} />
<hr />
<button onClick={reset}>Reset</button>
</div>
)
}
52 changes: 52 additions & 0 deletions exercises/06.state-initializers/01.solution.initial/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useReducer } from 'react'

function callAll<Args extends Array<unknown>>(
...fns: Array<((...args: Args) => unknown) | undefined>
) {
return (...args: Args) => fns.forEach(fn => fn?.(...args))
}

type ToggleState = { on: boolean }
type ToggleAction =
| { type: 'toggle' }
| { type: 'reset'; initialState: ToggleState }

function toggleReducer(state: ToggleState, action: ToggleAction) {
switch (action.type) {
case 'toggle': {
return { on: !state.on }
}
case 'reset': {
return action.initialState
}
}
}

export function useToggle({ initialOn = false } = {}) {
const initialState = { on: initialOn }
const [state, dispatch] = useReducer(toggleReducer, initialState)
const { on } = state

const toggle = () => dispatch({ type: 'toggle' })
const reset = () => dispatch({ type: 'reset', initialState })

function getTogglerProps<Props>({
onClick,
...props
}: {
onClick?: React.ComponentProps<'button'>['onClick']
} & Props) {
return {
'aria-checked': on,
onClick: callAll(onClick, toggle),
...props,
}
}

return {
on,
toggle,
reset,
getTogglerProps,
}
}
16 changes: 16 additions & 0 deletions exercises/06.state-initializers/02.problem.stability/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Stability

👨‍💼 We've noticed that if someone passes an `initialOn` that's based on state,
then calling `reset` will sometimes change the state to the wrong value based on
what the `initialOn` was set to at the time the component was initialized.

This is confusing and we want to make certain to avoid it.

🧝‍♂️ I've put together a simple example of this for you to experiment with. You'll
notice we now have a button for toggling the `initialOn` state which we pass as
an option to `useToggle`. So if you toggle the `initialOn` state and then click
the reset button, you'll notice it resets to the current `initialOn` state, not
the original one.

👨‍💼 This is a little confusing for users of the `useToggle` hook, so please fix
this issue! Thanks!
18 changes: 18 additions & 0 deletions exercises/06.state-initializers/02.problem.stability/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useState } from 'react'
import { Switch } from '#shared/switch.tsx'
import { useToggle } from './toggle.tsx'

export function App() {
const [initialOn, setInitialOn] = useState(true)
const { on, getTogglerProps, reset } = useToggle({ initialOn })
return (
<div>
<button onClick={() => setInitialOn(o => !o)}>
initialOn is: {initialOn ? 'true' : 'false'}
</button>
<Switch {...getTogglerProps({ on })} />
<hr />
<button onClick={reset}>Reset</button>
</div>
)
}
55 changes: 55 additions & 0 deletions exercises/06.state-initializers/02.problem.stability/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useReducer } from 'react'

function callAll<Args extends Array<unknown>>(
...fns: Array<((...args: Args) => unknown) | undefined>
) {
return (...args: Args) => fns.forEach(fn => fn?.(...args))
}

type ToggleState = { on: boolean }
type ToggleAction =
| { type: 'toggle' }
| { type: 'reset'; initialState: ToggleState }

function toggleReducer(state: ToggleState, action: ToggleAction) {
switch (action.type) {
case 'toggle': {
return { on: !state.on }
}
case 'reset': {
return action.initialState
}
}
}

export function useToggle({ initialOn = false } = {}) {
// 🐨 wrap this in a useRef
const initialState = { on: initialOn }
// 🐨 pass the ref-ed initial state into useReducer
const [state, dispatch] = useReducer(toggleReducer, initialState)
const { on } = state

const toggle = () => dispatch({ type: 'toggle' })
// 🐨 make sure the ref-ed initial state gets passed here
const reset = () => dispatch({ type: 'reset', initialState })

function getTogglerProps<Props>({
onClick,
...props
}: {
onClick?: React.ComponentProps<'button'>['onClick']
} & Props) {
return {
'aria-checked': on,
onClick: callAll(onClick, toggle),
...props,
}
}

return {
on,
toggle,
reset,
getTogglerProps,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Stability

👨‍💼 Great. Now we've got some stability with our state initializer. Well done!
18 changes: 18 additions & 0 deletions exercises/06.state-initializers/02.solution.stability/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useState } from 'react'
import { Switch } from '#shared/switch.tsx'
import { useToggle } from './toggle.tsx'

export function App() {
const [initialOn, setInitialOn] = useState(true)
const { on, getTogglerProps, reset } = useToggle({ initialOn })
return (
<div>
<button onClick={() => setInitialOn(o => !o)}>
initialOn is: {initialOn ? 'true' : 'false'}
</button>
<Switch {...getTogglerProps({ on })} />
<hr />
<button onClick={reset}>Reset</button>
</div>
)
}
52 changes: 52 additions & 0 deletions exercises/06.state-initializers/02.solution.stability/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useReducer, useRef } from 'react'

function callAll<Args extends Array<unknown>>(
...fns: Array<((...args: Args) => unknown) | undefined>
) {
return (...args: Args) => fns.forEach(fn => fn?.(...args))
}

type ToggleState = { on: boolean }
type ToggleAction =
| { type: 'toggle' }
| { type: 'reset'; initialState: ToggleState }

function toggleReducer(state: ToggleState, action: ToggleAction) {
switch (action.type) {
case 'toggle': {
return { on: !state.on }
}
case 'reset': {
return action.initialState
}
}
}

export function useToggle({ initialOn = false } = {}) {
const initialState = useRef({ on: initialOn }).current
const [state, dispatch] = useReducer(toggleReducer, initialState)
const { on } = state

const toggle = () => dispatch({ type: 'toggle' })
const reset = () => dispatch({ type: 'reset', initialState })

function getTogglerProps<Props>({
onClick,
...props
}: {
onClick?: React.ComponentProps<'button'>['onClick']
} & Props) {
return {
'aria-checked': on,
onClick: callAll(onClick, toggle),
...props,
}
}

return {
on,
toggle,
reset,
getTogglerProps,
}
}
9 changes: 9 additions & 0 deletions exercises/06.state-initializers/FINISHED.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# State Initializer

👨‍💼 Great job! You now know what to do to properly handle the initialization (and
reset) of your state.

🦉 Keep in mind that the `key` prop can also be used as a way to reset the state
of a component, and it works pretty well. However it does require that
everything is unmounted and remounted, which may not be what you want depending
on the situation.
57 changes: 57 additions & 0 deletions exercises/06.state-initializers/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# State Initializer

<callout-success>
**One liner:** The state initializer pattern is a way to initialize (and
reset) the state of a component in a predictable way.
</callout-success>

This one is simple in concept:

```tsx
function useCounter() {
const [count, setCount] = useState(0)
const increment = () => setCount(c => c + 1)
return { count, increment }
}
```

If I wanted to initialize the state of the count to a different value, I could
do so by passing an argument to the `useCounter` function:

```tsx
function useCounter({ initialCount = 0 } = {}) {
const [count, setCount] = useState(initialCount)
const increment = () => setCount(c => c + 1)
return { count, increment }
}
```

And often when you have a state initializer, you also have a state resetter:

```tsx
function useCounter({ initialCount = 0 } = {}) {
const [count, setCount] = useState(initialCount)
const increment = () => setCount(c => c + 1)
const reset = () => setCount(initialCount)
return { count, increment, reset }
}
```

But there's a catch. If you truly want to reset the component to its _initial_
state, then you need to make certain that any changes to the `initialCount` are
ignored!

You can do this, by using a `ref` which will keep the initial value constant
across renders:

```tsx
function useCounter({ initialCount = 0 } = {}) {
const initialCountRef = useRef(initialCount)
const [count, setCount] = useState(initialCountRef.current)
const increment = () => setCount(c => c + 1)
const reset = () => setCount(initialCountRef.current)
return { count, increment, reset }
}
```

And that's the crux of the state initializer pattern.
1 change: 1 addition & 0 deletions exercises/07.state-reducer/02.problem.default/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import '/switch.styles.css';
Loading

0 comments on commit 7b7eb57

Please sign in to comment.