Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Solid Primitives events

@solid-primitives/events

turborepo size version stage

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.

Contents

Installatiom

npm install solid-events

or

pnpm install solid-events

or

bun install solid-events

createEvent

Returns 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!"

Tranformation

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"

Disposal

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.

Halting

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 anything

halt() returns a never, so typescript correctly infers the return type of the handler.

Async Events

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}`))

createSubject

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.

createAsyncSubject

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)
    )
  );

  ...
}

createSubjectStore

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.

createTopic

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"

createPartition

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"

Use Cases