Skip to content

Commit

Permalink
improve slots exercise a lot
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Mar 20, 2024
1 parent 8cd8e05 commit 32b086d
Show file tree
Hide file tree
Showing 14 changed files with 95 additions and 69 deletions.
1 change: 1 addition & 0 deletions exercises/04.slots/01.problem.context/slots.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// 🦺 create a Slots type that's just an object of objects
// 💰 type Slots = Record<string, Record<string, unknown>>
// 🐨 create and export a SlotContext with that type and default it to an empty object

// 🐨 create a useSlotProps hook which:
Expand Down
11 changes: 4 additions & 7 deletions exercises/04.slots/01.problem.context/text-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ export function TextField({
children: React.ReactNode
}) {
const generatedId = useId()
id = id ?? generatedId
id ??= generatedId

// 🐨 use these for the slot you render
// const labelProps = { htmlFor: id }
// const inputProps = { id }
// 🐨 create a slots object that has props for both label and input slots
// 💰 the label should provide an htmlFor prop and the input should provide an id

// 🐨 wrap this in a SlotContext.Provider with the value set to an object
// that has a label and input property with the values of labelProps and
// inputProps respectively.
// 🐨 wrap this in a SlotContext.Provider with the value set to the slots object
return children
}
14 changes: 6 additions & 8 deletions exercises/04.slots/01.solution.context/text-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ export function TextField({
children: React.ReactNode
}) {
const generatedId = useId()
id = id ?? generatedId
id ??= generatedId

const labelProps = { htmlFor: id }
const inputProps = { id }
const slots = {
label: { htmlFor: id },
input: { id },
}

return (
<SlotContext.Provider value={{ label: labelProps, input: inputProps }}>
{children}
</SlotContext.Provider>
)
return <SlotContext.Provider value={slots}>{children}</SlotContext.Provider>
}
14 changes: 6 additions & 8 deletions exercises/04.slots/02.problem.generic/text-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ export function TextField({
children: React.ReactNode
}) {
const generatedId = useId()
id = id ?? generatedId
id ??= generatedId

const labelProps = { htmlFor: id }
const inputProps = { id }
const slots = {
label: { htmlFor: id },
input: { id },
}

return (
<SlotContext.Provider value={{ label: labelProps, input: inputProps }}>
{children}
</SlotContext.Provider>
)
return <SlotContext.Provider value={slots}>{children}</SlotContext.Provider>
}
3 changes: 2 additions & 1 deletion exercises/04.slots/02.problem.generic/toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export function Toggle({ children }: { children: React.ReactNode }) {

const toggle = () => setOn(!on)

// 🐨 create labelProps that sets htmlFor to the id
// 🐨 create a slots object that has props for a slot called
// "label" with an htmlFor prop

// 🐨 wrap this in SlotContext.Provider and pass the labelProps in the label slot
// 🐨 add the id to the value in the ToggleContext.Provider
Expand Down
14 changes: 6 additions & 8 deletions exercises/04.slots/02.solution.generic/text-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ export function TextField({
children: React.ReactNode
}) {
const generatedId = useId()
id = id ?? generatedId
id ??= generatedId

const labelProps = { htmlFor: id }
const inputProps = { id }
const slots = {
label: { htmlFor: id },
input: { id },
}

return (
<SlotContext.Provider value={{ label: labelProps, input: inputProps }}>
{children}
</SlotContext.Provider>
)
return <SlotContext.Provider value={slots}>{children}</SlotContext.Provider>
}
6 changes: 3 additions & 3 deletions exercises/04.slots/02.solution.generic/toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ export function Toggle({
}) {
const [on, setOn] = useState(false)
const generatedId = useId()
id = id ?? generatedId
id ??= generatedId

const toggle = () => setOn(!on)

const labelProps = { htmlFor: id }
const slots = { label: { htmlFor: id } }

return (
<SlotContext.Provider value={{ label: labelProps }}>
<SlotContext.Provider value={slots}>
<ToggleContext.Provider value={{ on, toggle, id }}>
{children}
</ToggleContext.Provider>
Expand Down
5 changes: 2 additions & 3 deletions exercises/04.slots/03.problem.prop/slots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ export function Text(props: React.ComponentProps<'span'>) {
}

// 🐨 add an optional slot to the props type here
export function Switch(
props: Omit<React.ComponentProps<typeof BaseSwitch>, 'on'>,
) {
type SwitchProps = Omit<React.ComponentProps<typeof BaseSwitch>, 'on'>
export function Switch(props: SwitchProps) {
return (
<BaseSwitch
{...(useSlotProps(props, 'switch') as React.ComponentProps<
Expand Down
14 changes: 6 additions & 8 deletions exercises/04.slots/03.problem.prop/text-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ export function TextField({
children: React.ReactNode
}) {
const generatedId = useId()
id = id ?? generatedId
id ??= generatedId

const labelProps = { htmlFor: id }
const inputProps = { id }
const slots = {
label: { htmlFor: id },
input: { id },
}

return (
<SlotContext.Provider value={{ label: labelProps, input: inputProps }}>
{children}
</SlotContext.Provider>
)
return <SlotContext.Provider value={slots}>{children}</SlotContext.Provider>
}
16 changes: 7 additions & 9 deletions exercises/04.slots/03.problem.prop/toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,18 @@ export function Toggle({
}) {
const [on, setOn] = useState(false)
const generatedId = useId()
id = id ?? generatedId
id ??= generatedId

const toggle = () => setOn(!on)

const labelProps = { htmlFor: id }
// 🐨 add props objects for onText, offText, and switch
const slots = {
label: { htmlFor: id },
// 🐨 add slots for onText (hidden prop), offText (hidden prop),
// and switch (id, on, onClick props)
}

return (
<SlotContext.Provider
value={{
label: labelProps,
// 🐨 add slots for onText, offText, and switch
}}
>
<SlotContext.Provider value={slots}>
{/* 🐨 get rid of the ToggleContext here */}
<ToggleContext.Provider value={{ on, toggle, id }}>
{children}
Expand Down
9 changes: 4 additions & 5 deletions exercises/04.slots/03.solution.prop/slots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ export function Text(props: React.ComponentProps<'span'> & { slot?: string }) {
return <span {...props} />
}

export function Switch(
props: Omit<React.ComponentProps<typeof BaseSwitch>, 'on'> & {
slot?: string
},
) {
type SwitchProps = Omit<React.ComponentProps<typeof BaseSwitch>, 'on'> & {
slot?: string
}
export function Switch(props: SwitchProps) {
return (
<BaseSwitch
{...(useSlotProps(props, 'switch') as React.ComponentProps<
Expand Down
14 changes: 6 additions & 8 deletions exercises/04.slots/03.solution.prop/text-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ export function TextField({
children: React.ReactNode
}) {
const generatedId = useId()
id = id ?? generatedId
id ??= generatedId

const labelProps = { htmlFor: id }
const inputProps = { id }
const slots = {
label: { htmlFor: id },
input: { id },
}

return (
<SlotContext.Provider value={{ label: labelProps, input: inputProps }}>
{children}
</SlotContext.Provider>
)
return <SlotContext.Provider value={slots}>{children}</SlotContext.Provider>
}
2 changes: 1 addition & 1 deletion exercises/04.slots/03.solution.prop/toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function Toggle({
}) {
const [on, setOn] = useState(false)
const generatedId = useId()
id = id ?? generatedId
id ??= generatedId

const toggle = () => setOn(!on)

Expand Down
41 changes: 41 additions & 0 deletions exercises/04.slots/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
particular role in the overall collection of components.
</callout-success>

This pattern is particularly useful for situations where you're building a UI
library with a lot of components that need to work together. It's a way to
provide a flexible API for your components that allows them to be used in a
variety of contexts.

If you're building a component library, you have to deal with two competing
interests:

Expand Down Expand Up @@ -71,3 +76,39 @@ This can be used to apply appropriate `aria-` attributes as well as `id`s and
event handlers. You might think about it as a way to implement compound
components in a way that doesn't require an individual component for every
single use case.

## Implementation

Folks tend to struggle with this one a bit more than the rest, but it's simpler
than it seems.

The basic concept is your root component creates collections of props like so:

```tsx
function NumberField({ children }: { children: React.ReactNode }) {
// setup state/events/etc

const slots = {
label: { htmlFor: inputId },
decrement: { onClick: decrement },
increment: { onClick: increment },
description: { id: descriptionId },
input: { id: inputId, 'aria-describedby': descriptionId },
}
return <SlotContext.Provider value={slots}>{children}</SlotContext.Provider>
}
```

Then the consuming components use the `use(SlotContext)` to get access to the
`slots` object and pluck off the props they need to do their job:

```tsx
function Input(props) {
props = useSlotProps(props, 'input')

return <input {...props}>
}
```

The `useSlotProps` hook is responsible for taking the props that have been
specified and combining it with those from the `SlotContext` for the named slot.

0 comments on commit 32b086d

Please sign in to comment.