{"id":19856881,"url":"https://github.com/malte-wessel/bassdrum","last_synced_at":"2025-06-20T17:08:52.533Z","repository":{"id":36496600,"uuid":"221805277","full_name":"malte-wessel/bassdrum","owner":"malte-wessel","description":"reactive, type safe components with preact and rxjs.","archived":false,"fork":false,"pushed_at":"2023-01-05T03:16:00.000Z","size":3687,"stargazers_count":45,"open_issues_count":82,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-14T22:04:30.947Z","etag":null,"topics":["dom","preact","reactive","rxjs"],"latest_commit_sha":null,"homepage":"https://bassdrum.js.org","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/malte-wessel.png","metadata":{"files":{"readme":"README.MD","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-11-14T23:31:55.000Z","updated_at":"2025-02-20T20:37:58.000Z","dependencies_parsed_at":"2023-01-17T02:15:27.993Z","dependency_job_id":null,"html_url":"https://github.com/malte-wessel/bassdrum","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/malte-wessel/bassdrum","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/malte-wessel%2Fbassdrum","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/malte-wessel%2Fbassdrum/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/malte-wessel%2Fbassdrum/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/malte-wessel%2Fbassdrum/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/malte-wessel","download_url":"https://codeload.github.com/malte-wessel/bassdrum/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/malte-wessel%2Fbassdrum/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259945063,"owners_count":22935724,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dom","preact","reactive","rxjs"],"created_at":"2024-11-12T14:16:51.645Z","updated_at":"2025-06-20T17:08:47.513Z","avatar_url":"https://github.com/malte-wessel.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🥁 bassdrum\n\nbassdrum let's you create **reactive, type safe components** with `preact` and `rxjs`.\n\n-   Handle data transformation, events and side effects with `rxjs`\n-   Let `preact` render your JSX templates\n-   bassdrum is tiny 2KB (1KB gzip)\n-   bassdrum components are _fully compatible_ with `preact`\n\n[![Actions Status](https://github.com/malte-wessel/bassdrum/workflows/Node%20CI/badge.svg)](https://github.com/malte-wessel/bassdrum/actions)\n\n## The gist\n\nWith bassdrum you create components by transforming `props` to `state` with `rxjs`. The transformed `state` will be passed to your JSX template and gets rendered with `preact`.\n\n```tsx\ninterface Props {\n    items: Item[];\n}\n\ninterface State {\n    items: Item[];\n    count: number;\n    itemsPerPage: number;\n}\n\nconst AppFn: ComponentFunction\u003cProps, State\u003e = ({ props }) =\u003e {\n    const items = props.pipe(pluck('items'));\n    const count = items.pipe(map(items =\u003e items.length));\n    return combine(props, { count, itemsPerPage: 20 });\n};\n\nconst AppTemplate: ComponentTemplate\u003cState\u003e = ({\n    items,\n    count,\n    itemsPerPage,\n}) =\u003e (\n    \u003csection\u003e\n        \u003cp\u003eWe got {count} items for you!\u003c/p\u003e\n        \u003cItemList items={items} itemsPerPage={itemsPerPage} /\u003e\n    \u003c/section\u003e\n);\n\nconst App = createComponent(AppFn, AppTemplate);\n```\n\n## Installation\n\n```bash\n# bassdrum has preact and rxjs as peer dependencies\nyarn add bassdrum preact rxjs\n```\n\nCheck out the `/examples` directory on how to set up typescript properly\n\n## Guide\n\nbassdrum has a minimal api surface. It consists of four functions: `createComponent`, `combine`, `createHandler` \u0026 `createRef`.\n\nTo create a new bassdrum component use `createComponent`. It expects a `ComponentFunction\u003cProps, State\u003e` and a `ComponentTemplate\u003cState\u003e`.\n\n```tsx\nimport { h, render } from 'preact';\nimport {\n    createComponent,\n    ComponentFunction,\n    ComponentTemplate,\n} from 'bassdrum';\n\ninterface Props {\n    name: string;\n}\n\ninterface State {\n    name: string;\n}\n\n// The component function is the heart of your component. The function maps the\n// received `props` stream to a `state` stream.\nconst ComponentFn: ComponentFunction\u003cProps, State\u003e = ({ props }) =\u003e props;\n\n// Use the state returned from your component function stream in your template\nconst Template: ComponentTemplate\u003cState\u003e = state =\u003e (\n    \u003cdiv\u003eHello, {state.name}\u003c/div\u003e\n);\n\n// Create the component\nconst Component = createComponent(ComponentFn, Template);\n\n// Render the component as you would do with preact\nconst root = document.getElementById('root');\nif (root) {\n    render(\u003cComponent name=\"Baby yoda\" /\u003e, root);\n}\n```\n\n### The component function\n\nLet's have a closer look at the component function. This function is called once your component is about to be created. It receives a `props` stream that emits whenever the props change (`componentWillReceiveProps`), an `updates` stream that emits whenever all changes have been flushed to the DOM (`componentDidMount` \u0026 `componentDidUpdate`) and a `subscribe` function that you can use to subscribe to streams. The component takes care of unsubscribing from those streams when the component is unmounted.\n\n#### Transforming data\n\nTo transform the data that you receive from the `props` stream, you can use the transformation operators from `rxjs`. In the following example we use the `pluck` operator to get a stream of `items` from the `props` and the `map` operator to map the `items` to its length.\n\n```tsx\ninterface Props {\n    items: Item[];\n}\n\ninterface State {\n    items: Item[];\n    count: number;\n    itemsPerPage: number;\n}\n\nconst ComponentFn: ComponentFunction\u003cProps, State\u003e = ({ props }) =\u003e {\n    const items = props.pipe(pluck('items'));\n    const count = items.pipe(map(items =\u003e items.length));\n    return combine(props, { count, itemsPerPage: 20 });\n};\n```\n\nTo combine the `props` stream and your transformed streams use the bassdrum `combine` method. This method can merge data from streams and plain objects that hold primitive values or streams. In the example above `combine` will emit an object that looks like this:\n\n```js\n{\n    items: [{...}],\n    count: 43,\n    itemsPerpage: 20\n}\n```\n\n`combine` emits whenever one of its streams emits. We do a `shallowEqual` compare and only rerender the component if at least one of the values has changed. The component automatically subscribes and unsubscibes from the stream that you return from the component function.\n\nIf you derive complex data make sure to use the `rxjs` operator `distinctUntilChanged` to avoid unnecessary rerenders, like in the following example:\n\n```tsx\nconst ComponentFn: ComponentFunction\u003cProps, State\u003e = ({ props }) =\u003e {\n    const items = props.pipe(\n        // `items` will emit whenever `props` emit,\n        // even though `items` haven't changed\n        pluck('items'),\n        // This operator will do a strict equal check\n        // and only emits if the items have really changed\n        distinctUntilChanged(),\n    );\n    const groupedByRating = items.pipe(map(groupBy(item =\u003e item.rating)));\n    return combine(props, { groupedByRating });\n};\n```\n\n#### Lifecycle events\n\nTo deal with lifecycle events in bassdrum all you need is the `updates` stream. Look at the following example\n\n```tsx\nconst log = value =\u003e tap(() =\u003e console.log(value));\n\nconst ComponentFn: ComponentFunction\u003cProps, State\u003e = ({\n    props,\n    updates,\n    subscribe,\n}) =\u003e {\n    // the first value emitted by `updates` signals the did mount event\n    const mounted = updates.pipe(first());\n\n    // skip the mounted event if you only want updates\n    const updated = updates.pipe(skip(1));\n\n    // when the component will unmount, the `updates` stream completes\n    const unmounted = updates.pipe(last());\n\n    subscribe(mounted.pipe(log('component did mount')));\n    subscribe(updated.pipe(log('component did update')));\n    subscribe(unmounted.pipe(log('component will unmount')));\n\n    return props;\n};\n```\n\nAlright, let's have a look what's happening in this example. We use the `updates` stream to get notified about certain lifecycle events. The `updates` stream emits the current `props` whenever `componenDidMount` or `componentDidUpdate` is called. When the component is about to get unmounted, the `updates` stream completes. We can use the `rxjs` filtering operators to only react to certain events.\n\nThe first time `updates` emits the component is mounted. By using the `first` operator we will get a stream that only emits once the component is mounted and attached to the DOM. If you want a stream that only emits when the component has been updated, skip the first emit by using the `skip` operator. To deal with cleaning up use the `last` operator that returns a stream that only emits once the `updates` stream completes.\n\nLet's say you want to notify a parent component about updates of your component. The `updates` stream emits the current value of `props`. Use the `tap` operator to hook into update events and use the data and callbacks passed to your component right there:\n\n```tsx\nconst ComponentFn: ComponentFunction\u003cProps, State\u003e = ({\n    props,\n    updates,\n    subscribe,\n}) =\u003e {\n    const updated = updates.pipe(\n        tap(props =\u003e {\n            const { handleUpdated, id } = props;\n            handleUpdated(id);\n        }),\n    );\n    subscribe(updated);\n    return props;\n};\n```\n\n#### Subscriptions\n\nIn the examples above we were using the `subscribe` function that is passed to your component function. Why is that? The streams that we are creating and not returning or passing to `combine` basically do nothing until you subscribe to them. In `rxjs` you would subscribe to a stream by calling it's `subscribe` method. This method returns an `unsubscribe` function that you need to call once you don't need your stream anymore. bassdrum provides you this util function that takes care of managing those subscriptions, so you don't need to deal with it. You can use this function e.g. when you are dealing with side effects that do not result in data that you use in your template.\n\n### Working with handlers\n\nHandlers in bassdrum work just like in `preact`. You create a function that you pass to the DOM or a child component. The way how you create them is different though. Since in bassdrum everything is a `rxjs` stream you want a stream of events when your handler is called. For this purpose we use `createHandler`.\n\nThe API of `createHandler` is inspired by react hooks. `createHandler` returns a handler function and an `rxjs` stream that emits whenever the handler is called. This way you can easily transform your event stream to data like in the following example.\n\n```tsx\nimport {\n    combine,\n    createHandler,\n    ComponentFunction,\n    ComponentTemplate,\n    Handler,\n    MouseEvent,\n} from 'bassdrum';\n\ninterface Props {}\n\ninterface State {\n    handleClick: Handler\u003cMouseEvent\u003e;\n    counter: number;\n}\n\nconst ComponentFn: ComponentFunction\u003cProps, State\u003e = ({ updates }) =\u003e {\n    const [handleClick, clicks] = createHandler\u003cMouseEvent\u003e();\n\n    const counter = clicks.pipe(\n        scan(value =\u003e value + 1, 0),\n        startWith(0),\n    );\n\n    return combine({\n        handleClick,\n        counter,\n    });\n};\n\nconst Template: ComponentTemplate\u003cState\u003e = ({ handleClick, counter }) =\u003e (\n    \u003csection\u003e\n        \u003cp\u003eYour pressed this button {counter} times.\u003c/p\u003e\n        \u003cbutton onClick={handleClick}\u003eIncrease counter\u003c/button\u003e\n    \u003c/section\u003e\n);\n```\n\nWhen the user presses the button the `clicks` stream emits a click `event`. We use this information to increase the counter. The `counter` stream is derived from those clicks. It starts with a `0` and increases everytime it receives an event.\n\nWhy do we need to use the `startsWith` operator here? bassdrum expects the stream you return from the component function to immediately emit once your component is created. `combine` won't emit until every input stream has emitted. At the time the component is created `clicks` has never emitted. Thus we need `startWith` to immediately emit on component creation time.\n\n### Working with refs\n\nSimilar to `createHandler`, `createRef` returns a `Ref` function that you pass to your template and a stream that emits once the respective element is created.\n\n```tsx\nconst ComponentFn: ComponentFunction\u003cProps, State\u003e = ({ props, updates }) =\u003e {\n    const [ref, el] = createRef\u003cHTMLDivElement\u003e();\n\n    subscribe(\n        el.pipe(tap(el =\u003e console.log('Look ma, this is an element', el))),\n    );\n\n    return combine(props, {\n        ref,\n    });\n};\n```\n\nWhen using `refs` there are a few things to keep in mind. The `el` stream returned from `createRef` emits `null` on component creation, since at that point we do not have an element. Once your element is created by `preact` it will emit the element instance. In the example above you would see the following logs:\n\n```\nLook ma, this is an element null\nLook ma, this is an element [Element]\n```\n\nWhen the component gets unmounted you will see another `Look ma, this is an element null` log message. This is because the element is removed from the DOM and preact calls the handler with `null`. Now it's up to you to do some clean up logic.\n\nAlso keep in mind that `el` emits at the time the DOM element is created. This happens even before the component is considered to be mounted. That's why most of the time you want to combine it with the `updates` stream like in the following example.\n\n```tsx\nconst ComponentFn: ComponentFunction\u003cProps, State\u003e = ({ props, updates }) =\u003e {\n    const [ref, el] = createRef\u003cHTMLDivElement\u003e();\n\n    const width = combineLatest(el, updates).pipe(\n        map(([el]) =\u003e (el ? el.offsetWidth : 0)),\n        startWith(0),\n    );\n\n    return combine(props, {\n        ref,\n        width,\n    });\n};\n```\n\n`combineLatest(el, updates)` will emit once both `el` and `updates` have emitted. At that time your component is mounted and you can interact with your `el`, e.g. gathering data like the `offsetWith`. Keep in mind to use `startWith(0)` for your inital emit.\n\n## Advanced example\n\n```tsx\nimport { h, Ref, render } from 'preact';\nimport { combineLatest } from 'rxjs';\nimport { map, startWith, scan, distinctUntilChanged } from 'rxjs/operators';\nimport {\n    createComponent,\n    ComponentFunction,\n    ComponentTemplate,\n    combine,\n    createRef,\n    createHandler,\n    Handler,\n    MouseEvent,\n} from 'bassdrum';\n\ninterface Props {\n    name: string;\n}\n\ninterface State {\n    name: string;\n    ref: Ref\u003cHTMLDivElement\u003e;\n    width: number;\n    handleClick: Handler\u003cMouseEvent\u003cHTMLButtonElement\u003e\u003e;\n    toggle: boolean;\n}\n\nconst ComponentFn: ComponentFunction\u003cProps, State\u003e = ({ props, updates }) =\u003e {\n    const [ref, el] = createRef\u003cHTMLDivElement\u003e();\n    const [handleClick, clicks] = createHandler\u003c\n        MouseEvent\u003cHTMLButtonElement\u003e\n    \u003e();\n\n    const toggle = clicks.pipe(\n        scan(value =\u003e !value, true),\n        startWith(true),\n    );\n\n    const width = combineLatest(el, updates).pipe(\n        map(([el]) =\u003e (el ? el.offsetWidth : 0)),\n        startWith(0),\n        distinctUntilChanged(),\n    );\n\n    return combine(props, {\n        width,\n        ref,\n        handleClick,\n        toggle,\n    });\n};\n\nconst Template: ComponentTemplate\u003cState\u003e = ({\n    name,\n    width,\n    ref,\n    toggle,\n    handleClick,\n}) =\u003e (\n    \u003cdiv ref={ref} style={{ width: toggle ? '100%' : '50%' }}\u003e\n        \u003cp\u003eHello {name}, welcome to bassdrum!\u003c/p\u003e\n        \u003cp\u003eThis element is {width}px wide.\u003c/p\u003e\n        \u003cbutton onClick={handleClick}\u003eToggle width\u003c/button\u003e\n    \u003c/div\u003e\n);\n\nconst Component = createComponent(ComponentFn, Template);\n\nconst root = document.getElementById('root');\nif (root) {\n    render(\u003cComponent name=\"Malte\" /\u003e, root);\n}\n```\n\n## Prior art\n\nThe idea of transforming `props` to `state` with `rxjs` originates from\n[melody-streams](https://github.com/trivago/melody/tree/master/packages/melody-streams)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmalte-wessel%2Fbassdrum","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmalte-wessel%2Fbassdrum","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmalte-wessel%2Fbassdrum/lists"}