Skip to content

feat: add support for reactive controllers #3162

@WickyNilliams

Description

@WickyNilliams

Prerequisites

Describe the Feature Request

Lit has added support for what it calls "Reactive controllers" in its latest major release. These are plain JS classes which conform to a known interface, and are able to hook into a component's lifecycle.

They are generic enough that they can be used/adapted to most/all frameworks, as they are not dependent on Lit at all (aside from interface definition, but that could be externalised). Therefore stencil could add support for these by implementing ReactiveControllerHost interface.

This could be considered a community standard, and would allow for sharing of functionality between stencil and lit (and any other frameworks that opt-into the standard), perhaps creating an ecosystem for pieces of reusable behaviour

Controllers are a mechanism for extracting both behaviour and state from a component in a reusable way.

Relevant links:

Describe the Use Case

There are currently no ways "official" ways to share functionality/logic/behaviour between components. At best you can do some manipulation of the component instance in a helper function, but this always feels fragile.

Reactive controllers offer a way to express a "has-a" relationship, and are composable. You can extract common behaviour and state for reuse between components and even frameworks.

Describe Preferred Solution

Stencil's base component class should implement ReactiveControllerHost. This could be as simple as:

class StencilBaseClass implements ReactiveControllerHost {
  controllers = new Set<ReactiveController>();

  addController(controller: ReactiveController) {
    this.controllers.add(controller);
  }
  removeController(controller: ReactiveController) {
    this.controllers.delete(controller);
  }

  requestUpdate() {
    forceUpdate(this);
  }

  connectedCallback() {
    this.controllers.forEach((controller) => controller.hostConnected());
  }

  disconnectedCallback() {
    this.controllers.forEach((controller) => controller.hostDisconnected());
  }

  componentWillUpdate() {
    this.controllers.forEach((controller) => controller.hostUpdate());
  }

  componentDidUpdate() {
    this.controllers.forEach((controller) => controller.hostUpdated());
  }
}

Just this would be enough to satisfy the support of reactive controllers.

Describe Alternatives

On slack, @snaptopixel described an alternative approach based around functional components: https://gist.github.com/snaptopixel/9dd86455a5791b65e9a0c0e576c097b6

However, this is entirely custom and stencil-specific, so would not be interoperable and is not framework agnostic.

Related Code

See the following gist: https://gist.github.com/WickyNilliams/79ee85ea370506ac6b16de1920f48e5e

This demonstrates the use of a number of custom controllers integrated into an example stencil component.

The MouseController is lifted verbatim from the Lit docs.

Perhaps, more interesting is that is uses the Task controller published by the lit team, as an example of how functionality can be shared across frameworks!

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Feature: Want this? Upvote it!This PR or Issue may be a great consideration for a future idea.Has WorkaroundThis PR or Issue has a work around detailed within it.Resolution: RefineThis PR is marked for Jira refinement. We're not working on it - we're talking it through.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions