Skip to main content

Slots

A slot is a place in a component where you can insert any unknown component. It's a well-known abstraction used by frameworks such as Vue.js and Svelte.

Slots aren't present in the React. With React, you can achieve this goal using props or React.Context. In large projects this is not convenient, because it generates "props hell" or smears the logic.

Using React with Effector we can achieve slot goals without the problems described above.

Try it

import {createApi, createStore, createEvent, guard, sample, split} from 'effector'
import {useStoreMap} from 'effector-react'
import React from 'react'

import type {ReactElement, PropsWithChildren} from 'react'

type Component<S> = (props: PropsWithChildren<S>) => ReactElement | null
type Store<S> = {
readonly component: Component<S>
}

export const createSlotFactory = <Id>({
slots,
}: {
readonly slots: Record<string, Id>
}) => {
const api = {
remove: createEvent<{readonly id: Id}>(),
set: createEvent<{readonly id: Id; readonly component: Component<any>}>(),
}

const createSlot = <P>({id}: {readonly id: Id}) => {
const defaultToStore: Store<P> = {
component: () => null,
}
const $slot = createStore<Store<P>>(defaultToStore)
const slotApi = createApi($slot, {
remove: state => ({...state, component: defaultToStore.component}),
set: (state, payload: Component<P>) => ({...state, component: payload}),
})
const isSlotEventCalling = (payload: {readonly id: Id}) => payload.id === id

guard({
clock: api.remove,
filter: isSlotEventCalling,
target: slotApi.remove,
})

sample({
clock: guard({
clock: api.set,
filter: isSlotEventCalling,
}),
fn: ({component}) => component,
target: slotApi.set,
})

const Slot = (props: P = {} as P) => {
const Component = useStoreMap({
store: $slot,
fn: ({ component }) => component,
keys: [],
});

return <Component {...props} />;
}

return {
$slot,
}
}

return {
api,
createSlot,
}
};

const SLOTS = {
FOO: 'foo',
} as const;

const {api, createSlot} = createSlotFactory({slots: SLOTS});

const {Slot: FooSlot} = createSlot({id: SLOTS.FOO});

const ComponentWithSlot = () => (
<>
<h1>Hello, Slots!</h1>
<FooSlot />
</>
);

const update = createEvent<string>('');
const $featureToggle = createStore<string>('').on(update, (_, p) => p);

const MyAwesomeFeature = () => <p>Look at my horse</p>;
const VeryAwesomeFeature = () => <p>My horse is amaizing</p>;

split({
source: $featureToggle,
match: {
awesome: (data) => data === 'awesome',
veryAwesome: (data) => data === 'veryAwesome',
hideAll: (data) => data === 'hideAll',
},
cases: {
awesome: api.set.prepend(() => ({ id: SLOTS.FOO, component: MyAwesomeFeature })),
veryAwesome: api.set.prepend(() => ({ id: SLOTS.FOO, component: VeryAwesomeFeature })),
hideAll: api.remove.prepend(() => ({ id: SLOTS.FOO })),
},
});

// update('awesome'); // render MyAwesomeFeature in slot
// update('veryAwesome'); // render VeryAwesomeFeature in slot
// update('hideAll'); // render nothing in slot