Component State Definitions & Hooks #192
cjpillsbury
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Component State Pattern
Executive Summary
Video.js 10 uses component state definitions to create framework-agnostic bridges between UI components and the media store. A definition addresses three fundamental concerns: (1) selecting which state the component needs, (2) exposing which actions the component can trigger, and (3) propagating changes when state updates. These definitions enable the same component logic to work across React, React Native, HTML/Web Components, and other platforms through thin framework-specific adapters that handle reactivity (hooks for React/React Native, subscription callbacks for HTML).
The declarative structure of these definitions—as "lenses" or "views" into the store—makes them inherently composable and extensible. Developers can easily extend existing definitions, compose multiple definitions together, build new ones following the same pattern, or override specific aspects while reusing the rest.
Note on maturity: This architecture is in early exploration. Design decisions are intentionally loose to allow experimentation with different approaches (key arrays vs. selectors, action lists vs. method generation, centralized vs. component-level defaults). This document presents concerns, considerations, and tradeoffs rather than prescribing specific solutions.
Overview: The Lens Concept
Component state definitions solve a fundamental problem: how do UI components declare their relationship with the media store without coupling to a specific framework or platform?
A component state definition acts as a lens or view into the media store. It doesn't implement UI rendering or framework integration—it declares what the component needs, not how to provide it. This separation enables the same definition to work across React, React Native, HTML/Web Components, and future platforms.
Three Fundamental Concerns
Layered Architecture
Framework adapters handle reactivity (hooks, observables, callbacks) and rendering (JSX, DOM, native components). The core definition remains unchanged across all platforms.
Why This Enables Architectural Goals
Core Responsibilities
1. State Selection
What: Define which pieces of store state matter to this component.
A play button cares about
paused. A volume slider cares aboutvolume,muted, andvolumeLevel. Components declare their state needs without knowing how subscriptions work.Key tradeoff: Key arrays (
['paused', 'volume']) are simple and easy to optimize. Selector functions (state => ({ paused: state.paused })) are flexible and support derived state, but require equality checking. Both are viable; key arrays can be built on top of selectors as a convenience.Architectural note: The current prototype uses nanostores'
subscribeKeys(keys, callback). When replacing nanostores, the store could continue supporting key-based subscriptions or move to selector-based. Either way, component definitions can provide whichever API serves components best—the store's internal subscription mechanism is an implementation detail.Open questions: Nested selection? Computed/derived state ownership? State transformation/renaming?
2. Action Exposure
What: Define which actions this component can trigger.
A play button dispatches
playrequestandpauserequest. A volume slider dispatchesvolumerequest. Components trigger state changes without coupling to dispatch implementation details.Key tradeoff: Declarative approaches (action lists/mappings) reduce boilerplate and are easy to understand. Imperative approaches (method generation functions) provide full control for validation, transformation, and complex logic. The current prototype uses imperative for flexibility, but this creates boilerplate that could be reduced with conventions for common cases.
Open questions: Should definitions expose actions directly or generate methods? How to handle action parameters? Who owns the action-to-method bridge? (See Media Store Pattern - Architectural Debt #2)
3. Change Propagation
What: Component updates when its selected state changes.
This is a reactivity concern handled by framework adapters (React hooks, HTML callbacks, etc.). Component definitions declare what to watch, not how to react.
Responsibility split:
useSyncExternalStore, HTML's subscription callbacks)Key tradeoff: Key-based subscriptions (
subscribeKeys(['paused'])) are efficient and simple. Selector-based subscriptions are more flexible but require equality checking to determine when output changes. These subscription mechanisms live in the store, not component definitions—definitions just declare intent.4. Default State (Open Question)
Concern: Who provides defaults when state is undefined?
When the media element isn't loaded,
pausedmight beundefined. Should it default totrue? Who decides?Options:
Current state: Mix of both. Not clearly delineated, causing confusion about undefined vs. default values.
Consideration: If using selectors, they can naturally provide defaults (
state => ({ paused: storeState.paused ?? true })), but this duplicates logic unless there's a shared mechanism.Framework Adaptation
Framework adapters translate component definitions into platform-specific reactivity and rendering.
React and React Native
Should be identical. Both use hooks, both use React's rendering model, both benefit from the same component factory patterns.
Framework adapters handle: subscription (via hooks), reactivity (React's re-render), and providing store access (context). The definition remains unchanged.
Key insight: React Native's media components may have different APIs than
HTMLMediaElement, but that's handled by mediators (state owner abstraction), not by component definitions or React adapters.HTML/Web Components
Different patterns due to platform constraints. Unlike React, vanilla HTML/Web Components lack a built-in reactivity system.
Conceptual approach (could look similar to React):
Key differences from React:
Architectural consideration: The adapter must bridge between the framework-agnostic definition and vanilla Web Component lifecycles without requiring a reactive framework. The specific pattern for triggering DOM updates (element reference with update method vs. callback) is still being explored.
Comparison to other state libraries: Most state management libraries don't provide vanilla Web Component integrations—they rely on framework wrappers. For example, TanStack Query/Store support React, Solid, and other frameworks, but not vanilla Web Components directly. Framework integrations like Solid bring their own reactive runtime and rendering paradigm, enabling hook-like patterns but adding framework overhead and bundle size. Video.js 10 targets true vanilla Web Components without requiring a reactive framework, which creates unique adapter design constraints.
Benefit for developers: This means developers can build or extend Video.js components using any Web Component approach—whether using frameworks like Lit or Solid.js, generic Web Component libraries, or building from scratch—and still leverage the Video.js architecture and component state definitions.
Open Questions & Future Directions
State selection strategy: Keys, selectors, or both? When to use which?
Nested state: Will we need to select from nested structures? How to express?
Derived state patterns: Should definitions compute derived state, or should store provide computed values?
Type inference: Can TypeScript automatically infer component state and method types from definitions?
Default values for unavailable state: Who provides defaults when state is undefined (before media loads)? Mediators, definitions, separate registry, or hybrid?
Framework adapter conventions: What patterns and code should be shared across all adapters? Where should they diverge?
Component composition: How do compound components (slider with track/thumb/progress) share state definitions?
Beta Was this translation helpful? Give feedback.
All reactions