Skip to content

Commit

Permalink
🚑 Fix case when Slot appears later
Browse files Browse the repository at this point in the history
  • Loading branch information
exah committed Dec 26, 2020
1 parent b9e3d0f commit bdadda3
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 24 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A super lightweight modern alternative to [`react-slot-fill`](https://github.com
- [x] Tested with [`testing-library`](https://testing-library.com)
- [x] Written in TypeScript
- [x] Zero dependencies
- [x] Only ~396 B
- [x] Only ~383 B

## 📦 Install

Expand Down
40 changes: 37 additions & 3 deletions src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ const List = ({ children }: { children: React.ReactNode }) => (
</ul>
)

function Hidden({ children }: { children: React.ReactNode }) {
const [visible, setVisible] = useState<boolean>(false)
return (
<SlotsProvider>
{children}
<ul>
<li>
<Slot name="foo" />
</li>
<li>{visible && <Slot name="foo" />}</li>
</ul>
<button onClick={() => setVisible(true)}>Show</button>
</SlotsProvider>
)
}

const Parent = ({ children }: { children: React.ReactNode }) => (
<SlotsProvider>
<ul>
Expand Down Expand Up @@ -141,7 +157,7 @@ test('render nested node in multiple places', () => {
defaultTest()
})

test('hydrate without errors on server render', async () => {
test('hydrate without errors on server render', () => {
isServer = true

const root = document.createElement('div')
Expand All @@ -166,7 +182,7 @@ test('hydrate without errors on server render', async () => {
unmountComponentAtNode(root)
})

test('create isolated context with specified types', async () => {
test('create isolated context with specified types', () => {
render(
<NS.Provider>
<List>
Expand All @@ -183,7 +199,7 @@ test('create isolated context with specified types', async () => {
expect(items[2]).toHaveTextContent('Baz Fallback')
})

test('isolated context without provider', async () => {
test('isolated context without provider', () => {
render(
<List>
<NS.Fill name="foo">Foo</NS.Fill>
Expand All @@ -196,3 +212,21 @@ test('isolated context without provider', async () => {
expect(items[0]).toHaveTextContent('Foo')
expect(items[1]).toHaveTextContent('Bar')
})

test('use saved node when slot rendered later', async () => {
render(
<Hidden>
<Fill name="foo">Foo</Fill>
</Hidden>
)

const items = screen.getAllByRole('listitem')

expect(items[0]).toHaveTextContent('Foo')
expect(items[1]).toHaveTextContent('')

screen.getByRole('button', { name: 'Show' }).click()

expect(items[0]).toHaveTextContent('Foo')
expect(items[1]).toHaveTextContent('Foo')
})
38 changes: 18 additions & 20 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
createElement as h,
useContext,
useLayoutEffect,
useRef,
useState,
Fragment,
} from 'react'
Expand All @@ -14,8 +13,9 @@ type Unsubscribe = () => void
type Callback = (node: React.ReactNode) => void

interface Emitter<Names extends PropertyKey> {
emit(event: Names, node: React.ReactNode): void
on(event: Names, cb: Callback): Unsubscribe
get(event: Names): React.ReactNode
emit(event: Names, node: React.ReactNode): Unsubscribe
}

export interface ProviderProps {
Expand Down Expand Up @@ -50,30 +50,22 @@ export default function createSlots<Names extends PropertyKey>() {
}

function Slot(props: SlotProps<Names>) {
const [state, setState] = useState<React.ReactNode>()
const emitter = useContext(SlotsContext)
const ref = useRef<Unsubscribe | null>()

if (ref.current === undefined) {
ref.current = emitter.on(props.name, setState)
}
const [node, setNode] = useState<React.ReactNode>()

useUniversalEffect(() => {
if (ref.current) {
ref.current()
ref.current = null
}
return emitter.on(props.name, setState)
setNode(emitter.get(props.name))
return emitter.on(props.name, setNode)
}, [emitter, props.name])

return <Fragment>{state === undefined ? props.children : state}</Fragment>
return <Fragment>{node === undefined ? props.children : node}</Fragment>
}

function Fill(props: FillProps<Names>) {
const emitter = useContext(SlotsContext)

useUniversalEffect(() => {
emitter.emit(props.name, props.children)
return emitter.emit(props.name, props.children)
}, [emitter, props.name, props.children])

return null
Expand All @@ -87,12 +79,21 @@ export default function createSlots<Names extends PropertyKey>() {
}

function createEmitter<Names extends PropertyKey>(): Emitter<Names> {
const events: { [K in Names]?: Callback[] } = {}
const cache: Partial<Record<Names, React.ReactNode>> = {}
const events: Partial<Record<Names, Callback[]>> = {}

return {
emit(event, node) {
const source = events[event]
if (source) source.forEach((cb) => cb(node))

cache[event] = node
return () => {
cache[event] = undefined
}
},
get(event) {
return cache[event]
},
on(event, cb) {
const source = (events[event] = events[event] || [])!
Expand All @@ -108,10 +109,7 @@ function useUniversalEffect(
effect: React.EffectCallback,
deps: React.DependencyList
) {
if (isServer()) {
const cleanup = effect()
if (cleanup) cleanup()
} else {
if (!isServer()) {
useLayoutEffect(effect, deps)
}
}

0 comments on commit bdadda3

Please sign in to comment.