A set of primitives for declarative event composition and state derivation for solidjs. You can think of it as a much simpler version of Rxjs that integrates well with Solidjs.
Here is an implementation of the Strello demo that uses solid-events.
npm install solid-eventsor
pnpm install solid-eventsor
bun install solid-eventsReturns an event handler and an event emitter. The handler can execute a callback when the event is emitted.
const [onEvent, emitEvent] = createEvent()
onEvent(payload => console.log(`Event emitted:`, payload))
...
emitEvent(`Hello World!`)
// logs "Event emitted: Hello World!"The handler can return a new handler with the value returned from the callback. This allows chaining transformations.
const [onIncrement, emitIncrement] = createEvent()
const onMessage = onIncrement((delta) => `Increment by ${delta}`)
onMessage(message => console.log(`Message emitted:`, message))
...
emitIncrement(2)
// logs "Message emitted: Increment by 2"Handlers that are called inside a component are automatically cleaned up with the component, so no manual bookeeping is necesarry.
function Counter() {
const [onIncrement, emitIncrement] = createEvent()
const onMessage = onIncrement((delta) => `Increment by ${delta}`)
onMessage(message => console.log(`Message emitted:`, message))
return <div>....</div>
}Calling onIncrement and onMessage registers a stateful subscription. The lifecycle of these subscriptions are tied to their owner components. This ensures there's no memory leaks.
Event propogation can be stopped at any point using halt()
const [onIncrement, emitIncrement] = createEvent()
const onValidIncrement = onIncrement(delta => delta < 1 ? halt() : delta)
const onMessage = onValidIncrement((delta) => `Increment by ${delta}`)
onMessage(message => console.log(`Message emitted:`, message))
...
emitIncrement(2)
// logs "Message emitted: Increment by 2"
...
emitIncrement(0)
// Doesn't log anythinghalt() returns a never, so typescript correctly infers the return type of the handler.
If you return a promise from an event callback, the resulting event will wait to emit until the promise resolves. In other words, promises are automatically flattened by events.
async function createBoard(boardData) {
"use server"
const boardId = await db.boards.create(boardData)
return boardId
}
const [onCreateBoard, emitCreateBoard] = createEvent()
const onBoardCreated = onCreateBoard(boardData => createBoard(boardData))
onBoardCreated(boardId => navigate(`/board/${boardId}`))Events can be used to derive state using Subjects. A Subject is a signal that can be derived from event handlers.
const [onIncrement, emitIncrement] = createEvent()
const [onReset, emitReset] = createEvent()
const onMessage = onIncrement((delta) => `Increment by ${delta}`)
onMessage(message => console.log(`Message emitted:`, message))
const count = createSubject(
0,
onIncrement(delta => currentCount => currentCount + delta),
onReset(() => 0)
)
createEffect(() => console.log(`count`, count()))
...
emitIncrement(2)
// logs "Message emitted: Increment by 2"
// logs "count 2"
emitReset()
// logs "count 0"To update the value of a subject, event handlers can return a value (like onReset), or a function that transforms the current value (like onIncrement).
createSubject can also accept a signal as the first input instead of a static value. The subject's value resets whenever the source signal updates.
function Counter(props) {
const [onIncrement, emitIncrement] = createEvent()
const [onReset, emitReset] = createEvent()
const count = createSubject(
() => props.count,
onIncrement(delta => currentCount => currentCount + delta),
onReset(() => 0)
)
return <div>...</div>
}createSubject has some compound variations to complete use cases.
This subject accepts a reactive async function as the first argument similar to createAsync, and resets whenever the function reruns.
const getBoards = cache(async () => {
"use server";
// fetch from database
}, "get-boards");
export default function HomePage() {
const [onDeleteBoard, emitDeleteBoard] = createEvent<number>();
const boards = createAsyncSubject(
() => getBoards(),
onDeleteBoard(
(boardId) => (boards) => boards.filter((board) => board.id !== boardId)
)
);
...
}This subject is a store instead of a regular signal. Event handlers can mutate the current state of the board directly. Uses produce under the hood.
const boardStore = createSubjectStore(
() => boardData(),
onCreateNote((createdNote) => (board) => {
const index = board.notes.findIndex((n) => n.id === note.id);
if (index === -1) board.notes.push(note);
}),
onDeleteNote(([id]) => (board) => {
const index = board.notes.findIndex((n) => n.id === id);
if (index !== -1) board.notes.splice(index, 1);
})
...
)Similar to createSubject, the first argument can be a signal that resets the value of the store. When this signal updates, the store is updated using reconcile.
A topic combines multiple events into one. This is simply a more convenient way to merge events than manually iterating through them.
const [onIncrement, emitIncrement] = createEvent()
const [onDecrement, emitDecrement] = createEvent()
const onMessage = createTopic(
onIncrement(() => `Increment by ${delta}`),
onDecrement(() => `Decrement by ${delta}`)
);
onMessage(message => console.log(`Message emitted:`, message))
...
emitIncrement(2)
// logs "Message emitted: Increment by 2"
emitDecrement(1)
// logs "Message emitted: Decrement by 1"A partition splits an event based on a conditional. This is simply a more convenient way to conditionally split events than using halt().
const [onIncrement, emitIncrement] = createEvent()
const [onValidIncrement, onInvalidIncrement] = createPartition(
onIncrement,
delta => delta > 0
)
onValidIncrement(delta => console.log(`Valid increment by ${delta}`))
onInvalidIncrement(delta => console.log(`Please use a number greater than 0`))
...
emitIncrement(2)
// logs "Valid increment by 2"
emitIncrement(0)
// logs "Please use a number greater than 0"