Weaverlet: OOP for Dash, reusable components, scalable apps (no DuplicateIdError, no global callbacks, no string IDs and more)

Hi all,

I’d like to share Weaverlet, an open-source framework we built at the Observatorio Metropolitano CentroGeo (Mexico) for structuring Plotly Dash applications as a hierarchy of reusable Python classes. It’s been quietly powering our internal dashboards since 2024, and we just shipped 0.3, which modernizes the dependency stack (Dash 4, Python 3.12+) and rounds out the framework story.

Weaverlet vs. plain Dash

Plain Dash gives you primitives, not encapsulation. If you’ve ever wanted to drop the same widget into two pages of the same app, you’ve probably bumped into:

  • Callbacks register globally against a module-level app.
  • IDs are strings you manage by hand, and reusing one anywhere gives you DuplicateIdError.
  • Reusing a widget across apps means copy-paste and rename.

Weaverlet packages layout + callbacks + IDs into a class:

from weaverlet import WeaverletComponent, WeaverletApp, Identifier
from dash_extensions.enrich import Input, Output
from dash import html, dcc

class EchoComponent(WeaverletComponent):
    text_input_id = Identifier()
    echo_div_id = Identifier()

    def get_layout(self):
        return html.Div([
            dcc.Input(id=self.text_input_id, type="text"),
            html.Div(id=self.echo_div_id),
        ])

    def register_callbacks(self, app):
        @app.callback(Output(self.echo_div_id, "children"),
                      Input(self.text_input_id, "value"))
        def echo(value):
            return value or ""

WeaverletApp(root_component=EchoComponent()).app.run()

Identifier() is a descriptor that returns a globally-unique string per instance. You can put EchoComponent() on the same page twice and it just works.

What you get on top of Dash

  • Class-based encapsulation. Each WeaverletComponent owns its own layout, callbacks, and IDs. Compose them into a tree and hand the root to WeaverletApp.
  • Auto-unique IDs via the Identifier() descriptor. No more string juggling, no more DuplicateIdError.
  • Typed inter-component events via SignalComponent, instead of hand-rolled dcc.Store + MultiplexerTransform patterns for every cross-widget event.
  • Routing, programmatic and filesystem-free, via SimpleRouterComponent.
  • Session-based auth gating via AuthRouterComponent backed by Flask sessions.
  • Shared context propagated to every component in the DAG, so configuration and services flow down cleanly without prop drilling.

Under the hood, WeaverletApp.app is a dash_extensions.enrich.DashProxy, so everything in the Dash ecosystem (DBC, DMC, dash-leaflet, dash-ag-grid, etc.) keeps working. You can also use AIO widgets as leaves inside a WeaverletComponent.get_layout() without friction.

Weaverlet vs. Dash Pages and AIO

These solve adjacent problems:

  • Dash Pages is a filesystem routing convention.
  • AIO is a widget pattern (one reusable widget, pattern-matching IDs).
  • Weaverlet is a whole-app framework: a tree of classes with lifecycle, routing, signals, and shared context.

There’s a side-by-side comparison page in the docs (under “Weaverlet vs Dash Pages/AIO”) if you want the long form.

Links

Requires Python 3.12+ and Dash 4.1+. MIT-licensed.

I’d love feedback, especially from anyone whose Dash app has outgrown a single file and is starting to feel the limits of global callbacks and string IDs. Also happy to discuss design choices, or how it interacts with Dash Pages if you’re considering a migration path.

Thanks for reading!