From 4fce51f5877233fd243348ba5af51d57c0aeecab Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 3 Jul 2025 13:21:09 -0400 Subject: [PATCH 01/27] ooooooooey --- text/0000-resources.md | 654 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 654 insertions(+) create mode 100644 text/0000-resources.md diff --git a/text/0000-resources.md b/text/0000-resources.md new file mode 100644 index 0000000000..a4e5a0da27 --- /dev/null +++ b/text/0000-resources.md @@ -0,0 +1,654 @@ +--- +stage: accepted +start-date: 2025-07-03T00:00:00.000Z +release-date: # In format YYYY-MM-DDT00:00:00.000Z +release-versions: +teams: + - framework + - learning + - typescript +prs: + accepted: # Fill this in with the URL for the Proposal RFC PR +project-link: +suite: +--- + +# Add Resources as a Reactive Primitive + +## Summary + +Resources are a reactive primitive that enables managing stateful processes with cleanup logic as reactive values. They unify concepts like custom helpers, modifiers, and services by providing a consistent pattern for expressing values that have lifecycles and may require cleanup when their owner is destroyed. + +## Motivation + +Ember's current Octane programming model provides excellent primitives for reactive state (`@tracked`), declarative rendering (templates), and component lifecycle, but it lacks a unified primitive for managing stateful processes that need cleanup. This gap leads to several problems: + +### Current Pain Points + +**1. Scattered Lifecycle Management** +Today, managing stateful processes requires spreading setup and cleanup across component lifecycle hooks: + +```js +export default class TimerComponent extends Component { + @tracked time = new Date(); + timer = null; + + constructor() { + super(...arguments); + this.timer = setInterval(() => { + this.time = new Date(); + }, 1000); + } + + willDestroy() { + if (this.timer) { + clearInterval(this.timer); + } + } +} +``` + +This pattern has several issues: +- Setup and cleanup logic are separated across the component lifecycle +- It's easy to forget cleanup, leading to memory leaks +- The logic is tied to component granularity +- Testing requires instantiating entire components + +**2. Reactive Cleanup Complexity** +When tracked data changes, manually managing cleanup of dependent processes is error-prone: + +```js +export default class DataLoader extends Component { + @tracked url; + controller = null; + + @cached + get request() { + // Need to manually handle cleanup when URL changes + this.controller?.abort(); + this.controller = new AbortController(); + + return fetch(this.url, { signal: this.controller.signal }); + } + + willDestroy() { + this.controller?.abort(); + } +} +``` + +**3. Code Organization and Reusability** +Business logic becomes tightly coupled to components, making it hard to: +- Extract and reuse patterns across components +- Test logic in isolation +- Compose smaller pieces into larger functionality + +**4. Lack of Unified Abstraction** +Ember has several overlapping concepts that solve similar problems: +- Custom helpers for derived values +- Modifiers for DOM lifecycle management +- Services for shared state +- Component lifecycle hooks for cleanup + +Each uses different APIs and patterns, creating cognitive overhead and preventing a unified mental model. + +### What Resources Solve + +Resources provide a unified primitive that: + +1. **Co-locates setup and cleanup logic** in a single function +2. **Automatically handles reactive cleanup** when dependencies change +3. **Enables fine-grained composition** independent of component boundaries +4. **Provides a consistent abstraction** for all stateful processes with cleanup +5. **Improves testability** by separating concerns from component lifecycle + +Resources allow developers to model any stateful process as a reactive value with an optional cleanup lifecycle, unifying concepts across the framework while maintaining Ember's declarative, reactive programming model. + +## Detailed design + +### Overview + +A **resource** is a reactive function that represents a value with lifecycle and optional cleanup logic. Resources are created using the `resource()` function and automatically manage their lifecycle through Ember's existing destroyable system. + +```js +import { resource } from '@ember/resources'; +import { cell } from '@glimmer/tracking'; + +const Clock = resource(({ on }) => { + const time = cell(new Date()); + + const timer = setInterval(() => { + time.set(new Date()); + }, 1000); + + on.cleanup(() => clearInterval(timer)); + + return time; +}); +``` + +### Core API + +The `resource()` function takes a single argument: a function that receives a ResourceAPI object and returns a reactive value. + +```ts +interface ResourceAPI { + on: { + cleanup: (destructor: () => void) => void; + }; + use: (resource: T) => ReactiveValue; + owner: Owner; +} + +type ResourceFunction = (api: ResourceAPI) => T; + +function resource(fn: ResourceFunction): Resource +``` + +### Resource Creation and Usage + +Resources can be used in several ways: + +**1. In Templates (as helpers)** +```js +const Clock = resource(({ on }) => { + const time = cell(new Date()); + const timer = setInterval(() => time.set(new Date()), 1000); + on.cleanup(() => clearInterval(timer)); + return time; +}); + +// In template + +``` + +**2. With the `@use` decorator** +```js +import { use } from '@ember/resources'; + +export default class MyComponent extends Component { + @use clock = Clock; + + +} +``` + +**3. With the `use()` function** +```js +export default class MyComponent extends Component { + clock = use(this, Clock); + + +} +``` + +**4. Manual instantiation (for library authors)** +```js +const clockBuilder = resource(() => { /* ... */ }); +const owner = getOwner(this); +const clockInstance = clockBuilder.create(); +clockInstance.link(owner); +const currentTime = clockInstance.current; +``` + +### Resource API Details + +**`on.cleanup(destructor)`** + +Registers a cleanup function that will be called when the resource is destroyed. This happens automatically when: +- The owning context (component, service, etc.) is destroyed +- The resource re-runs due to tracked data changes +- The resource is manually destroyed + +```js +const DataLoader = resource(({ on }) => { + const controller = new AbortController(); + const state = cell({ loading: true }); + + on.cleanup(() => controller.abort()); + + fetch('/api/data', { signal: controller.signal }) + .then(response => response.json()) + .then(data => state.set({ loading: false, data })) + .catch(error => state.set({ loading: false, error })); + + return state; +}); +``` + +**`use(resource)`** + +Allows composition of resources by consuming other resources with proper lifecycle management: + +```js +const Now = resource(({ on }) => { + const time = cell(new Date()); + const timer = setInterval(() => time.set(new Date()), 1000); + on.cleanup(() => clearInterval(timer)); + return time; +}); + +const FormattedTime = resource(({ use }) => { + const time = use(Now); + return () => time.current.toLocaleTimeString(); +}); +``` + +**`owner`** + +Provides access to the Ember owner for dependency injection: + +```js +const UserSession = resource(({ owner }) => { + const session = owner.lookup('service:session'); + const router = owner.lookup('service:router'); + + return () => ({ + user: session.currentUser, + route: router.currentRouteName + }); +}); +``` + +### Resource Lifecycle + +1. **Creation**: When a resource is first accessed, its function is invoked +2. **Reactivity**: If the resource function reads tracked data, it will re-run when that data changes +3. **Cleanup**: Before re-running or when destroyed, all registered cleanup functions are called +4. **Destruction**: When the owning context is destroyed, the resource and all its cleanup functions are invoked + +### Type Definitions + +```ts +interface ResourceAPI { + on: { + cleanup: (destructor: () => void) => void; + }; + use: (resource: T) => ReactiveValue; + owner: Owner; +} + +type ResourceFunction = (api: ResourceAPI) => T; + +interface Resource { + create(): ResourceInstance; +} + +interface ResourceInstance { + current: T; + link(context: object): void; +} + +function resource(fn: ResourceFunction): Resource; +function use(context: object, resource: Resource): ReactiveValue; +function use(resource: Resource): PropertyDecorator; +``` + +### Integration with Existing Systems + +**Helper Manager Integration** + +Resources integrate with Ember's existing helper manager system. The `resource()` function returns a value that can be used directly in templates through the helper invocation syntax. + +**Destroyable Integration** + +Resources automatically integrate with Ember's destroyable system via `associateDestroyableChild()`, ensuring proper cleanup when parent contexts are destroyed. + +**Ownership Integration** + +Resources receive the owner from their parent context, enabling dependency injection and integration with existing Ember services and systems. + +### Example Use Cases + +**1. Data Fetching** +```js +const RemoteData = resource(({ on }) => { + const state = cell({ loading: true }); + const controller = new AbortController(); + + on.cleanup(() => controller.abort()); + + fetch(this.args.url, { signal: controller.signal }) + .then(response => response.json()) + .then(data => state.set({ loading: false, data })) + .catch(error => state.set({ loading: false, error })); + + return state; +}); +``` + +**2. WebSocket Connection** +```js +const WebSocketConnection = resource(({ on, owner }) => { + const notifications = owner.lookup('service:notifications'); + const socket = new WebSocket('ws://localhost:8080'); + + socket.addEventListener('message', (event) => { + notifications.add(JSON.parse(event.data)); + }); + + on.cleanup(() => socket.close()); + + return () => socket.readyState; +}); +``` + +**3. DOM Event Listeners** +```js +const WindowSize = resource(({ on }) => { + const size = cell({ + width: window.innerWidth, + height: window.innerHeight + }); + + const updateSize = () => size.set({ + width: window.innerWidth, + height: window.innerHeight + }); + + window.addEventListener('resize', updateSize); + on.cleanup(() => window.removeEventListener('resize', updateSize)); + + return size; +}); +``` + +### Implementation Notes + +The implementation will build upon Ember's existing infrastructure: + +- **Helper Manager**: Resources use the existing helper manager system for template integration +- **Destroyable System**: Automatic lifecycle management through existing destroyable primitives +- **Tracking System**: Resources participate in Glimmer's autotracking system +- **Owner System**: Resources receive owners for dependency injection + +The core implementation involves: +1. A helper manager that creates and manages resource instances +2. Integration with `@glimmer/tracking` for reactivity +3. Integration with `@ember/destroyable` for cleanup +4. TypeScript definitions for proper type inference + +## How we teach this + +### Terminology and Naming + +The term **"resource"** was chosen because: + +1. It accurately describes something that requires management (like memory, network connections, timers) +2. It's already familiar to developers from other contexts (system resources, web resources) +3. It emphasizes the lifecycle aspect - resources are acquired and must be cleaned up +4. It unifies existing concepts without deprecating them + +**Key terms:** +- **Resource**: A reactive function that manages a stateful process with cleanup +- **Resource Function**: The function passed to `resource()` that defines the behavior +- **Resource Instance**: The instantiated resource tied to a specific owner +- **Resource API**: The object passed to resource functions containing `on`, `use`, and `owner` + +### Teaching Strategy + +**1. Progressive Enhancement of Existing Patterns** + +Introduce resources as a natural evolution of patterns developers already know: + +```js +// Familiar pattern +export default class extends Component { + @tracked time = new Date(); + + constructor() { + super(...arguments); + this.timer = setInterval(() => { + this.time = new Date(); + }, 1000); + } + + willDestroy() { + clearInterval(this.timer); + } +} + +// Resource pattern +const Clock = resource(({ on }) => { + const time = cell(new Date()); + const timer = setInterval(() => time.set(new Date()), 1000); + on.cleanup(() => clearInterval(timer)); + return time; +}); +``` + +**2. Emphasize Problem-Solution Fit** + +Lead with the problems resources solve: +- "Have you ever forgotten to clean up a timer?" +- "Want to reuse this data loading logic across components?" +- "Need to test stateful logic without rendering components?" + +**3. Start with Simple Examples** + +Begin with straightforward use cases before introducing composition: + +```js +// Start here: Simple timer +const Timer = resource(({ on }) => { + let count = cell(0); + let timer = setInterval(() => count.set(count.current + 1), 1000); + on.cleanup(() => clearInterval(timer)); + return count; +}); + +// Then: Data fetching +const UserData = resource(({ on }) => { + // ... fetch logic +}); + +// Finally: Composition +const Dashboard = resource(({ use }) => { + const timer = use(Timer); + const userData = use(UserData); + return () => ({ time: timer.current, user: userData.current }); +}); +``` + +### Documentation Strategy + +**Ember Guides Updates** + +1. **New Section**: "Working with Resources" + - When to use resources vs components/services/helpers + - Basic patterns and examples + - Composition and testing + +2. **Enhanced Sections**: + - Update "Managing Application State" to include resources + - Add resource examples to "Handling User Interaction" + - Include resource patterns in "Working with Data" + +**API Documentation** + +- Complete API reference for `resource()`, `use()`, and ResourceAPI +- TypeScript definitions with comprehensive JSDoc comments +- Interactive examples for each major use case + +**Migration Guides** + +- How to convert class-based helpers to resources +- Converting component lifecycle patterns to resources +- When to use resources vs existing patterns + +### Learning Materials + +**Blog Posts and Tutorials** +- "Introducing Resources: A New Reactive Primitive" +- "Converting from Class Helpers to Resources" +- "Building Reusable Data Loading with Resources" +- "Testing Resources in Isolation" + +**Interactive Tutorials** +- Ember Tutorial additions demonstrating resource usage +- Playground examples for common patterns +- Step-by-step conversion guides + +### Integration with Existing Learning + +Resources complement existing Ember concepts rather than replacing them: + +- **Components**: Still the primary UI abstraction, now with better tools for managing stateful logic +- **Services**: Still used for app-wide shared state, resources handle localized stateful processes +- **Helpers**: Resources can serve as stateful helpers, while pure functions remain as regular helpers +- **Modifiers**: Resources can encapsulate modifier-like behavior with better composition + +## Drawbacks + +### Learning Curve + +**Additional Abstraction**: Resources introduce a new primitive that developers must learn. While they simplify many patterns, they add to the initial cognitive load for new Ember developers. + +**Multiple Ways to Do Things**: Resources provide another way to manage state and side effects, potentially creating confusion about when to use components vs. services vs. resources. + +### Performance Considerations + +**Memory Overhead**: Each resource instance maintains its own cache and cleanup tracking, which could increase memory usage in applications with many resource instances. + +**Re-computation**: Resources re-run their entire function when tracked dependencies change, which could be less efficient than more granular update patterns. + +### Migration and Ecosystem Impact + +**Existing Patterns**: The addition of resources doesn't make existing patterns invalid, but it might create inconsistency in codebases during transition periods. + +**Addon Ecosystem**: Addons will need time to adopt resource patterns, potentially creating a split between "old" and "new" style addons. + +**Testing Infrastructure**: Existing testing patterns and helpers may not work optimally with resources, requiring new testing utilities and patterns. + +### TypeScript Complexity + +**Advanced Types**: The resource type system, especially around composition with `use()`, involves complex TypeScript patterns that may be difficult for some developers to understand or extend. + +**Decorator Limitations**: The `@use` decorator may not provide optimal TypeScript inference in all scenarios due to limitations in decorator typing. + +## Alternatives + +### Class-Based Resources + +Instead of function-based resources, we could provide a class-based API similar to the current ember-resources library: + +```js +class TimerResource extends Resource { + @tracked time = new Date(); + + setup() { + this.timer = setInterval(() => { + this.time = new Date(); + }, 1000); + } + + teardown() { + clearInterval(this.timer); + } +} +``` + +**Pros**: More familiar to developers used to class-based patterns +**Cons**: More verbose, harder to compose, doesn't leverage functional programming benefits + +### Built-in Helpers with Lifecycle + +Extend the existing helper system to support lifecycle hooks: + +```js +export default class TimerHelper extends Helper { + @tracked time = new Date(); + + didBecomeActive() { + this.timer = setInterval(() => { + this.time = new Date(); + }, 1000); + } + + willDestroy() { + clearInterval(this.timer); + } + + compute() { + return this.time; + } +} +``` + +**Pros**: Builds on existing helper system +**Cons**: Limited to helper use cases, doesn't solve composition or broader lifecycle management + +### Enhanced Modifiers + +Expand the modifier system to handle more use cases: + +```js +export default class DataModifier extends Modifier { + modify(element, [url]) { + // Fetch data and update element + } +} +``` + +**Pros**: Builds on existing modifier system +**Cons**: Still limited to DOM-centric use cases, doesn't solve general stateful logic management + +### Service-Based Solutions + +Encourage using services for all stateful logic: + +```js +export default class TimerService extends Service { + @tracked time = new Date(); + + startTimer() { + this.timer = setInterval(() => { + this.time = new Date(); + }, 1000); + } + + stopTimer() { + clearInterval(this.timer); + } +} +``` + +**Pros**: Uses existing patterns +**Cons**: Too heavyweight for localized state, poor cleanup semantics, singleton limitations + +### No Action (Status Quo) + +Continue with current patterns and encourage better use of existing primitives. + +**Pros**: No additional complexity or learning curve +**Cons**: Doesn't solve the identified problems, perpetuates scattered lifecycle management + +## Unresolved questions + +### Future Synchronization API + +While this RFC focuses on the core resource primitive, a future `on.sync` API (similar to what Starbeam provides) could enable even more powerful reactive patterns. However, this is explicitly out of scope for this RFC to keep the initial implementation focused and stable. + +### Integration with Strict Mode and Template Imports + +How should resources work with Ember's strict mode and template imports? Should resources be importable in templates directly, or only through helper invocation? + +### Performance Optimization Opportunities + +Are there opportunities to optimize resource re-computation through more granular dependency tracking or memoization strategies? + +### Testing Utilities + +What additional testing utilities should be provided in the box for resource testing? Should there be special test helpers for resource lifecycle management? + +### Debugging and Ember Inspector Integration + +How should resources appear in Ember Inspector? What debugging information should be available for resource instances and their lifecycles? + +### Migration Tooling + +Should there be automated codemods to help migrate from existing patterns (class helpers, certain component patterns) to resources? + +### Bundle Size Impact + +What is the impact on bundle size for applications that don't use resources? Can the implementation be designed to be tree-shakeable? From f9d8b31e79bd5b915b40f7a85fc66b2a3464509d Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:34:37 -0400 Subject: [PATCH 02/27] Updates --- text/0000-resources.md | 105 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index a4e5a0da27..340d73b642 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -111,8 +111,7 @@ Resources allow developers to model any stateful process as a reactive value wit A **resource** is a reactive function that represents a value with lifecycle and optional cleanup logic. Resources are created using the `resource()` function and automatically manage their lifecycle through Ember's existing destroyable system. ```js -import { resource } from '@ember/resources'; -import { cell } from '@glimmer/tracking'; +import { cell, resource } from '@ember/reactive'; const Clock = resource(({ on }) => { const time = cell(new Date()); @@ -151,6 +150,8 @@ Resources can be used in several ways: **1. In Templates (as helpers)** ```js +import { cell, resource } from '@ember/reactive'; + const Clock = resource(({ on }) => { const time = cell(new Date()); const timer = setInterval(() => time.set(new Date()), 1000); @@ -164,7 +165,7 @@ const Clock = resource(({ on }) => { **2. With the `@use` decorator** ```js -import { use } from '@ember/resources'; +import { use } from '@ember/reactive'; export default class MyComponent extends Component { @use clock = Clock; @@ -173,6 +174,48 @@ export default class MyComponent extends Component { } ``` +The `@use` decorator is an ergonomic shorthand that automatically invokes any value with a registered helper manager. This means that `Clock` (which has a resource helper manager) gets automatically invoked when accessed, eliminating the need to call it explicitly or access `.current`. This works for resources, but also for any other construct that has registered a helper manager via `setHelperManager` from RFC 625 and RFC 756. + +**Convention: Function Wrapping for Clarity** + +While `@use clock = Clock` works, assigning a value via `@use` can look unusual since no invocation is apparent. By convention, it's more appropriate to wrap resources in a function so that the invocation site clearly indicates that behavior is happening: + +```js +// Preferred: Clear that invocation/behavior is occurring +@use clock = Clock() + +// Works but less clear: Looks like simple assignment +@use clock = Clock +``` + +This convention makes the code more readable and aligns with the expectation that decorators like `@use` are performing some active behavior rather than passive assignment. + +#### Helper Manager Integration and the `@use` Decorator + +The `@use` decorator builds upon Ember's existing helper manager infrastructure (RFC 625 and RFC 756) to provide automatic invocation of values with registered helper managers. When a resource is created with `resource()`, it receives a helper manager that makes it invokable in templates and enables the `@use` decorator's automatic behavior. + +Here's how it works: + +```js +// A resource has a helper manager registered +const Clock = resource(({ on }) => { + // ... resource implementation +}); + +// The @use decorator detects the helper manager and automatically invokes it +@use clock = Clock; // Equivalent to: clock = Clock() + +// Without @use, you need explicit invocation or .current access +clock = use(this, Clock); // Returns reactive value, need .current +clock = Clock(); // Direct invocation in template context +``` + +The `@use` decorator pattern extends beyond resources to work with any construct that has registered a helper manager. This means that future primitives that integrate with the helper manager system (like certain kinds of computed values, cached functions, or other reactive constructs) will automatically work with `@use` without any changes to the decorator itself. + +**Helper Manager vs Direct Invocation:** + +When using resources in templates directly (e.g., `{{Clock}}`), the helper manager handles the invocation automatically. The `@use` decorator brings this same ergonomic benefit to class properties, bridging the gap between template usage and class-based usage patterns. + **3. With the `use()` function** ```js export default class MyComponent extends Component { @@ -182,6 +225,8 @@ export default class MyComponent extends Component { } ``` +The `use()` function provides manual resource instantiation when you need more control over the lifecycle or want to avoid the automatic invocation behavior of `@use`. + **4. Manual instantiation (for library authors)** ```js const clockBuilder = resource(() => { /* ... */ }); @@ -216,9 +261,9 @@ const DataLoader = resource(({ on }) => { }); ``` -**`use(resource)`** +**`use(resource)` - Resource Composition** -Allows composition of resources by consuming other resources with proper lifecycle management: +The `use()` method within a resource function allows composition of resources by consuming other resources with proper lifecycle management. This is different from the top-level `use()` function and the `@use` decorator: ```js const Now = resource(({ on }) => { @@ -234,6 +279,56 @@ const FormattedTime = resource(({ use }) => { }); ``` +### Key Differences Between Usage Patterns + +**Understanding `@use` as an Ergonomic Shorthand** + +The `@use` decorator is fundamentally an ergonomic convenience that builds upon Ember's existing helper manager infrastructure. When you apply `@use` to a property, it doesn't assign the value directly—instead, like `@tracked`, it replaces the property with a getter that provides lazy evaluation and automatic invocation. + +```js +export default class MyComponent extends Component { + // This: + @use clock = Clock; + + // Is equivalent to defining a getter that automatically invokes + // any value with a registered helper manager: + get clock() { + // Detect helper manager and invoke automatically + if (hasHelperManager(Clock)) { + return invokeHelper(this, Clock); + } + return Clock; + } +} +``` + +This getter-based approach enables several key benefits: +- **Lazy instantiation**: The resource is only created when first accessed +- **Automatic lifecycle management**: The resource is tied to the component's lifecycle +- **Transparent integration**: Works seamlessly with any construct that has a helper manager + +**`@use` decorator vs resource `use()` vs top-level `use()`:** + +1. **`@use` decorator** - An ergonomic shorthand that leverages Ember's helper manager system: + - Replaces the property with a getter (like `@tracked`) for lazy access + - Automatically invokes values with registered helper managers (RFC 625/756) + - Works with resources, but also any construct that has a helper manager + - Returns the "unwrapped" value directly (no `.current` needed) + - Best for when you want the simplest possible API with automatic lifecycle management + +2. **Resource `use()` method** - For resource composition within resource functions: + - Available only within the resource function's API object + - Manages lifecycle and cleanup of nested resources automatically + - Returns a reactive value that can be consumed with `.current` + - Essential for building complex resources from simpler ones + - Enables hierarchical resource composition with proper cleanup chains + +3. **Top-level `use()` function** - For manual control: + - Requires explicit owner/context management + - Returns a reactive value requiring `.current` access + - Useful when you need fine-grained control over instantiation timing + - Primarily for library authors or advanced use cases where automatic behavior isn't desired + **`owner`** Provides access to the Ember owner for dependency injection: From 864f2b372a0240cae4882b4c4482abff1fef4c26 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:29:41 -0400 Subject: [PATCH 03/27] ope --- text/0000-resources.md | 187 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/text/0000-resources.md b/text/0000-resources.md index 340d73b642..06fba61bb4 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -379,6 +379,65 @@ function use(context: object, resource: Resource): ReactiveValue; function use(resource: Resource): PropertyDecorator; ``` +### Relationship to the Cell Primitive + +While the `cell` primitive (RFC 1071) is not strictly required for resources to function, resources are significantly more ergonomic and powerful when used together with `cell`. Resources can work with Ember's existing `@tracked` properties, but `cell` provides several advantages: + +**Without `cell` (using `@tracked`):** +```js +import { resource } from '@ember/reactive'; +import { tracked } from '@glimmer/tracking'; + +const Clock = resource(({ on }) => { + // Must create a separate class to hold tracked state + class ClockState { + @tracked time = new Date(); + } + + const state = new ClockState(); + + const timer = setInterval(() => { + state.time = new Date(); + }, 1000); + + on.cleanup(() => clearInterval(timer)); + + return () => state.time; +}); + +// Usage requires property access + +``` + +**With `cell` (more ergonomic):** +```js +import { cell, resource } from '@ember/reactive'; + +const Clock = resource(({ on }) => { + const time = cell(new Date()); + + const timer = setInterval(() => { + time.set(new Date()); + }, 1000); + + on.cleanup(() => clearInterval(timer)); + + return time; // Consumer gets the cell directly +}); + +// Usage is more direct + +``` + +**Key advantages of using `cell` with resources:** + +1. **Simpler State Management**: No need to create wrapper classes for tracked properties +2. **Direct Value Returns**: Resources can return cells directly rather than objects with tracked properties +3. **Cleaner APIs**: Consumers get more intuitive interfaces without property access +4. **Better Composition**: Resources that return cells compose more naturally with other resources + +While resources provide significant value on their own by solving lifecycle management and cleanup, the combination with `cell` creates a more complete and developer-friendly reactive primitive system. + ### Integration with Existing Systems **Helper Manager Integration** @@ -393,6 +452,45 @@ Resources automatically integrate with Ember's destroyable system via `associate Resources receive the owner from their parent context, enabling dependency injection and integration with existing Ember services and systems. +**Relationship to the Cell Primitive** + +While this RFC does not strictly depend on the `cell` primitive from RFC 1071, resources become significantly more ergonomic when used together with `cell`. Resources can work with any reactive primitive, including existing `@tracked` properties and `@cached` getters, but `cell` provides several advantages: + +1. **Function-based APIs**: Resources are function-based, and `cell` provides a function-based reactive primitive that composes naturally +2. **Encapsulated state**: Unlike `@tracked` which requires classes, `cell` allows creating reactive state without class overhead +3. **Immediate updates**: `cell` provides both getter and setter APIs (`cell.current` and `cell.set()`) that work well in resource contexts + +Without `cell`, developers would need to either: +- Use classes with `@tracked` properties (heavier abstraction) +- Manually manage reactivity with lower-level tracking APIs +- Return functions from resources that close over mutable state + +Example comparison: + +```js +// With cell (ergonomic) +const Clock = resource(({ on }) => { + const time = cell(new Date()); + const timer = setInterval(() => time.set(new Date()), 1000); + on.cleanup(() => clearInterval(timer)); + return time; +}); + +// Without cell (more verbose) +const Clock = resource(({ on }) => { + let currentTime = new Date(); + const timer = setInterval(() => { + currentTime = new Date(); + // Manual invalidation needed + }, 1000); + on.cleanup(() => clearInterval(timer)); + + return () => currentTime; // Must return function for reactivity +}); +``` + +Therefore, while `cell` is not a hard dependency, implementing both RFCs together would provide the best developer experience. + ### Example Use Cases **1. Data Fetching** @@ -463,6 +561,95 @@ The core implementation involves: 3. Integration with `@ember/destroyable` for cleanup 4. TypeScript definitions for proper type inference +### Future Rendering Architecture + +While not part of this RFC's scope, resources have the potential to serve as a foundation for a future rendering architecture that could eventually replace Glimmer VM. The resource primitive's lifecycle management and reactive cleanup capabilities make it well-suited for managing DOM nodes directly: + +```js +// Hypothetical internal framework implementation +// (NOT user-facing API - inspired by Starbeam patterns) + +const TextNode = resource(({ on }) => { + // In practice, text would come from tracked template bindings + const textContent = getTrackedContent(); // hypothetical framework API + const node = document.createTextNode(''); + + // Update text when reactive content changes + textContent.current; // consume for reactivity + node.textContent = textContent.current; + + on.cleanup(() => { + if (node.parentNode) { + node.parentNode.removeChild(node); + } + }); + + return { + domNode: node, + insert: (parent) => parent.appendChild(node) + }; +}); + +const ElementNode = resource(({ on, use }) => { + // In practice, element info would come from template compilation + const elementSpec = getTrackedElementSpec(); // hypothetical framework API + const element = document.createElement(elementSpec.current.tagName); + + // Handle attributes reactively + const attrs = elementSpec.current.attributes; + for (const [name, valueCell] of Object.entries(attrs)) { + valueCell.current; // consume for reactivity + if (valueCell.current !== null) { + element.setAttribute(name, String(valueCell.current)); + } else { + element.removeAttribute(name); + } + } + + // Compose child resources + const childSpecs = elementSpec.current.children; + const children = childSpecs.map(childSpec => use(childSpec)); + children.forEach(child => child.insert(element)); + + on.cleanup(() => { + if (element.parentNode) { + element.parentNode.removeChild(element); + } + }); + + return { + domNode: element, + insert: (parent) => parent.appendChild(element) + }; +}); + +// Template:
{{@message}}
+// Could compile to: +const CompiledTemplate = resource(({ use }) => { + const className = getArgument('className'); // from template compilation + const message = getArgument('message'); + + return use(ElementNode.with({ + tagName: 'div', + attributes: { class: className }, + children: [ + TextNode.with({ content: message }) + ] + })); +}); +``` + +This approach would provide several advantages over traditional virtual DOM or VM-based rendering: + +- **Granular Reactivity**: Each DOM node could be individually reactive to its specific tracked dependencies +- **Automatic Cleanup**: DOM nodes would be automatically removed when their owning resources are destroyed +- **Resource Composition**: Complex UI could be built by composing simpler DOM resources using the `use()` method +- **Memory Efficiency**: No need for a separate virtual DOM layer or intermediate representation +- **Direct DOM Updates**: Changes could be applied directly to the affected DOM nodes without diffing +- **Structured Return Values**: Resources can return rich objects (like `{ domNode, insert }`) that provide both the DOM node and methods for composition + +This is a long-term architectural possibility that demonstrates the flexibility and power of the resource primitive, though it remains outside the scope of this RFC. + ## How we teach this ### Terminology and Naming From 3b240060133fe0eddfe3dc3a20d7ae703316a910 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 3 Jul 2025 20:41:05 -0400 Subject: [PATCH 04/27] ope --- text/0000-resources.md | 940 ++++++++++++++++++++++++++++++++--------- 1 file changed, 740 insertions(+), 200 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index 06fba61bb4..8fab4a7db7 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -178,7 +178,7 @@ The `@use` decorator is an ergonomic shorthand that automatically invokes any va **Convention: Function Wrapping for Clarity** -While `@use clock = Clock` works, assigning a value via `@use` can look unusual since no invocation is apparent. By convention, it's more appropriate to wrap resources in a function so that the invocation site clearly indicates that behavior is happening: +While `@use clock = Clock` works, assigning a value via `@use` can look unusual since no invocation is apparent. By convention, it's more appropriate to wrap resources in a function so that the invocation site clearly indicates that behavior: ```js // Preferred: Clear that invocation/behavior is occurring @@ -491,294 +491,641 @@ const Clock = resource(({ on }) => { Therefore, while `cell` is not a hard dependency, implementing both RFCs together would provide the best developer experience. -### Example Use Cases +### Advanced Reactive Concepts + +Ember's resource primitive embodies several advanced reactivity concepts that position it as a modern, best-in-class reactive abstraction. Understanding these concepts helps developers leverage resources most effectively and appreciate how they fit into the broader reactive ecosystem. + +#### Evaluation Models: Lazy by Default, Scheduled When Needed + +Resources follow a **lazy evaluation model** by default, meaning they are only created and evaluated when their value is actually consumed. This aligns with the principle that expensive computations should only occur when their results are needed: -**1. Data Fetching** ```js -const RemoteData = resource(({ on }) => { - const state = cell({ loading: true }); - const controller = new AbortController(); +const ExpensiveComputation = resource(({ on }) => { + console.log('This only runs when accessed'); // Lazy evaluation + const result = cell(performExpensiveCalculation()); + return result; +}); - on.cleanup(() => controller.abort()); +// Resource not created yet +@use expensiveData = ExpensiveComputation; - fetch(this.args.url, { signal: controller.signal }) - .then(response => response.json()) - .then(data => state.set({ loading: false, data })) - .catch(error => state.set({ loading: false, error })); +// Only now is the resource created and evaluated + +``` + +However, certain types of resources benefit from **scheduled evaluation**, particularly those that involve side effects or async operations: +```js +const DataFetcher = resource(({ on }) => { + const state = cell({ loading: true }); + + // Side effect happens immediately (scheduled evaluation) + // to avoid waterfalls when multiple async resources are composed + fetchData().then(data => state.set({ loading: false, data })); + return state; }); ``` -**2. WebSocket Connection** +This hybrid approach—lazy by default, scheduled when beneficial—provides optimal performance while avoiding common pitfalls like request waterfalls in async scenarios. + +#### Reactive Ownership and Automatic Disposal + +Resources implement **reactive ownership**, a pattern pioneered by fine-grained reactive systems, which enables automatic memory management without manual disposal: + ```js -const WebSocketConnection = resource(({ on, owner }) => { - const notifications = owner.lookup('service:notifications'); - const socket = new WebSocket('ws://localhost:8080'); +const ParentResource = resource(({ use, on }) => { + // Child resources are automatically owned by parent + const child1 = use(SomeChildResource); + const child2 = use(AnotherChildResource); - socket.addEventListener('message', (event) => { - notifications.add(JSON.parse(event.data)); + // When ParentResource is destroyed, all children are automatically cleaned up + // This creates a disposal tree similar to component hierarchies + + return () => ({ + data1: child1.current, + data2: child2.current }); - - on.cleanup(() => socket.close()); - - return () => socket.readyState; }); ``` -**3. DOM Event Listeners** -```js -const WindowSize = resource(({ on }) => { - const size = cell({ - width: window.innerWidth, - height: window.innerHeight - }); +The ownership model ensures that: +- **No Memory Leaks**: Child resources are automatically disposed when parents are destroyed +- **Hierarchical Cleanup**: Disposal propagates down through the resource tree +- **Automatic Lifecycle**: No manual `willDestroy` or cleanup management needed +- **Composable Boundaries**: Resources can create isolated ownership scopes - const updateSize = () => size.set({ - width: window.innerWidth, - height: window.innerHeight - }); +#### Push-Pull Reactivity and Glitch-Free Consistency - window.addEventListener('resize', updateSize); - on.cleanup(() => window.removeEventListener('resize', updateSize)); +Resources participate in Ember's **push-pull reactive system**, which combines the benefits of both push-based (event-driven) and pull-based (demand-driven) reactivity: - return size; +```js +const DerivedValue = resource(({ use }) => { + const source1 = use(SourceA); + const source2 = use(SourceB); + + // This derivation is guaranteed to be glitch-free: + // When SourceA changes, this won't re-run with stale SourceB data + return () => source1.current + source2.current; }); ``` -### Implementation Notes +**Push Phase**: When tracked data changes, notifications propagate down the dependency graph, marking potentially affected resources as "dirty." -The implementation will build upon Ember's existing infrastructure: +**Pull Phase**: When a value is actually consumed (in templates, effects, etc.), the system pulls fresh values up the dependency chain, ensuring all intermediate values are consistent. -- **Helper Manager**: Resources use the existing helper manager system for template integration -- **Destroyable System**: Automatic lifecycle management through existing destroyable primitives -- **Tracking System**: Resources participate in Glimmer's autotracking system -- **Owner System**: Resources receive owners for dependency injection +This approach guarantees **glitch-free consistency**—user code never observes intermediate or inconsistent states during reactive updates. -The core implementation involves: -1. A helper manager that creates and manages resource instances -2. Integration with `@glimmer/tracking` for reactivity -3. Integration with `@ember/destroyable` for cleanup -4. TypeScript definitions for proper type inference +#### Phased Execution and Ember's Rendering Lifecycle -### Future Rendering Architecture +Resources integrate seamlessly with Ember's three-phase execution model: -While not part of this RFC's scope, resources have the potential to serve as a foundation for a future rendering architecture that could eventually replace Glimmer VM. The resource primitive's lifecycle management and reactive cleanup capabilities make it well-suited for managing DOM nodes directly: +1. **Pure Phase**: Resource functions run and compute derived values +2. **Render Phase**: Template rendering consumes resource values +3. **Post-Render Phase**: Effects and cleanup logic execute ```js -// Hypothetical internal framework implementation -// (NOT user-facing API - inspired by Starbeam patterns) - -const TextNode = resource(({ on }) => { - // In practice, text would come from tracked template bindings - const textContent = getTrackedContent(); // hypothetical framework API - const node = document.createTextNode(''); +const UIResource = resource(({ on, use }) => { + // PURE PHASE: Calculations and data preparation + const data = use(DataSource); + const formattedData = () => formatForDisplay(data.current); - // Update text when reactive content changes - textContent.current; // consume for reactivity - node.textContent = textContent.current; + // RENDER PHASE: Template consumes formattedData + // POST-RENDER PHASE: Side effects via cleanup on.cleanup(() => { - if (node.parentNode) { - node.parentNode.removeChild(node); - } + // Analytics, logging, or other side effects + trackResourceUsage('UIResource', data.current); }); - return { - domNode: node, - insert: (parent) => parent.appendChild(node) - }; + return formattedData; }); +``` -const ElementNode = resource(({ on, use }) => { - // In practice, element info would come from template compilation - const elementSpec = getTrackedElementSpec(); // hypothetical framework API - const element = document.createElement(elementSpec.current.tagName); - - // Handle attributes reactively - const attrs = elementSpec.current.attributes; - for (const [name, valueCell] of Object.entries(attrs)) { - valueCell.current; // consume for reactivity - if (valueCell.current !== null) { - element.setAttribute(name, String(valueCell.current)); - } else { - element.removeAttribute(name); - } - } - - // Compose child resources - const childSpecs = elementSpec.current.children; - const children = childSpecs.map(childSpec => use(childSpec)); - children.forEach(child => child.insert(element)); +This phased approach ensures predictable execution order and enables advanced features like React's concurrent rendering patterns. + +#### The Principle: "What Can Be Derived, Should Be Derived" + +Resources embody the fundamental reactive principle that **state should be minimized and derived values should be maximized**. This leads to more predictable, testable, and maintainable applications: + +```js +// AVOID: Manual state synchronization +const BadPattern = resource(({ on }) => { + const firstName = cell('John'); + const lastName = cell('Doe'); + const fullName = cell(''); // Redundant state! + // Manual synchronization - error prone on.cleanup(() => { - if (element.parentNode) { - element.parentNode.removeChild(element); - } + // Complex logic to keep fullName in sync... }); - return { - domNode: element, - insert: (parent) => parent.appendChild(element) - }; + return { firstName, lastName, fullName }; }); -// Template:
{{@message}}
-// Could compile to: -const CompiledTemplate = resource(({ use }) => { - const className = getArgument('className'); // from template compilation - const message = getArgument('message'); +// PREFER: Derived values +const GoodPattern = resource(() => { + const firstName = cell('John'); + const lastName = cell('Doe'); + + // Derived value - always consistent + const fullName = () => `${firstName.current} ${lastName.current}`; - return use(ElementNode.with({ - tagName: 'div', - attributes: { class: className }, - children: [ - TextNode.with({ content: message }) - ] - })); + return { firstName, lastName, fullName }; }); ``` -This approach would provide several advantages over traditional virtual DOM or VM-based rendering: +Resources make it natural to follow this principle by: +- **Encouraging functional composition** through the `use()` method +- **Making derivation explicit** through return values +- **Automating consistency** through reactive dependencies +- **Eliminating manual synchronization** through automatic re-evaluation -- **Granular Reactivity**: Each DOM node could be individually reactive to its specific tracked dependencies -- **Automatic Cleanup**: DOM nodes would be automatically removed when their owning resources are destroyed -- **Resource Composition**: Complex UI could be built by composing simpler DOM resources using the `use()` method -- **Memory Efficiency**: No need for a separate virtual DOM layer or intermediate representation -- **Direct DOM Updates**: Changes could be applied directly to the affected DOM nodes without diffing -- **Structured Return Values**: Resources can return rich objects (like `{ domNode, insert }`) that provide both the DOM node and methods for composition +#### Async Resources and Colorless Async -This is a long-term architectural possibility that demonstrates the flexibility and power of the resource primitive, though it remains outside the scope of this RFC. +For async operations, resources support **colorless async**—patterns where async and sync values can be treated uniformly without pervasive `async`/`await` coloring: -## How we teach this +```js +const AsyncData = resource(({ on }) => { + const state = cell({ loading: true, data: null, error: null }); + const controller = new AbortController(); + + on.cleanup(() => controller.abort()); + + fetchData({ signal: controller.signal }) + .then(data => state.set({ loading: false, data, error: null })) + .catch(error => state.set({ loading: false, data: null, error })); + + return state; +}); -### Terminology and Naming +// Usage is the same whether data is sync or async +const ProcessedData = resource(({ use }) => { + const asyncData = use(AsyncData); + + // No async/await needed - just reactive composition + return () => { + const { loading, data, error } = asyncData.current; + if (loading) return 'Loading...'; + if (error) return `Error: ${error.message}`; + return processData(data); + }; +}); +``` -The term **"resource"** was chosen because: +This approach avoids the "function coloring problem" where async concerns leak throughout the application, instead containing them within specific resources while maintaining uniform composition patterns. -1. It accurately describes something that requires management (like memory, network connections, timers) -2. It's already familiar to developers from other contexts (system resources, web resources) -3. It emphasizes the lifecycle aspect - resources are acquired and must be cleaned up -4. It unifies existing concepts without deprecating them +These advanced concepts work together to make resources not just a convenience feature, but a foundational primitive that enables sophisticated reactive architectures while remaining approachable for everyday use. By implementing these patterns, Ember's resource system positions itself at the forefront of modern reactive programming, providing developers with tools that are both powerful and intuitive. -**Key terms:** -- **Resource**: A reactive function that manages a stateful process with cleanup -- **Resource Function**: The function passed to `resource()` that defines the behavior -- **Resource Instance**: The instantiated resource tied to a specific owner -- **Resource API**: The object passed to resource functions containing `on`, `use`, and `owner` +### .current Collapsing and Function Returns -### Teaching Strategy +A key ergonomic feature of resources is **automatic `.current` collapsing** in templates and with the `@use` decorator. When a resource returns a `cell` or reactive value, consumers don't need to manually access `.current`: -**1. Progressive Enhancement of Existing Patterns** +```js +const Time = resource(({ on }) => { + const time = cell(new Date()); + const timer = setInterval(() => time.set(new Date()), 1000); + on.cleanup(() => clearInterval(timer)); + return time; // Returns a cell +}); -Introduce resources as a natural evolution of patterns developers already know: +// Template usage - .current is automatic + -```js -// Familiar pattern -export default class extends Component { - @tracked time = new Date(); +// @use decorator - .current is automatic +export default class MyComponent extends Component { + @use time = Time; - constructor() { - super(...arguments); - this.timer = setInterval(() => { - this.time = new Date(); - }, 1000); - } + // No .current needed +} + +// Manual usage - .current is explicit +export default class MyComponent extends Component { + time = use(this, Time); - willDestroy() { - clearInterval(this.timer); - } + // .current needed } +``` -// Resource pattern -const Clock = resource(({ on }) => { - const time = cell(new Date()); - const timer = setInterval(() => time.set(new Date()), 1000); - on.cleanup(() => clearInterval(timer)); - return time; +**Alternative Pattern: Function Returns** + +Resources can also return functions instead of cells, which provides a different composition pattern: + +```js +const FormattedTime = resource(({ use }) => { + const time = use(Time); + + // Return a function that computes the formatted time + return () => time.current.toLocaleTimeString(); }); + +// Usage is identical regardless of return type + ``` -**2. Emphasize Problem-Solution Fit** +Function returns are particularly useful for: +- **Derived computations** that transform reactive values +- **Conditional logic** that depends on multiple reactive sources +- **Complex formatting** or data transformation +- **Memoization patterns** where you want to control when computation occurs -Lead with the problems resources solve: -- "Have you ever forgotten to clean up a timer?" -- "Want to reuse this data loading logic across components?" -- "Need to test stateful logic without rendering components?" +The choice between returning cells and returning functions depends on whether the resource primarily holds state (use cells) or computes derived values (use functions). -**3. Start with Simple Examples** +### Example Use Cases -Begin with straightforward use cases before introducing composition: +**1. Data Fetching with Modern Async Patterns** +```js +const RemoteData = resource(({ on }) => { + const state = cell({ loading: true, data: null, error: null }); + const controller = new AbortController(); + + on.cleanup(() => controller.abort()); + // Scheduled evaluation - fetch starts immediately to avoid waterfalls + fetch(this.args.url, { signal: controller.signal }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response.json(); + }) + .then(data => state.set({ loading: false, data, error: null })) + .catch(error => { + // Only update state if the request wasn't aborted + if (!controller.signal.aborted) { + state.set({ loading: false, data: null, error }); + } + }); + + return state; +}); + +// Composable data processing - avoids request waterfalls +const ProcessedData = resource(({ use }) => { + const rawData = use(RemoteData); + + return () => { + const { loading, data, error } = rawData.current; + + if (loading) return { status: 'loading' }; + if (error) return { status: 'error', message: error.message }; + + return { + status: 'success', + processedData: data.map(item => ({ + ...item, + timestamp: new Date(item.createdAt).toLocaleDateString() + })) + }; + }; +}); +``` + +**2. WebSocket Connection with Reactive State Management** ```js -// Start here: Simple timer -const Timer = resource(({ on }) => { - let count = cell(0); - let timer = setInterval(() => count.set(count.current + 1), 1000); - on.cleanup(() => clearInterval(timer)); - return count; +const WebSocketConnection = resource(({ on, owner }) => { + const notifications = owner.lookup('service:notifications'); + const connectionState = cell({ + status: 'connecting', + lastMessage: null, + errorCount: 0 + }); + + const socket = new WebSocket('ws://localhost:8080'); + + socket.addEventListener('open', () => { + connectionState.set({ + status: 'connected', + lastMessage: null, + errorCount: 0 + }); + }); + + socket.addEventListener('message', (event) => { + const message = JSON.parse(event.data); + connectionState.set({ + ...connectionState.current, + lastMessage: message, + status: 'connected' + }); + notifications.add(message); + }); + + socket.addEventListener('error', () => { + const current = connectionState.current; + connectionState.set({ + ...current, + status: 'error', + errorCount: current.errorCount + 1 + }); + }); + + socket.addEventListener('close', () => { + connectionState.set({ + ...connectionState.current, + status: 'disconnected' + }); + }); + + on.cleanup(() => { + if (socket.readyState === WebSocket.OPEN) { + socket.close(); + } + }); + + return connectionState; }); +``` -// Then: Data fetching -const UserData = resource(({ on }) => { - // ... fetch logic +**3. Reactive DOM Event Handling with Debouncing** +```js +const WindowSize = resource(({ on }) => { + const size = cell({ + width: window.innerWidth, + height: window.innerHeight, + aspectRatio: window.innerWidth / window.innerHeight + }); + + // Debounced update to avoid excessive re-renders + let timeoutId; + const updateSize = () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + const width = window.innerWidth; + const height = window.innerHeight; + size.set({ + width, + height, + aspectRatio: width / height + }); + }, 150); + }; + + window.addEventListener('resize', updateSize, { passive: true }); + + on.cleanup(() => { + window.removeEventListener('resize', updateSize); + clearTimeout(timeoutId); + }); + + return size; }); -// Finally: Composition -const Dashboard = resource(({ use }) => { - const timer = use(Timer); +// Derived breakpoint resource +const BreakpointInfo = resource(({ use }) => { + const windowSize = use(WindowSize); + + return () => { + const { width } = windowSize.current; + + if (width < 768) return { breakpoint: 'mobile', isMobile: true }; + if (width < 1024) return { breakpoint: 'tablet', isMobile: false }; + return { breakpoint: 'desktop', isMobile: false }; + }; +}); +``` + +### Best Practices and Patterns + +#### Resource Composition Patterns + +**Hierarchical Composition**: Build complex resources from simpler ones using reactive ownership: + +```js +const UserSession = resource(({ use, owner }) => { + const auth = owner.lookup('service:auth'); + const currentUser = use(CurrentUser); + const preferences = use(UserPreferences); + + return () => ({ + user: currentUser.current, + preferences: preferences.current, + isAdmin: auth.hasRole('admin', currentUser.current) + }); +}); + +const CurrentUser = resource(({ on, owner }) => { + const session = owner.lookup('service:session'); + const userData = cell(session.currentUser); + + const handleUserChange = () => userData.set(session.currentUser); + session.on('userChanged', handleUserChange); + on.cleanup(() => session.off('userChanged', handleUserChange)); + + return userData; +}); +``` + +**Parallel Data Loading**: Avoid waterfalls by composing resources that fetch independently: + +```js +const DashboardData = resource(({ use }) => { + // All three resources start fetching in parallel const userData = use(UserData); - return () => ({ time: timer.current, user: userData.current }); + const analytics = use(AnalyticsData); + const notifications = use(NotificationData); + + return () => ({ + user: userData.current, + analytics: analytics.current, + notifications: notifications.current, + // Derived state based on all three + hasUnreadNotifications: notifications.current.unreadCount > 0 + }); }); ``` -### Documentation Strategy +#### Error Handling and Resilience -**Ember Guides Updates** +**Graceful Degradation**: Design resources to handle errors gracefully: -1. **New Section**: "Working with Resources" - - When to use resources vs components/services/helpers - - Basic patterns and examples - - Composition and testing +```js +const ResilientDataLoader = resource(({ on }) => { + const state = cell({ + status: 'loading', + data: null, + error: null, + retryCount: 0 + }); + + const maxRetries = 3; + const backoffDelay = (attempt) => Math.min(1000 * Math.pow(2, attempt), 10000); + + const fetchWithRetry = async (attempt = 0) => { + try { + const response = await fetch('/api/critical-data'); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + + const data = await response.json(); + state.set({ status: 'success', data, error: null, retryCount: attempt }); + } catch (error) { + if (attempt < maxRetries) { + const delay = backoffDelay(attempt); + setTimeout(() => fetchWithRetry(attempt + 1), delay); + state.set({ + status: 'retrying', + data: null, + error, + retryCount: attempt + 1 + }); + } else { + state.set({ status: 'failed', data: null, error, retryCount: attempt }); + } + } + }; + + fetchWithRetry(); + + return state; +}); +``` -2. **Enhanced Sections**: - - Update "Managing Application State" to include resources - - Add resource examples to "Handling User Interaction" - - Include resource patterns in "Working with Data" +#### Performance Optimization -**API Documentation** +**Lazy Evaluation for Expensive Operations**: -- Complete API reference for `resource()`, `use()`, and ResourceAPI -- TypeScript definitions with comprehensive JSDoc comments -- Interactive examples for each major use case +```js +const ExpensiveComputation = resource(({ use }) => { + const sourceData = use(SourceData); + + // Use function return to defer computation until needed + return () => { + const data = sourceData.current; + + // Only compute when actually accessed + if (data.status !== 'success') { + return { status: data.status }; + } + + return { + status: 'computed', + result: performExpensiveAnalysis(data.data) + }; + }; +}); +``` -**Migration Guides** +**Memoization for Stable Results**: -- How to convert class-based helpers to resources -- Converting component lifecycle patterns to resources -- When to use resources vs existing patterns +```js +const MemoizedProcessor = resource(({ use }) => { + const input = use(InputData); + let lastInput = null; + let cachedResult = null; + + return () => { + const currentInput = input.current; + + // Memoize based on input identity/equality + if (currentInput !== lastInput) { + lastInput = currentInput; + cachedResult = expensiveProcessing(currentInput); + } + + return cachedResult; + }; +}); +``` -### Learning Materials +#### Testing Patterns -**Blog Posts and Tutorials** -- "Introducing Resources: A New Reactive Primitive" -- "Converting from Class Helpers to Resources" -- "Building Reusable Data Loading with Resources" -- "Testing Resources in Isolation" +**Resource Testing in Isolation**: -**Interactive Tutorials** -- Ember Tutorial additions demonstrating resource usage -- Playground examples for common patterns -- Step-by-step conversion guides +```js +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { cell } from '@ember/reactive'; + +module('Unit | Resource | timer', function(hooks) { + setupTest(hooks); + + test('timer updates every second', async function(assert) { + const TestTimer = resource(({ on }) => { + const time = cell(new Date('2023-01-01T00:00:00')); + let interval = 0; + + const timer = setInterval(() => { + interval += 1000; + time.set(new Date('2023-01-01T00:00:00').getTime() + interval); + }, 100); // Faster for testing + + on.cleanup(() => clearInterval(timer)); + return time; + }); + + const instance = TestTimer.create(); + instance.link(this.owner); + + const initialTime = instance.current.current; + + await new Promise(resolve => setTimeout(resolve, 250)); + + const laterTime = instance.current.current; + assert.ok(laterTime > initialTime, 'Timer advances'); + + instance.destroy(); + }); +}); +``` -### Integration with Existing Learning +#### Integration Patterns -Resources complement existing Ember concepts rather than replacing them: +**Service Integration**: -- **Components**: Still the primary UI abstraction, now with better tools for managing stateful logic -- **Services**: Still used for app-wide shared state, resources handle localized stateful processes -- **Helpers**: Resources can serve as stateful helpers, while pure functions remain as regular helpers -- **Modifiers**: Resources can encapsulate modifier-like behavior with better composition +```js +const ServiceAwareResource = resource(({ owner }) => { + const session = owner.lookup('service:session'); + const router = owner.lookup('service:router'); + const store = owner.lookup('service:store'); + + // Resource can depend on and coordinate multiple services + return () => ({ + currentUser: session.currentUser, + currentRoute: router.currentRouteName, + // Reactive query based on current context + relevantData: store.query('item', { + userId: session.currentUser?.id, + route: router.currentRouteName + }) + }); +}); +``` + +**Cross-Resource Communication**: + +```js +const NotificationBus = resource(({ on }) => { + const subscribers = cell(new Map()); + const messages = cell([]); + + const subscribe = (id, callback) => { + const current = subscribers.current; + const newMap = new Map(current); + newMap.set(id, callback); + subscribers.set(newMap); + }; + + const publish = (message) => { + messages.set([...messages.current, message]); + + // Notify all subscribers + subscribers.current.forEach(callback => { + callback(message); + }); + }; + + const unsubscribe = (id) => { + const current = subscribers.current; + const newMap = new Map(current); + newMap.delete(id); + subscribers.set(newMap); + }; + + return { subscribe, publish, unsubscribe, messages }; +}); +``` + +These patterns demonstrate how resources can be composed to build sophisticated reactive architectures while maintaining clarity, testability, and performance. ## Drawbacks @@ -786,8 +1133,12 @@ Resources complement existing Ember concepts rather than replacing them: **Additional Abstraction**: Resources introduce a new primitive that developers must learn. While they simplify many patterns, they add to the initial cognitive load for new Ember developers. +**Advanced Reactive Concepts**: Resources embody sophisticated reactivity concepts (push-pull evaluation, reactive ownership, lazy vs scheduled evaluation) that may be overwhelming for developers new to reactive programming. + **Multiple Ways to Do Things**: Resources provide another way to manage state and side effects, potentially creating confusion about when to use components vs. services vs. resources. +**Mental Model Shifts**: Developers must learn to think in terms of "what can be derived, should be derived" and understand concepts like glitch-free consistency and automatic ownership disposal. + ### Performance Considerations **Memory Overhead**: Each resource instance maintains its own cache and cleanup tracking, which could increase memory usage in applications with many resource instances. @@ -905,12 +1256,201 @@ Continue with current patterns and encourage better use of existing primitives. **Pros**: No additional complexity or learning curve **Cons**: Doesn't solve the identified problems, perpetuates scattered lifecycle management +## How we teach this + +### Terminology and Naming + +The term **"resource"** was chosen because: + +1. It accurately describes something that requires management (like memory, network connections, timers) +2. It's already familiar to developers from other contexts (system resources, web resources) +3. It emphasizes the lifecycle aspect - resources are acquired and must be cleaned up +4. It unifies existing concepts without deprecating them + +**Key terms:** +- **Resource**: A reactive function that manages a stateful process with cleanup +- **Resource Function**: The function passed to `resource()` that defines the behavior +- **Resource Instance**: The instantiated resource tied to a specific owner +- **Resource API**: The object passed to resource functions containing `on`, `use`, and `owner` + +### Teaching Strategy + +**1. Progressive Enhancement of Existing Patterns** + +Introduce resources as a natural evolution of patterns developers already know: + +```js +// Familiar pattern - manual lifecycle management +export default class extends Component { + @tracked time = new Date(); + + constructor() { + super(...arguments); + this.timer = setInterval(() => { + this.time = new Date(); + }, 1000); + } + + willDestroy() { + clearInterval(this.timer); + } +} + +// Resource pattern - automatic lifecycle management +const Clock = resource(({ on }) => { + const time = cell(new Date()); + const timer = setInterval(() => time.set(new Date()), 1000); + on.cleanup(() => clearInterval(timer)); + return time; +}); +``` + +**2. Emphasize Modern Reactive Principles** + +Lead with the foundational principles that make resources powerful: + +- **"What can be derived, should be derived"**: Minimize state, maximize derivation +- **Automatic cleanup**: No more manual lifecycle management +- **Glitch-free consistency**: Never observe inconsistent intermediate states +- **Lazy by default**: Expensive operations only run when needed +- **Composable ownership**: Resources clean up their children automatically + +**3. Start with Simple Examples, Build to Advanced Patterns** + +```js +// Level 1: Basic resource with cleanup +const SimpleTimer = resource(({ on }) => { + let count = cell(0); + let timer = setInterval(() => count.set(count.current + 1), 1000); + on.cleanup(() => clearInterval(timer)); + return count; +}); + +// Level 2: Async data fetching +const UserData = resource(({ on }) => { + const state = cell({ loading: true }); + const controller = new AbortController(); + + fetch('/api/user', { signal: controller.signal }) + .then(r => r.json()) + .then(data => state.set({ loading: false, data })); + + on.cleanup(() => controller.abort()); + return state; +}); + +// Level 3: Resource composition and derivation +const Dashboard = resource(({ use }) => { + const timer = use(SimpleTimer); + const userData = use(UserData); + + // Derived state - always consistent, no manual synchronization + return () => ({ + uptime: timer.current, + user: userData.current, + // This derivation is glitch-free + greeting: userData.current.loading + ? 'Loading...' + : `Welcome back, ${userData.current.data.name}!` + }); +}); +``` + +**4. Address Common Mental Models** + +Help developers understand how resources differ from familiar patterns: + +| Pattern | Manual Management | Resource Approach | +|---------|------------------|-------------------| +| Component lifecycle | Split setup/cleanup across hooks | Co-located in single function | +| Data synchronization | Manual effects and state updates | Automatic derivation | +| Error boundaries | Try/catch in multiple places | Centralized error handling | +| Memory management | Manual cleanup tracking | Automatic ownership disposal | +| Testing | Complex component setup | Isolated resource testing | + +### Documentation Strategy + +**Ember Guides Updates** + +1. **New Section**: "Working with Resources" + - When to use resources vs components/services/helpers + - Reactive principles and best practices + - Composition patterns and testing + +2. **Enhanced Sections**: + - Update "Managing Application State" to include resource patterns + - Add resource examples to "Handling User Interaction" + - Include advanced reactive concepts in "Data Flow and Reactivity" + +**API Documentation** + +- Complete API reference for `resource()`, `use()`, and ResourceAPI +- TypeScript definitions with comprehensive JSDoc comments +- Interactive examples for each major use case +- Performance guidelines and best practices + +**Migration Guides** + +- How to convert class-based helpers to resources +- Converting component lifecycle patterns to resources +- When to use resources vs existing patterns +- Advanced patterns: async resources, error handling, composition + +### Learning Materials + +**Blog Posts and Tutorials** +- "Introducing Resources: Modern Reactive Architecture for Ember" +- "From Manual Cleanup to Automatic Ownership: Resource Lifecycle Management" +- "Building Robust Async Resources: Error Handling and Resilience Patterns" +- "Resource Composition: Building Complex Logic from Simple Parts" +- "Testing Resources: Isolation, Mocking, and Integration Patterns" + +**Interactive Tutorials** +- Ember Tutorial additions demonstrating resource usage +- Playground examples for common reactive patterns +- Step-by-step migration guides with working examples +- Advanced composition and async pattern workshops + +### Integration with Existing Learning + +Resources complement and enhance existing Ember concepts: + +- **Components**: Still the primary UI abstraction, now with better tools for managing stateful logic +- **Services**: Still used for app-wide shared state, resources handle localized stateful processes +- **Helpers**: Resources can serve as stateful helpers, while pure functions remain as regular helpers +- **Modifiers**: Resources can encapsulate modifier-like behavior with better composition +- **Tracked Properties**: Resources work with `@tracked`, but `cell` provides more ergonomic patterns + +### Advanced Concepts Introduction + +Once developers are comfortable with basic resource usage, introduce advanced reactive concepts: + +1. **Push-Pull Reactivity**: How resources participate in Ember's reactive system +2. **Lazy vs Scheduled Evaluation**: When and why resources evaluate +3. **Glitch-Free Consistency**: Why derived state is always coherent +4. **Ownership and Disposal**: How automatic cleanup prevents memory leaks +5. **Async Patterns**: Colorless async and avoiding request waterfalls + +These concepts position Ember's resource system as a modern, best-in-class reactive architecture while remaining approachable for everyday development. + ## Unresolved questions ### Future Synchronization API While this RFC focuses on the core resource primitive, a future `on.sync` API (similar to what Starbeam provides) could enable even more powerful reactive patterns. However, this is explicitly out of scope for this RFC to keep the initial implementation focused and stable. +### Async Resource Patterns + +Should resources support first-class async primitives (like `createAsync` from the dev.to articles) that throw promises for unresolved values? This could enable "colorless async" patterns where async and sync resources compose uniformly, but it would require significant integration with Ember's rendering system. + +### Scheduling and Evaluation Strategies + +Should resources provide explicit control over evaluation timing (immediate vs lazy vs scheduled)? While the current design uses lazy evaluation by default, certain patterns (like data fetching) might benefit from immediate or scheduled evaluation modes. + +### Error Boundaries and Resource Hierarchies + +How should errors in parent resources affect child resources? Should there be automatic error boundary mechanisms, or should error handling remain explicit through resource return values? + ### Integration with Strict Mode and Template Imports How should resources work with Ember's strict mode and template imports? Should resources be importable in templates directly, or only through helper invocation? From 4fa3a79757e2fd02f97afcc08736c29207afcc53 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:51:06 -0400 Subject: [PATCH 05/27] ope --- text/0000-resources.md | 352 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 310 insertions(+), 42 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index 8fab4a7db7..fc31079b89 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -21,13 +21,13 @@ Resources are a reactive primitive that enables managing stateful processes with ## Motivation -Ember's current Octane programming model provides excellent primitives for reactive state (`@tracked`), declarative rendering (templates), and component lifecycle, but it lacks a unified primitive for managing stateful processes that need cleanup. This gap leads to several problems: +Ember's current Octane programming model provides excellent primitives for reactive state (`@tracked`), declarative rendering (templates), and component lifecycle, but it lacks a unified primitive for managing stateful processes that need cleanup. This fragmentation has created a complex ecosystem where different concepts each require their own approach to lifecycle management, leading to scattered patterns and cognitive overhead. -### Current Pain Points +### The Fragmented Landscape of Lifecycle Management -**1. Scattered Lifecycle Management** -Today, managing stateful processes requires spreading setup and cleanup across component lifecycle hooks: +Today, Ember developers must navigate multiple, disconnected systems for managing setup and teardown: +**Components** use lifecycle hooks like `willDestroy()`: ```js export default class TimerComponent extends Component { @tracked time = new Date(); @@ -35,74 +35,339 @@ export default class TimerComponent extends Component { constructor() { super(...arguments); - this.timer = setInterval(() => { - this.time = new Date(); - }, 1000); + this.timer = setInterval(() => this.time = new Date(), 1000); } willDestroy() { - if (this.timer) { - clearInterval(this.timer); - } + if (this.timer) clearInterval(this.timer); // Easy to forget! + } +} +``` + +**Modifiers** require implementing `destroyModifier()` in their manager: +```js +class OnModifier extends Modifier { + constructor() { + super(...arguments); + registerDestructor(this, cleanup); // Different pattern entirely + } + + modify(element, [event, handler]) { + this.addEventListener(element, event, handler); + } +} +``` + +**Helpers** split between class-based and function-based approaches with no built-in cleanup: +```js +export default class DataHelper extends Helper { + willDestroy() { + this.abortController?.abort(); // Another lifecycle hook + } +} + +// Function helpers have no cleanup mechanism at all +export default helper(() => { + // No way to clean up side effects! +}); +``` + +**Services** rely on manual `willDestroy()` but have singleton lifecycle issues: +```js +export default class WebSocketService extends Service { + willDestroy() { + this.socket?.close(); // Only called on app teardown } } ``` -This pattern has several issues: -- Setup and cleanup logic are separated across the component lifecycle -- It's easy to forget cleanup, leading to memory leaks -- The logic is tied to component granularity -- Testing requires instantiating entire components +**Library authors** must patch `willDestroy()` methods, leading to complex instrumentation: +```js +// From ember-concurrency's approach +function patchWillDestroy(obj) { + let oldWillDestroy = obj.willDestroy; + obj.willDestroy = function() { + if (oldWillDestroy) oldWillDestroy.call(this); + teardownTasks(this); + }; +} +``` -**2. Reactive Cleanup Complexity** -When tracked data changes, manually managing cleanup of dependent processes is error-prone: +### The Destructor API: A Step Forward, But Not Enough + +The `registerDestructor()` API (RFC 580) improved this situation by providing a unified way to register cleanup functions: +```js +class MyComponent extends Component { + constructor() { + let timeoutId = setTimeout(() => console.log('hello'), 1000); + registerDestructor(this, () => clearTimeout(timeoutId)); + } +} +``` + +However, this still requires: +- Manual destructor registration scattered throughout constructors +- Separate setup and cleanup logic +- Understanding when and how to use `associateDestroyableChild()` +- Different patterns for different types of objects + +### Current Pain Points + +**1. Fragmented Setup/Teardown Patterns** +Every Ember construct handles lifecycle differently: +- Components: `constructor()` + `willDestroy()` +- Modifiers: `installModifier()` + `destroyModifier()` +- Helpers: No standard lifecycle pattern +- Services: Singleton-only `willDestroy()` +- Custom classes: Manual `registerDestructor()` calls + +**2. Scattered Lifecycle Logic** +Setup and cleanup are separated across different methods and files: ```js export default class DataLoader extends Component { @tracked url; controller = null; - @cached + constructor() { + super(...arguments); + // Setup happens here + registerDestructor(this, () => this.controller?.abort()); + } + + @cached get request() { - // Need to manually handle cleanup when URL changes + // More setup happens here this.controller?.abort(); this.controller = new AbortController(); - return fetch(this.url, { signal: this.controller.signal }); } willDestroy() { + // Manual cleanup here too this.controller?.abort(); } } ``` -**3. Code Organization and Reusability** -Business logic becomes tightly coupled to components, making it hard to: -- Extract and reuse patterns across components -- Test logic in isolation -- Compose smaller pieces into larger functionality +**3. No Standard Container for Stateful Logic** +Developers lack a consistent way to encapsulate stateful processes with cleanup. This leads to: +- Logic scattered across multiple lifecycle hooks +- Difficulty extracting and reusing patterns +- Testing challenges that require instantiating entire components +- Memory leaks from forgotten cleanup + +**4. Cognitive Overhead from Multiple Patterns** +Developers must learn and remember: +- When to use `willDestroy()` vs `registerDestructor()` +- How helper managers differ from modifier managers +- Which constructs support lifecycle and which don't +- How to properly associate children with `associateDestroyableChild()` + +### Resources: A Unified Solution + +Resources solve these problems by providing a **single, consistent container for setup and teardown logic** that works across all contexts. Instead of learning multiple lifecycle patterns, developers work with one unified primitive: + +```js +const Clock = resource(({ on }) => { + const time = cell(new Date()); + + // Setup and cleanup co-located + const timer = setInterval(() => time.set(new Date()), 1000); + on.cleanup(() => clearInterval(timer)); + + return time; +}); +``` + +**Resources unify all existing concepts** by providing: + +1. **A Standard Container**: One primitive that works for components, helpers, modifiers, services, and custom logic +2. **Co-located Setup/Teardown**: No more spreading logic across constructors, lifecycle hooks, and destructor registrations +3. **Automatic Cleanup Management**: No need to manually call `registerDestructor()` or remember `willDestroy()` +4. **Consistent Patterns**: Same API whether you're building a helper, managing component state, or creating reusable logic +5. **Hierarchical Cleanup**: Automatic resource disposal and child management without manual `associateDestroyableChild()` + +**Before (fragmented patterns):** +- Component lifecycle hooks (`willDestroy`) +- Manual destructor registration (`registerDestructor`) +- Manager-specific cleanup (`destroyModifier`, helper teardown) +- Scattered setup/teardown logic +- Different APIs for every construct + +**After (unified with resources):** +- Single `resource()` function for all stateful logic +- Co-located setup and cleanup via `on.cleanup()` +- Automatic lifecycle management +- Consistent patterns across all use cases +- No more manual lifecycle management + +Resources don't replace existing patterns—they **unify them under a single, powerful abstraction** that eliminates the need to choose between different lifecycle approaches or remember multiple APIs. Whether you're building a component helper, managing WebSocket connections, or creating reusable business logic, resources provide the same elegant pattern for setup and teardown. + +### Conceptual Unification Examples + +To illustrate how resources unify existing concepts, here are small examples showing how traditional Ember constructs could be reimagined using resources. Note that **classes are user-defined** while **resources are framework-internal** - this separation allows resources to wrap and manage existing class-based patterns. + +**Services → Resources** +```js +// User-defined Service class (unchanged) +class SessionService extends Service { + @tracked currentUser = null; + + willDestroy() { + this.logout(); // Manual cleanup + } + + login(credentials) { + return authenticate(credentials).then(user => this.currentUser = user); + } + + logout() { + this.currentUser = null; + } +} + +// Framework-internal resource wrapper +const SessionServiceResource = resource(({ on, owner }) => { + const service = new SessionService(); + + // Framework manages lifecycle via resource pattern + on.cleanup(() => service.willDestroy()); + + return service; +}); +``` + +**Helpers → Resources** +```js +// User-defined Helper class (unchanged) +class FormatDateHelper extends Helper { + willDestroy() { + this.intl?.off('localeChanged', this.recompute); + } + + compute([date]) { + return new Intl.DateTimeFormat().format(date); + } +} -**4. Lack of Unified Abstraction** -Ember has several overlapping concepts that solve similar problems: -- Custom helpers for derived values -- Modifiers for DOM lifecycle management -- Services for shared state -- Component lifecycle hooks for cleanup +// Framework-internal resource wrapper +const FormatDateResource = resource(({ on, owner }) => { + const helper = new FormatDateHelper(); + const intl = owner.lookup('service:intl'); + helper.intl = intl; + + // Framework manages lifecycle via resource pattern + on.cleanup(() => helper.willDestroy()); + + return (date) => helper.compute([date]); +}); +``` -Each uses different APIs and patterns, creating cognitive overhead and preventing a unified mental model. +**Modifiers → Resources** +```js +// User-defined Modifier class (unchanged) +class OnModifier extends Modifier { + modify(element, [event, handler]) { + this.element = element; + this.event = event; + this.handler = handler; + element.addEventListener(event, handler); + } + + willDestroy() { + this.element?.removeEventListener(this.event, this.handler); + } +} -### What Resources Solve +// Framework-internal resource wrapper +const OnModifierResource = resource(({ on }) => { + return (element, [event, handler]) => { + const modifier = new OnModifier(); + modifier.modify(element, [event, handler]); + + // Framework manages lifecycle via resource pattern + on.cleanup(() => modifier.willDestroy()); + }; +}); +``` -Resources provide a unified primitive that: +**Components → Resources** +```js +// User-defined Timer logic class (unchanged) +class TimerLogic { + constructor() { + this.seconds = 0; + this.interval = setInterval(() => this.seconds++, 1000); + } + + willDestroy() { + clearInterval(this.interval); + } +} -1. **Co-locates setup and cleanup logic** in a single function -2. **Automatically handles reactive cleanup** when dependencies change -3. **Enables fine-grained composition** independent of component boundaries -4. **Provides a consistent abstraction** for all stateful processes with cleanup -5. **Improves testability** by separating concerns from component lifecycle +// Framework-internal resource wrapper +const TimerResource = resource(({ on }) => { + const timer = new TimerLogic(); + const seconds = cell(timer.seconds); + + // Sync timer.seconds to cell on each tick + const syncInterval = setInterval(() => seconds.set(timer.seconds), 1000); + + // Framework manages lifecycle via resource pattern + on.cleanup(() => { + clearInterval(syncInterval); + timer.willDestroy(); + }); + + return { seconds }; +}); -Resources allow developers to model any stateful process as a reactive value with an optional cleanup lifecycle, unifying concepts across the framework while maintaining Ember's declarative, reactive programming model. +// Component uses resource +export default class TimerComponent extends Component { + @use timer = TimerResource; + + +} +``` + +**Routes → Resources** +```js +// User-defined Route class (unchanged) +class PostRoute extends Route { + model(params) { + return this.store.findRecord('post', params.id); + } + + willDestroy() { + this.controller?.abort(); + } +} + +// Framework-internal resource wrapper +const PostRouteResource = resource(({ on, owner }) => { + const route = new PostRoute(); + route.store = owner.lookup('service:store'); + + // Framework manages lifecycle via resource pattern + on.cleanup(() => route.willDestroy()); + + return route; +}); +``` + +**The Power of Unified Patterns** + +These examples demonstrate several key points: + +1. **Clear Separation of Concerns**: User-defined classes contain domain logic, while framework-internal resources handle lifecycle management +2. **Migration Path**: Existing classes can be wrapped with resources without modification +3. **Consistent Setup/Teardown**: Always use `on.cleanup()` instead of different lifecycle hooks +4. **Automatic Memory Management**: No need to remember different cleanup patterns across constructs +5. **Composable Logic**: Resources can be easily combined and tested in isolation +6. **Uniform Mental Model**: Same patterns whether wrapping helpers, modifiers, services, or routes + +This unification eliminates the cognitive overhead of learning multiple lifecycle APIs while preserving existing class-based patterns. Resources become the framework's internal mechanism for managing lifecycle, while users continue working with familiar class structures. ## Detailed design @@ -283,7 +548,7 @@ const FormattedTime = resource(({ use }) => { **Understanding `@use` as an Ergonomic Shorthand** -The `@use` decorator is fundamentally an ergonomic convenience that builds upon Ember's existing helper manager infrastructure. When you apply `@use` to a property, it doesn't assign the value directly—instead, like `@tracked`, it replaces the property with a getter that provides lazy evaluation and automatic invocation. +The `@use` decorator is fundamentally an ergonomic convenience that builds upon Ember's helper manager infrastructure. When you apply `@use` to a property, it doesn't assign the value directly—instead, like `@tracked`, it replaces the property with a getter that provides lazy evaluation and automatic invocation. ```js export default class MyComponent extends Component { @@ -523,6 +788,10 @@ const DataFetcher = resource(({ on }) => { // to avoid waterfalls when multiple async resources are composed fetchData().then(data => state.set({ loading: false, data })); + on.cleanup(() => { + // Cleanup logic + }); + return state; }); ``` @@ -663,7 +932,6 @@ const AsyncData = resource(({ on }) => { const ProcessedData = resource(({ use }) => { const asyncData = use(AsyncData); - // No async/await needed - just reactive composition return () => { const { loading, data, error } = asyncData.current; if (loading) return 'Loading...'; From 2f76269067de7b97c72a5f75c5328fb781228fe4 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:12:06 -0400 Subject: [PATCH 06/27] wip --- text/0000-resources.md | 191 +++++++++++++++++++++-------------------- 1 file changed, 96 insertions(+), 95 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index fc31079b89..ca20da0967 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -13,11 +13,11 @@ project-link: suite: --- -# Add Resources as a Reactive Primitive +# Add Resources as a low-level Reactive Primitive ## Summary -Resources are a reactive primitive that enables managing stateful processes with cleanup logic as reactive values. They unify concepts like custom helpers, modifiers, and services by providing a consistent pattern for expressing values that have lifecycles and may require cleanup when their owner is destroyed. +Resources are a reactive primitive that enables managing stateful processes with cleanup logic as reactive values. They unify concepts like custom helpers, modifiers, components, and services by providing a consistent pattern for expressing values that have lifecycles and may require cleanup when their owner is destroyed. ## Motivation @@ -27,7 +27,9 @@ Ember's current Octane programming model provides excellent primitives for react Today, Ember developers must navigate multiple, disconnected systems for managing setup and teardown: -**Components** use lifecycle hooks like `willDestroy()`: +
Components + +have lifecycle hook: `willDestroy()`: ```js export default class TimerComponent extends Component { @tracked time = new Date(); @@ -44,21 +46,62 @@ export default class TimerComponent extends Component { } ``` -**Modifiers** require implementing `destroyModifier()` in their manager: +
+ +
Modifiers + +two different approaches from `ember-modifier` for class-based and function based modifiers. + +class-based: ```js -class OnModifier extends Modifier { +import Modifier from 'ember-modifier'; + +class foo extends Modifier { constructor() { super(...arguments); - registerDestructor(this, cleanup); // Different pattern entirely - } - - modify(element, [event, handler]) { - this.addEventListener(element, event, handler); + registerDestructor(this, cleanup); } } ``` -**Helpers** split between class-based and function-based approaches with no built-in cleanup: +function-based: +```js +import { modifier } from 'ember-modifier'; + +const foo = modifier((element) => { + + return () => { + /* this is the destructor */ + } +}) +``` + +the function-based modifier here is _almost_ like a resource. + +There is a another modifier implementation availabel from a community library, [reactiveweb](https://github.com/universal-ember/reactiveweb), +which looks like this: + +```js +import { resource } from 'ember-resources'; +import { modifier } from 'reactiveweb/resource/modifier'; + +const wiggle = modifier((element, arg1, arg2, namedArgs) => { + return resource(({ on }) => { + let animation = element.animate(/* ... */); + + on.cleanup(() => animation.cancel()); + }); +}); +``` + +The downside to this approach is, of course, 2 imports, and it doesn't solve the problem of function based modifiers not supporting generics. + +If resources were formally supported by the framework, the wrapper function would not be needed, and we could gain generics support (more on that later) + +
+ +
Helpers + ```js export default class DataHelper extends Helper { willDestroy() { @@ -72,101 +115,64 @@ export default helper(() => { }); ``` -**Services** rely on manual `willDestroy()` but have singleton lifecycle issues: +
+ + +
Services + +Services have `willDestroy`, but because they are only able to be classes, `registerDestructor` may also be used. This gives us two ways to do things, which can add to decision fatigue like all the other multi-option scenarios above. + ```js export default class WebSocketService extends Service { + constructor(...args) { + super(...args); + + registerDestructor(this, () => this.otherSocket?.close()); + } + willDestroy() { - this.socket?.close(); // Only called on app teardown + this.socket?.close(); } } ``` -**Library authors** must patch `willDestroy()` methods, leading to complex instrumentation: -```js -// From ember-concurrency's approach -function patchWillDestroy(obj) { - let oldWillDestroy = obj.willDestroy; - obj.willDestroy = function() { - if (oldWillDestroy) oldWillDestroy.call(this); - teardownTasks(this); - }; -} -``` +
-### The Destructor API: A Step Forward, But Not Enough +### The Destructor API -The `registerDestructor()` API (RFC 580) improved this situation by providing a unified way to register cleanup functions: +The `registerDestructor()` API ([RFC #580](https://github.com/emberjs/rfcs/pull/580)) improved destruction in general by providing a unified way to register cleanup functions: ```js +import { registerDestructor } from '@ember/destroyable'; // Additional import required + class MyComponent extends Component { constructor() { + // Setup happens here let timeoutId = setTimeout(() => console.log('hello'), 1000); + + // Cleanup requires manual registration registerDestructor(this, () => clearTimeout(timeoutId)); } } ``` -However, this still requires: -- Manual destructor registration scattered throughout constructors -- Separate setup and cleanup logic -- Understanding when and how to use `associateDestroyableChild()` -- Different patterns for different types of objects - -### Current Pain Points +While this remains useful as a low-level tool, and essential for adding destruction in custom classes today, it's not the most ergonomic for the common use case: -**1. Fragmented Setup/Teardown Patterns** -Every Ember construct handles lifecycle differently: -- Components: `constructor()` + `willDestroy()` -- Modifiers: `installModifier()` + `destroyModifier()` -- Helpers: No standard lifecycle pattern -- Services: Singleton-only `willDestroy()` -- Custom classes: Manual `registerDestructor()` calls +- `registerDestructor()` requires an additional import, adding friction to every file that needs cleanup logic. +- Understanding when and how to use `associateDestroyableChild()` (see the [link RFC #1067](https://github.com/emberjs/rfcs/pull/1067)) +- Different patterns for different types of objects / classes / lifecycles -**2. Scattered Lifecycle Logic** -Setup and cleanup are separated across different methods and files: -```js -export default class DataLoader extends Component { - @tracked url; - controller = null; +### Outside of cleanup - constructor() { - super(...arguments); - // Setup happens here - registerDestructor(this, () => this.controller?.abort()); - } - - @cached - get request() { - // More setup happens here - this.controller?.abort(); - this.controller = new AbortController(); - return fetch(this.url, { signal: this.controller.signal }); - } - - willDestroy() { - // Manual cleanup here too - this.controller?.abort(); - } -} -``` - -**3. No Standard Container for Stateful Logic** -Developers lack a consistent way to encapsulate stateful processes with cleanup. This leads to: +There isn't a cohesive / consistent way to encapsulate state with cleanup. This leads to: - Logic scattered across multiple lifecycle hooks - Difficulty extracting and reusing patterns -- Testing challenges that require instantiating entire components +- Not knowing how to properly associate children with `associateDestroyableChild()` - Memory leaks from forgotten cleanup -**4. Cognitive Overhead from Multiple Patterns** -Developers must learn and remember: -- When to use `willDestroy()` vs `registerDestructor()` -- How helper managers differ from modifier managers -- Which constructs support lifecycle and which don't -- How to properly associate children with `associateDestroyableChild()` - ### Resources: A Unified Solution -Resources solve these problems by providing a **single, consistent container for setup and teardown logic** that works across all contexts. Instead of learning multiple lifecycle patterns, developers work with one unified primitive: +Resources solve these problems by providing a **single, consistent container for setup and teardown logic** that works across all contexts. Instead of learning multiple[^14-competing-standards] lifecycle patterns, developers work with one unified primitive: ```js const Clock = resource(({ on }) => { @@ -180,27 +186,14 @@ const Clock = resource(({ on }) => { }); ``` +[^14-competing-standards]: This is "yet another competing standard", but over time we can unify on this concept -- but the plan for doing so is out of scope for this RFC. This RFC is focused on unifying low-level concepts, and end-users of the framework may choose to continue not using resources directly. See also [XKCD 927](https://xkcd.com/927/). + **Resources unify all existing concepts** by providing: -1. **A Standard Container**: One primitive that works for components, helpers, modifiers, services, and custom logic +1. **A Standard Container**: One primitive that works for components, helpers, modifiers, services, and custom logic (more on this later) 2. **Co-located Setup/Teardown**: No more spreading logic across constructors, lifecycle hooks, and destructor registrations 3. **Automatic Cleanup Management**: No need to manually call `registerDestructor()` or remember `willDestroy()` -4. **Consistent Patterns**: Same API whether you're building a helper, managing component state, or creating reusable logic -5. **Hierarchical Cleanup**: Automatic resource disposal and child management without manual `associateDestroyableChild()` - -**Before (fragmented patterns):** -- Component lifecycle hooks (`willDestroy`) -- Manual destructor registration (`registerDestructor`) -- Manager-specific cleanup (`destroyModifier`, helper teardown) -- Scattered setup/teardown logic -- Different APIs for every construct - -**After (unified with resources):** -- Single `resource()` function for all stateful logic -- Co-located setup and cleanup via `on.cleanup()` -- Automatic lifecycle management -- Consistent patterns across all use cases -- No more manual lifecycle management +5. **Hierarchical Cleanup**: Automatic owership linkage and disposal and child management without manual `associateDestroyableChild()` + `registerDestructor` (also with a way to manually link, similar to [RFC #1067](https://github.com/emberjs/rfcs/pull/1067)) Resources don't replace existing patterns—they **unify them under a single, powerful abstraction** that eliminates the need to choose between different lifecycle approaches or remember multiple APIs. Whether you're building a component helper, managing WebSocket connections, or creating reusable business logic, resources provide the same elegant pattern for setup and teardown. @@ -1742,3 +1735,11 @@ Should there be automated codemods to help migrate from existing patterns (class ### Bundle Size Impact What is the impact on bundle size for applications that don't use resources? Can the implementation be designed to be tree-shakeable? + +## References + +- Other low-level primitives + - [RFC #1071: cell](https://github.com/emberjs/rfcs/pull/1071) + - [RFC #1067: link](https://github.com/emberjs/rfcs/pull/1067) +- Related(ish) / would be influenced by resources + - [RFC #502: Explicit Service Injection](https://github.com/emberjs/rfcs/pull/502) \ No newline at end of file From f881e52d3a5123b97f7b0b69570ce740df8bdd9f Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:24:07 -0400 Subject: [PATCH 07/27] wip --- text/0000-resources.md | 68 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index ca20da0967..b9c7581219 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -192,8 +192,7 @@ const Clock = resource(({ on }) => { 1. **A Standard Container**: One primitive that works for components, helpers, modifiers, services, and custom logic (more on this later) 2. **Co-located Setup/Teardown**: No more spreading logic across constructors, lifecycle hooks, and destructor registrations -3. **Automatic Cleanup Management**: No need to manually call `registerDestructor()` or remember `willDestroy()` -5. **Hierarchical Cleanup**: Automatic owership linkage and disposal and child management without manual `associateDestroyableChild()` + `registerDestructor` (also with a way to manually link, similar to [RFC #1067](https://github.com/emberjs/rfcs/pull/1067)) +3. **Hierarchical Cleanup**: Automatic owership linkage and disposal and child management without manual `associateDestroyableChild()` + `registerDestructor` (also with a way to manually link, similar to [RFC #1067](https://github.com/emberjs/rfcs/pull/1067)) Resources don't replace existing patterns—they **unify them under a single, powerful abstraction** that eliminates the need to choose between different lifecycle approaches or remember multiple APIs. Whether you're building a component helper, managing WebSocket connections, or creating reusable business logic, resources provide the same elegant pattern for setup and teardown. @@ -201,7 +200,10 @@ Resources don't replace existing patterns—they **unify them under a single, po To illustrate how resources unify existing concepts, here are small examples showing how traditional Ember constructs could be reimagined using resources. Note that **classes are user-defined** while **resources are framework-internal** - this separation allows resources to wrap and manage existing class-based patterns. -**Services → Resources** +
Services + +Using existing services, keeping the same behavior: + ```js // User-defined Service class (unchanged) class SessionService extends Service { @@ -222,7 +224,7 @@ class SessionService extends Service { // Framework-internal resource wrapper const SessionServiceResource = resource(({ on, owner }) => { - const service = new SessionService(); + const service = new SessionService(owner); // Framework manages lifecycle via resource pattern on.cleanup(() => service.willDestroy()); @@ -231,6 +233,64 @@ const SessionServiceResource = resource(({ on, owner }) => { }); ``` +A hypothetical new way to use services without the string registry (exact details and implementation is outside the scope of this RFC) + +```js +class Storage { + foo = 2; +} + +// Usage +class MyComponent extends Component { + storage = service(this, Storage); + + get data() { + // no proxy + return this.storage.current.foo; + // with proxy + return this.storage.foo; + } +} + +// where service is +function service(context, klass) { + const manager = resource(({ on, link, owner }) => { + let instance = new klass(owner); + // sets up destroyable linking to the owner + // without passing owner, the instance would be linked to the lifetime of the context (the component in this example) + link(instance, owner); + // not needed due to `link`. Any registerDestructor setup in the passed klass will just work + on.cleanup(() => /* call wilLDestroy? */) + + return instance; + }); + + return use(context, manager); +} +``` +however, without using a proxy to kick off all the instantiation of the service (since we still want service instantiation to be lazy), it may be better to support something like this: + + +```js +class MyComponent extends Component { + // the use decorator turns the property in to a cached getter that is lazily evaluated upon access -- the only requirement + // is that the right-hand side has a registered helper manager + // + // in addition, @use also has access to the instance / this. So passing to `service` is not needed. + // + // NOTE: TypeScript still does not allow decorators (even the decorators that are landing in browsers) to alter the type on the right-hand side of the equals + @use accessor storage = service(Storage); + + get data() { + return this.storage.foo; + } +} +``` + +See also: [RFC: #502 | Explicit Service Injection](https://github.com/emberjs/rfcs/pull/502) + +
+ **Helpers → Resources** ```js // User-defined Helper class (unchanged) From d13222ed94870d6b42ab5079ed08fdad8ad727d2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:28:48 -0400 Subject: [PATCH 08/27] There is a fair bit of duplication in here --- text/0000-resources.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index b9c7581219..26f593f678 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -291,22 +291,32 @@ See also: [RFC: #502 | Explicit Service Injection](https://github.com/emberjs/rf -**Helpers → Resources** +
Helpers + +Resources _are_ helpers, so while this is not needed exactly (as we have the helper manager), we could look at helpers like this: + ```js // User-defined Helper class (unchanged) class FormatDateHelper extends Helper { + @service intl; + willDestroy() { this.intl?.off('localeChanged', this.recompute); } compute([date]) { - return new Intl.DateTimeFormat().format(date); + if (!this.isSetup) { + this.intl.on('localeChanged', this.recompute); + this.isSetup = true; + } + + return this.intl.formatDate(date); } } // Framework-internal resource wrapper const FormatDateResource = resource(({ on, owner }) => { - const helper = new FormatDateHelper(); + const helper = new FormatDateHelper(owner); const intl = owner.lookup('service:intl'); helper.intl = intl; @@ -317,6 +327,8 @@ const FormatDateResource = resource(({ on, owner }) => { }); ``` +
+ **Modifiers → Resources** ```js // User-defined Modifier class (unchanged) From ef2fd0cd0792bcb70c93d0a8d1f2036ee1617c23 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:42:02 -0400 Subject: [PATCH 09/27] Progress --- text/0000-resources.md | 145 ++++++++++++++--------------------------- 1 file changed, 50 insertions(+), 95 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index 26f593f678..aab55a158e 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -299,103 +299,54 @@ Resources _are_ helpers, so while this is not needed exactly (as we have the hel // User-defined Helper class (unchanged) class FormatDateHelper extends Helper { @service intl; - - willDestroy() { - this.intl?.off('localeChanged', this.recompute); - } compute([date]) { - if (!this.isSetup) { - this.intl.on('localeChanged', this.recompute); - this.isSetup = true; - } - return this.intl.formatDate(date); } } +``` -// Framework-internal resource wrapper -const FormatDateResource = resource(({ on, owner }) => { - const helper = new FormatDateHelper(owner); - const intl = owner.lookup('service:intl'); - helper.intl = intl; - - // Framework manages lifecycle via resource pattern - on.cleanup(() => helper.willDestroy()); - - return (date) => helper.compute([date]); -}); +an equiv resource-based helper would look like this: +```js +function formatDate(date) { + return resource(({ on, owner }) => { + let intl = owner.lookup('service:intl'); + + return () => + // entangles with the intl's locale + intl.formatdate(date); + }); +} ``` -**Modifiers → Resources** -```js -// User-defined Modifier class (unchanged) -class OnModifier extends Modifier { - modify(element, [event, handler]) { - this.element = element; - this.event = event; - this.handler = handler; - element.addEventListener(event, handler); - } - - willDestroy() { - this.element?.removeEventListener(this.event, this.handler); - } -} -// Framework-internal resource wrapper -const OnModifierResource = resource(({ on }) => { - return (element, [event, handler]) => { - const modifier = new OnModifier(); - modifier.modify(element, [event, handler]); - - // Framework manages lifecycle via resource pattern - on.cleanup(() => modifier.willDestroy()); - }; -}); -``` +
Components -**Components → Resources** -```js -// User-defined Timer logic class (unchanged) -class TimerLogic { - constructor() { - this.seconds = 0; - this.interval = setInterval(() => this.seconds++, 1000); - } - - willDestroy() { - clearInterval(this.interval); - } +```gjs +class Demo extends Component { + } // Framework-internal resource wrapper -const TimerResource = resource(({ on }) => { - const timer = new TimerLogic(); - const seconds = cell(timer.seconds); - - // Sync timer.seconds to cell on each tick - const syncInterval = setInterval(() => seconds.set(timer.seconds), 1000); - - // Framework manages lifecycle via resource pattern - on.cleanup(() => { - clearInterval(syncInterval); - timer.willDestroy(); - }); - - return { seconds }; -}); +function InternalInvoker(Component, argsProxy) { + return resource(({ on, owner, link }) => { + const instance = new Component(owner, argsProxy); -// Component uses resource -export default class TimerComponent extends Component { - @use timer = TimerResource; - - + link(instance); + + if ('willDestroy' in instance) { + on.cleanup(() => instance.willDestroy()); + } + + return instance; + }); } ``` +
+ **Routes → Resources** ```js // User-defined Route class (unchanged) @@ -410,15 +361,19 @@ class PostRoute extends Route { } // Framework-internal resource wrapper -const PostRouteResource = resource(({ on, owner }) => { - const route = new PostRoute(); - route.store = owner.lookup('service:store'); - - // Framework manages lifecycle via resource pattern - on.cleanup(() => route.willDestroy()); - - return route; -}); +function InternalInvoker(Route) { + return resource(({ on, owner, link }) => { + const instance = new Route(owner); + + link(instance); + + if ('willDestroy' in instance) { + on.cleanup(() => instance.willDestroy()); + } + + return instance; + }); +} ``` **The Power of Unified Patterns** @@ -466,6 +421,7 @@ interface ResourceAPI { cleanup: (destructor: () => void) => void; }; use: (resource: T) => ReactiveValue; + link: (obj: unknown, parent?: obj: unknown) => void; owner: Owner; } @@ -536,8 +492,7 @@ const Clock = resource(({ on }) => { @use clock = Clock; // Equivalent to: clock = Clock() // Without @use, you need explicit invocation or .current access -clock = use(this, Clock); // Returns reactive value, need .current -clock = Clock(); // Direct invocation in template context +clock = use(this, Clock); // Returns object with tracked property: clock.current ``` The `@use` decorator pattern extends beyond resources to work with any construct that has registered a helper manager. This means that future primitives that integrate with the helper manager system (like certain kinds of computed values, cached functions, or other reactive constructs) will automatically work with `@use` without any changes to the decorator itself. @@ -711,19 +666,19 @@ function use(resource: Resource): PropertyDecorator; ### Relationship to the Cell Primitive -While the `cell` primitive (RFC 1071) is not strictly required for resources to function, resources are significantly more ergonomic and powerful when used together with `cell`. Resources can work with Ember's existing `@tracked` properties, but `cell` provides several advantages: +While the `cell` primitive ([RFC #1071](https://github.com/emberjs/rfcs/pull/1071)) is not strictly required for resources to function, resources are significantly more ergonomic and powerful when used together with `cell`. Resources can work with Ember's existing `@tracked` properties, but `cell` provides several advantages: **Without `cell` (using `@tracked`):** ```js import { resource } from '@ember/reactive'; import { tracked } from '@glimmer/tracking'; +// Must create a separate class to hold tracked state +class ClockState { + @tracked time = new Date(); +} + const Clock = resource(({ on }) => { - // Must create a separate class to hold tracked state - class ClockState { - @tracked time = new Date(); - } - const state = new ClockState(); const timer = setInterval(() => { From 73e85cfb46c8e4e6cef18c220e3defa91532e974 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:44:09 -0400 Subject: [PATCH 10/27] ope --- text/0000-resources.md | 140 ++++++++++++++++++++++++----------------- 1 file changed, 82 insertions(+), 58 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index aab55a158e..f2e191e8dc 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -196,7 +196,7 @@ const Clock = resource(({ on }) => { Resources don't replace existing patterns—they **unify them under a single, powerful abstraction** that eliminates the need to choose between different lifecycle approaches or remember multiple APIs. Whether you're building a component helper, managing WebSocket connections, or creating reusable business logic, resources provide the same elegant pattern for setup and teardown. -### Conceptual Unification Examples +### Examples To illustrate how resources unify existing concepts, here are small examples showing how traditional Ember constructs could be reimagined using resources. Note that **classes are user-defined** while **resources are framework-internal** - this separation allows resources to wrap and manage existing class-based patterns. @@ -291,6 +291,32 @@ See also: [RFC: #502 | Explicit Service Injection](https://github.com/emberjs/rf +
Components, Routes, Services, (anything with willDestroy) + +```gjs +class Demo extends Component { + +} + +// Framework-internal resource wrapper +function InternalInvoker(Component, argsProxy) { + return resource(({ on, owner, link }) => { + const instance = new Component(owner, argsProxy); + + link(instance); + + if ('willDestroy' in instance) { + on.cleanup(() => instance.willDestroy()); + } + + return instance; + }); +} +``` + +
+ +
Helpers Resources _are_ helpers, so while this is not needed exactly (as we have the helper manager), we could look at helpers like this: @@ -313,7 +339,6 @@ function formatDate(date) { let intl = owner.lookup('service:intl'); return () => - // entangles with the intl's locale intl.formatdate(date); }); } @@ -321,79 +346,78 @@ function formatDate(date) {
+
DOM Rendering -
Components - -```gjs -class Demo extends Component { - -} -// Framework-internal resource wrapper -function InternalInvoker(Component, argsProxy) { - return resource(({ on, owner, link }) => { - const instance = new Component(owner, argsProxy); +Instead of the VM, we could use resources to manage the insertion and removal of known dynamic nodes in the DOM: - link(instance); - - if ('willDestroy' in instance) { - on.cleanup(() => instance.willDestroy()); - } - - return instance; - }); +```gjs +// Let's pretend we are rendering a simple counter: +const count = cell(0); +const increment = () => count.current++; + + + +// render could be responsible for directly managing the create/destroy +// / invocation/cleanup +// each node in this array will be wrapped in a resource if cleanup is possibly needed, such as the case of if blocks +export default component([ + () => count.current, + [createElement, 'button' { + // the arrow function is for the chance that the value might by reactive + onclick: () => increment, + }, ["++"]], + [condition, () => isEven(count.current), ['EVEN!']], +]); + +// a hypothetical component() +function component(instructions) { + return resource(({ on, use }) => { + let fragment = document.createDocumentFragment(); + + on.cleanup() => fragment.remove(); + + // iterate over instructions, + // if any instructions are resoures, they'll be `use`d. + // each `use`d thing has its own cache / begin/end tracking frame pair. + + // caller of component() will append fragment + return fragment; + }) } -``` -
+// a hypothetical if evaluator +function condition(evaluate, whenTrue, whenFalse) { + return resource(({ on, use }) => { + let fragment = document.createDocumentFragment(); -**Routes → Resources** -```js -// User-defined Route class (unchanged) -class PostRoute extends Route { - model(params) { - return this.store.findRecord('post', params.id); - } - - willDestroy() { - this.controller?.abort(); - } -} - -// Framework-internal resource wrapper -function InternalInvoker(Route) { - return resource(({ on, owner, link }) => { - const instance = new Route(owner); + on.cleanup() => fragment.remove(); - link(instance); - - if ('willDestroy' in instance) { - on.cleanup(() => instance.willDestroy()); - } - - return instance; - }); + return () => { + let contents = evalute() ? whenTrue() : whenFalse(); + fragment.append(contents); + return fragment; + } + }); } ``` -**The Power of Unified Patterns** +> [!NOTE] these examples are are hypothetical and non-functional today. They are strictly for illustration. -These examples demonstrate several key points: +
-1. **Clear Separation of Concerns**: User-defined classes contain domain logic, while framework-internal resources handle lifecycle management -2. **Migration Path**: Existing classes can be wrapped with resources without modification -3. **Consistent Setup/Teardown**: Always use `on.cleanup()` instead of different lifecycle hooks -4. **Automatic Memory Management**: No need to remember different cleanup patterns across constructs -5. **Composable Logic**: Resources can be easily combined and tested in isolation -6. **Uniform Mental Model**: Same patterns whether wrapping helpers, modifiers, services, or routes -This unification eliminates the cognitive overhead of learning multiple lifecycle APIs while preserving existing class-based patterns. Resources become the framework's internal mechanism for managing lifecycle, while users continue working with familiar class structures. +This unification hopefully will lead to simplification of the implmentation of all our concepts. ## Detailed design ### Overview -A **resource** is a reactive function that represents a value with lifecycle and optional cleanup logic. Resources are created using the `resource()` function and automatically manage their lifecycle through Ember's existing destroyable system. +A **resource** is a reactive function that represents a value with lifecycle and optional cleanup logic. Resources are created using the `resource()` function and automatically manage their lifecycle through Ember's existing destroyable system, though the exact implementation could change at any time (to be simpler) as we work at simplifying the internals. ```js import { cell, resource } from '@ember/reactive'; @@ -1769,4 +1793,4 @@ What is the impact on bundle size for applications that don't use resources? Can - [RFC #1071: cell](https://github.com/emberjs/rfcs/pull/1071) - [RFC #1067: link](https://github.com/emberjs/rfcs/pull/1067) - Related(ish) / would be influenced by resources - - [RFC #502: Explicit Service Injection](https://github.com/emberjs/rfcs/pull/502) \ No newline at end of file + - [RFC #502: Explicit Service Injection](https://github.com/emberjs/rfcs/pull/502) From 1cfc7aa3491d0c202f8d2131ee194a147fa72c3b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:49:18 -0400 Subject: [PATCH 11/27] Mermaid dependencies --- text/0000-resources.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/text/0000-resources.md b/text/0000-resources.md index f2e191e8dc..78c0a3235e 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -19,6 +19,37 @@ suite: Resources are a reactive primitive that enables managing stateful processes with cleanup logic as reactive values. They unify concepts like custom helpers, modifiers, components, and services by providing a consistent pattern for expressing values that have lifecycles and may require cleanup when their owner is destroyed. + +> [!NOTE] +> This RFC has some dependencies / relation with other RFCs + +```mermaid +graph LR; + use["@use"]; + cell["cell from RFC #1071"] + resource["resource"] + import["import { ... } from '@ember/reactive';"]; + rfc-1038["RFC #1068 (already accepted)"]; + + use -- makes using resources easier --> resource + cell --> import + cell -- convenience for one-off state in --> resource + + subgraph built-ins["Tracked built-ins built in"] + import --> rfc-1038 + end + + subgraph introduced-here["Introduced in this RFC"] + use --> import + resource --> import + end + + + click rfc-1038 "https://github.com/emberjs/rfcs/pull/1068" "RFC for tracked-built-ins being bulit-in" + click Cell "https://github.com/emberjs/rfcs/pull/1071" "RFC for new low-level primitive for state" + click Resource "https://github.com/emberjs/rfcs/pull/todo-not-yet-submitted" "This RFC" +``` + ## Motivation Ember's current Octane programming model provides excellent primitives for reactive state (`@tracked`), declarative rendering (templates), and component lifecycle, but it lacks a unified primitive for managing stateful processes that need cleanup. This fragmentation has created a complex ecosystem where different concepts each require their own approach to lifecycle management, leading to scattered patterns and cognitive overhead. From 9d73c30288e241807eab86cd9a87a24b10b3225e Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:16:55 -0400 Subject: [PATCH 12/27] prose --- text/0000-resources.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index 78c0a3235e..81c19a4e56 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -54,9 +54,9 @@ graph LR; Ember's current Octane programming model provides excellent primitives for reactive state (`@tracked`), declarative rendering (templates), and component lifecycle, but it lacks a unified primitive for managing stateful processes that need cleanup. This fragmentation has created a complex ecosystem where different concepts each require their own approach to lifecycle management, leading to scattered patterns and cognitive overhead. -### The Fragmented Landscape of Lifecycle Management +### Lifecycle Management -Today, Ember developers must navigate multiple, disconnected systems for managing setup and teardown: +Today, Ember developers must be aware of multiple systems for managing setup and teardown:
Components @@ -201,7 +201,7 @@ There isn't a cohesive / consistent way to encapsulate state with cleanup. This - Not knowing how to properly associate children with `associateDestroyableChild()` - Memory leaks from forgotten cleanup -### Resources: A Unified Solution +### A unified approach to lifecycle management in one package (function) Resources solve these problems by providing a **single, consistent container for setup and teardown logic** that works across all contexts. Instead of learning multiple[^14-competing-standards] lifecycle patterns, developers work with one unified primitive: @@ -225,7 +225,7 @@ const Clock = resource(({ on }) => { 2. **Co-located Setup/Teardown**: No more spreading logic across constructors, lifecycle hooks, and destructor registrations 3. **Hierarchical Cleanup**: Automatic owership linkage and disposal and child management without manual `associateDestroyableChild()` + `registerDestructor` (also with a way to manually link, similar to [RFC #1067](https://github.com/emberjs/rfcs/pull/1067)) -Resources don't replace existing patterns—they **unify them under a single, powerful abstraction** that eliminates the need to choose between different lifecycle approaches or remember multiple APIs. Whether you're building a component helper, managing WebSocket connections, or creating reusable business logic, resources provide the same elegant pattern for setup and teardown. +Resources don't replace existing patterns. Resources unify those patterns under a single abstraction that eliminates the need to choose between different lifecycle approaches or remember multiple APIs. ### Examples From fcdf77c66c129773b3d187753bb151fe205da196 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:36:34 -0400 Subject: [PATCH 13/27] Need more examples --- text/0000-resources.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index 81c19a4e56..3a897f3599 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -448,9 +448,9 @@ This unification hopefully will lead to simplification of the implmentation of a ### Overview -A **resource** is a reactive function that represents a value with lifecycle and optional cleanup logic. Resources are created using the `resource()` function and automatically manage their lifecycle through Ember's existing destroyable system, though the exact implementation could change at any time (to be simpler) as we work at simplifying the internals. +A **resource** is a reactive function that represents a value with lifecycle and optional cleanup logic. Resources are created using the `resource()` function and automatically manage their lifecycle through Ember's existing destroyable system, though the exact implementation could change at any time (to be simpler) as we work at simplifying a bunch of internals. -```js +```gjs import { cell, resource } from '@ember/reactive'; const Clock = resource(({ on }) => { @@ -464,8 +464,36 @@ const Clock = resource(({ on }) => { return time; }); + + + {{Clock}} + ``` +
Ignoring the Value, using for the lifecycle management -- synchronizing external state + +```gjs +import { cell, resource } from '@ember/reactive'; + +function addScript(url) { + return resource(({ on }) => { + let el = document.createElement('script'); + + on.cleanup(() => el.remove()); + + Object.assign(el, { src: url }); + }); +} + + +``` + +
+ ### Core API The `resource()` function takes a single argument: a function that receives a ResourceAPI object and returns a reactive value. From 2dc0ce123913757e45f247931bd21b9aab10f57e Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:44:57 -0400 Subject: [PATCH 14/27] stubs --- text/0000-resources.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/text/0000-resources.md b/text/0000-resources.md index 3a897f3599..9cc32780e8 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -513,6 +513,18 @@ type ResourceFunction = (api: ResourceAPI) => T; function resource(fn: ResourceFunction): Resource ``` +#### `on.cleanup` + + +#### `use` + +#### `link` + + +#### `owner` + + + ### Resource Creation and Usage Resources can be used in several ways: From 4d8436b7c803ced056cb32422138a173dfdace81 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 10 Jul 2025 19:29:52 -0400 Subject: [PATCH 15/27] Cleanup drawbacks and Alternatives --- text/0000-resources.md | 343 +---------------------------------------- 1 file changed, 7 insertions(+), 336 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index 9cc32780e8..26e6531d1a 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -1512,351 +1512,22 @@ These patterns demonstrate how resources can be composed to build sophisticated ## Drawbacks -### Learning Curve +- The function coloring problem, described here [TC39 Signals Issue#30](https://github.com/tc39/proposal-signals/issues/30). + This problem is not new to us -- nor is it new to resources or JavaScript in general -- odds are ember developers have to deal with this already. The most common analogy here is that if one function goes from sync to async, all the functions higher in the callstack also need to convert from sync to async if they weren't async already, _and then_ developers also may need to be concerned with the varying states of async (error, load, cache, etc). -**Additional Abstraction**: Resources introduce a new primitive that developers must learn. While they simplify many patterns, they add to the initial cognitive load for new Ember developers. +- [ember-resources](https://github.com/NullVoxPopuli/ember-resources) has been around a while, and there has been _some_ over use. The most common thing folks have accidentaly assumed (which could have been a documentation issue!) is that resources were a blessed pathway to side-effecting code. This is not the case (as of this RFC, anyway). There are future plans for an `on.sync()` later, but it needs more planning, and some deep work in the rendering and reactivity handling layers of ember. `on.sync()` is called such because the intent is no "syrchronize external state". There are other utilities folks can use for this purpose though that don't require framework integration. There is `{{ (functianCallHere) }}`, and with test-waiter-integration in [reactiveweb](https://reactive.nullvoxpopuli.com/modules/effect.html) (a community library) and also [`sync`](https://reactive.nullvoxpopuli.com/functions/sync.sync.html). -**Advanced Reactive Concepts**: Resources embody sophisticated reactivity concepts (push-pull evaluation, reactive ownership, lazy vs scheduled evaluation) that may be overwhelming for developers new to reactive programming. - -**Multiple Ways to Do Things**: Resources provide another way to manage state and side effects, potentially creating confusion about when to use components vs. services vs. resources. - -**Mental Model Shifts**: Developers must learn to think in terms of "what can be derived, should be derived" and understand concepts like glitch-free consistency and automatic ownership disposal. - -### Performance Considerations - -**Memory Overhead**: Each resource instance maintains its own cache and cleanup tracking, which could increase memory usage in applications with many resource instances. - -**Re-computation**: Resources re-run their entire function when tracked dependencies change, which could be less efficient than more granular update patterns. - -### Migration and Ecosystem Impact - -**Existing Patterns**: The addition of resources doesn't make existing patterns invalid, but it might create inconsistency in codebases during transition periods. - -**Addon Ecosystem**: Addons will need time to adopt resource patterns, potentially creating a split between "old" and "new" style addons. - -**Testing Infrastructure**: Existing testing patterns and helpers may not work optimally with resources, requiring new testing utilities and patterns. - -### TypeScript Complexity - -**Advanced Types**: The resource type system, especially around composition with `use()`, involves complex TypeScript patterns that may be difficult for some developers to understand or extend. - -**Decorator Limitations**: The `@use` decorator may not provide optimal TypeScript inference in all scenarios due to limitations in decorator typing. +- Another thing that has come up with resources being in library land so far is managing when reactivity occurs. In templates, we have a lot of implicit laziness -- but in JavaScript we don't have that. For example ## Alternatives -### Class-Based Resources - -Instead of function-based resources, we could provide a class-based API similar to the current ember-resources library: - -```js -class TimerResource extends Resource { - @tracked time = new Date(); - - setup() { - this.timer = setInterval(() => { - this.time = new Date(); - }, 1000); - } - - teardown() { - clearInterval(this.timer); - } -} -``` - -**Pros**: More familiar to developers used to class-based patterns -**Cons**: More verbose, harder to compose, doesn't leverage functional programming benefits - -### Built-in Helpers with Lifecycle - -Extend the existing helper system to support lifecycle hooks: - -```js -export default class TimerHelper extends Helper { - @tracked time = new Date(); - - didBecomeActive() { - this.timer = setInterval(() => { - this.time = new Date(); - }, 1000); - } - - willDestroy() { - clearInterval(this.timer); - } - - compute() { - return this.time; - } -} -``` - -**Pros**: Builds on existing helper system -**Cons**: Limited to helper use cases, doesn't solve composition or broader lifecycle management +- We _could_ do nothing and just encourage [standard explicit resources management](https://v8.dev/features/explicit-resource-management) everywhere -- though they have more limitide functionality, and probably would still want a wrapper to make accessing the owner and handling linkage nicer. Note that that we absolutely should just make explicit resource management work everywhere anyway. It likely would not even need an RFC, as we could treat those as a limited version of the Resource proposed in this RFC, with the same timing, etc. When to support this is maybe more of a question of our browser support policy however. Polyfills work in userland, but as a framework we need access to the _same_ `Symbol.dispose` (and `Symbol.asyncDispose`) that userland folks would have access to. -### Enhanced Modifiers - -Expand the modifier system to handle more use cases: - -```js -export default class DataModifier extends Modifier { - modify(element, [url]) { - // Fetch data and update element - } -} -``` - -**Pros**: Builds on existing modifier system -**Cons**: Still limited to DOM-centric use cases, doesn't solve general stateful logic management - -### Service-Based Solutions - -Encourage using services for all stateful logic: - -```js -export default class TimerService extends Service { - @tracked time = new Date(); - - startTimer() { - this.timer = setInterval(() => { - this.time = new Date(); - }, 1000); - } - - stopTimer() { - clearInterval(this.timer); - } -} -``` - -**Pros**: Uses existing patterns -**Cons**: Too heavyweight for localized state, poor cleanup semantics, singleton limitations - -### No Action (Status Quo) - -Continue with current patterns and encourage better use of existing primitives. - -**Pros**: No additional complexity or learning curve -**Cons**: Doesn't solve the identified problems, perpetuates scattered lifecycle management - -## How we teach this - -### Terminology and Naming - -The term **"resource"** was chosen because: - -1. It accurately describes something that requires management (like memory, network connections, timers) -2. It's already familiar to developers from other contexts (system resources, web resources) -3. It emphasizes the lifecycle aspect - resources are acquired and must be cleaned up -4. It unifies existing concepts without deprecating them - -**Key terms:** -- **Resource**: A reactive function that manages a stateful process with cleanup -- **Resource Function**: The function passed to `resource()` that defines the behavior -- **Resource Instance**: The instantiated resource tied to a specific owner -- **Resource API**: The object passed to resource functions containing `on`, `use`, and `owner` - -### Teaching Strategy - -**1. Progressive Enhancement of Existing Patterns** - -Introduce resources as a natural evolution of patterns developers already know: - -```js -// Familiar pattern - manual lifecycle management -export default class extends Component { - @tracked time = new Date(); - - constructor() { - super(...arguments); - this.timer = setInterval(() => { - this.time = new Date(); - }, 1000); - } - - willDestroy() { - clearInterval(this.timer); - } -} - -// Resource pattern - automatic lifecycle management -const Clock = resource(({ on }) => { - const time = cell(new Date()); - const timer = setInterval(() => time.set(new Date()), 1000); - on.cleanup(() => clearInterval(timer)); - return time; -}); -``` - -**2. Emphasize Modern Reactive Principles** - -Lead with the foundational principles that make resources powerful: - -- **"What can be derived, should be derived"**: Minimize state, maximize derivation -- **Automatic cleanup**: No more manual lifecycle management -- **Glitch-free consistency**: Never observe inconsistent intermediate states -- **Lazy by default**: Expensive operations only run when needed -- **Composable ownership**: Resources clean up their children automatically - -**3. Start with Simple Examples, Build to Advanced Patterns** - -```js -// Level 1: Basic resource with cleanup -const SimpleTimer = resource(({ on }) => { - let count = cell(0); - let timer = setInterval(() => count.set(count.current + 1), 1000); - on.cleanup(() => clearInterval(timer)); - return count; -}); - -// Level 2: Async data fetching -const UserData = resource(({ on }) => { - const state = cell({ loading: true }); - const controller = new AbortController(); - - fetch('/api/user', { signal: controller.signal }) - .then(r => r.json()) - .then(data => state.set({ loading: false, data })); - - on.cleanup(() => controller.abort()); - return state; -}); - -// Level 3: Resource composition and derivation -const Dashboard = resource(({ use }) => { - const timer = use(SimpleTimer); - const userData = use(UserData); - - // Derived state - always consistent, no manual synchronization - return () => ({ - uptime: timer.current, - user: userData.current, - // This derivation is glitch-free - greeting: userData.current.loading - ? 'Loading...' - : `Welcome back, ${userData.current.data.name}!` - }); -}); -``` - -**4. Address Common Mental Models** - -Help developers understand how resources differ from familiar patterns: - -| Pattern | Manual Management | Resource Approach | -|---------|------------------|-------------------| -| Component lifecycle | Split setup/cleanup across hooks | Co-located in single function | -| Data synchronization | Manual effects and state updates | Automatic derivation | -| Error boundaries | Try/catch in multiple places | Centralized error handling | -| Memory management | Manual cleanup tracking | Automatic ownership disposal | -| Testing | Complex component setup | Isolated resource testing | - -### Documentation Strategy - -**Ember Guides Updates** - -1. **New Section**: "Working with Resources" - - When to use resources vs components/services/helpers - - Reactive principles and best practices - - Composition patterns and testing - -2. **Enhanced Sections**: - - Update "Managing Application State" to include resource patterns - - Add resource examples to "Handling User Interaction" - - Include advanced reactive concepts in "Data Flow and Reactivity" - -**API Documentation** - -- Complete API reference for `resource()`, `use()`, and ResourceAPI -- TypeScript definitions with comprehensive JSDoc comments -- Interactive examples for each major use case -- Performance guidelines and best practices - -**Migration Guides** - -- How to convert class-based helpers to resources -- Converting component lifecycle patterns to resources -- When to use resources vs existing patterns -- Advanced patterns: async resources, error handling, composition - -### Learning Materials - -**Blog Posts and Tutorials** -- "Introducing Resources: Modern Reactive Architecture for Ember" -- "From Manual Cleanup to Automatic Ownership: Resource Lifecycle Management" -- "Building Robust Async Resources: Error Handling and Resilience Patterns" -- "Resource Composition: Building Complex Logic from Simple Parts" -- "Testing Resources: Isolation, Mocking, and Integration Patterns" - -**Interactive Tutorials** -- Ember Tutorial additions demonstrating resource usage -- Playground examples for common reactive patterns -- Step-by-step migration guides with working examples -- Advanced composition and async pattern workshops - -### Integration with Existing Learning - -Resources complement and enhance existing Ember concepts: - -- **Components**: Still the primary UI abstraction, now with better tools for managing stateful logic -- **Services**: Still used for app-wide shared state, resources handle localized stateful processes -- **Helpers**: Resources can serve as stateful helpers, while pure functions remain as regular helpers -- **Modifiers**: Resources can encapsulate modifier-like behavior with better composition -- **Tracked Properties**: Resources work with `@tracked`, but `cell` provides more ergonomic patterns - -### Advanced Concepts Introduction - -Once developers are comfortable with basic resource usage, introduce advanced reactive concepts: - -1. **Push-Pull Reactivity**: How resources participate in Ember's reactive system -2. **Lazy vs Scheduled Evaluation**: When and why resources evaluate -3. **Glitch-Free Consistency**: Why derived state is always coherent -4. **Ownership and Disposal**: How automatic cleanup prevents memory leaks -5. **Async Patterns**: Colorless async and avoiding request waterfalls - -These concepts position Ember's resource system as a modern, best-in-class reactive architecture while remaining approachable for everyday development. +- for the Async / intermediate state / function coloring problem, we could build and design a babel plugin (similar to ember-concurrency that automatically wraps async resource usage and correctly forwards intermediary state in resource definitions, but this may require TypeScript lies, which are not desirable) ## Unresolved questions -### Future Synchronization API - -While this RFC focuses on the core resource primitive, a future `on.sync` API (similar to what Starbeam provides) could enable even more powerful reactive patterns. However, this is explicitly out of scope for this RFC to keep the initial implementation focused and stable. - -### Async Resource Patterns - -Should resources support first-class async primitives (like `createAsync` from the dev.to articles) that throw promises for unresolved values? This could enable "colorless async" patterns where async and sync resources compose uniformly, but it would require significant integration with Ember's rendering system. - -### Scheduling and Evaluation Strategies - -Should resources provide explicit control over evaluation timing (immediate vs lazy vs scheduled)? While the current design uses lazy evaluation by default, certain patterns (like data fetching) might benefit from immediate or scheduled evaluation modes. - -### Error Boundaries and Resource Hierarchies - -How should errors in parent resources affect child resources? Should there be automatic error boundary mechanisms, or should error handling remain explicit through resource return values? - -### Integration with Strict Mode and Template Imports - -How should resources work with Ember's strict mode and template imports? Should resources be importable in templates directly, or only through helper invocation? - -### Performance Optimization Opportunities - -Are there opportunities to optimize resource re-computation through more granular dependency tracking or memoization strategies? - -### Testing Utilities - -What additional testing utilities should be provided in the box for resource testing? Should there be special test helpers for resource lifecycle management? - -### Debugging and Ember Inspector Integration - -How should resources appear in Ember Inspector? What debugging information should be available for resource instances and their lifecycles? - -### Migration Tooling - -Should there be automated codemods to help migrate from existing patterns (class helpers, certain component patterns) to resources? - -### Bundle Size Impact - -What is the impact on bundle size for applications that don't use resources? Can the implementation be designed to be tree-shakeable? +n/a ## References From ede7fa1be5ff2f7881a50e159db8a0adeb9820dc Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:19:33 -0400 Subject: [PATCH 16/27] Moar --- text/0000-resources.md | 280 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) diff --git a/text/0000-resources.md b/text/0000-resources.md index 26e6531d1a..651dc0f50b 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -1510,6 +1510,286 @@ const NotificationBus = resource(({ on }) => { These patterns demonstrate how resources can be composed to build sophisticated reactive architectures while maintaining clarity, testability, and performance. +## How we teach this + +#### How autotracking works + +This information can, and maybe _should_ be added to the guides before this RFC, as much of it is is not specific to resources, but there are core reactivity concepts that our current guides content is presently missing. It is fundamental to understanding reactivity outside the `@tracked` usage that folks may be used to in components. + +------- + +##### tl;dr: autotracking + +Every `{{}}`, `()`, and non-element `` region in a component is auto-tracked. + +What this is means is before the renderer evaluates what content should be rendered, a "tracking frame" is opened -- and when the renderer gets an answer about what should be rendered the "tracking frame" is closed. This happens synchronously, and can be understood with this psuedo code: +```js +function getValueFor(x) { + openTrackingFrame(); + x(); // the value, helper, modifier, component, etc + closeTrackingFrame(); +} +``` + +Internally when we evaluate `x()`, any tracked values encountered are added to the current tracking frame. +Like this psuedo code / psuedo implementation: +``` +let currentFrame = null; +// beginTrackingFrame +currentFrame = []; +function readTracked(trackedRef) { + currentFrame.push(trackedRef); +} +✨ associate currentFrame with the region ✨ + +// read x +// -> encounter and read @tracked foo +readTracked(ref); // for each encountered tracked value (this is inherent to the plumbing of the getter used by @tracked) + +// closeTrackingFrame +currentFrame = null; +``` + +Later, when `@tracked foo` is set, we schedule the renderer to check what regions' associated frame informations may have changed, and then re-render just those regions, repeated the above process. + + +##### _what_ is autotracking + +Autotracking only works when access of a tracked value is deferred untli the tracked region that needs it. Thinking about this in terms of templates and javascript is inherently different, because the templates are designed to be much less cumbersome than would otherwise be required in JavaScript. + +For example, in a component-template, we understand that in +```gjs +class Demo extends Component { + @tracekd foo = 0; + + +} +``` +even though `{{this.foo}}` looks like an _eager access_, the actual value of `foo` isn't read until rendered -- until `{{@count}}` is rendered within ``. + +In JavaScript, without templating, this would be the equivelent of: +```js +class Demo { + @tracked foo; + + render() { + return Display({ + '@count': () => this.foo, + }) + } +} +``` + +In this hypothetical implementation, `foo` would not be auto-tracked until `@count()` was invoked. + +This is exactly how getters work as well. + +> [!NOTE] +> FUN FACT: in most JS ASTs, a getter is a ClassMethod with `kind: 'get'` + +```js +class Demo { + @tracked foo + + get bar() { + return this.foo; + } + + render() { + return Display({ + '@count': () => this.bar; + }); + } +} +``` + +> [!Important] +> the `@tracked` decorator replaces the property with a getter, so that the behavior on access is _lazy evaluation_. + +Consequentially, this is why this common mistake _doesn't work as initially expected_ +```js +class Display extends Component { + @tracked count = this.args.count; + + +} +``` +the right side of an equals sign only happens one. _There is no possible way in JavaScript to make the right side of an equals sign reactive_. + +Access and evaluation is still lazy, however. In decorator terminology this is (roughly) called the `init` phase, and only happens once. + + +Likelwise, when we want to build a plain old javascript object, and give it reactivity, we have to follow the JavaScript rules of lazy evaulation: +```js +class Demo extends Component { + @tracked foo = 0; + + // 1. ❌ Not reactive, this.foo is evaluated onece when `State` is created + state = new State(this.foo) + + // 2. ✅ reactive -- this.foo isn't evaluated until `State` needs it. + state = new State(() => this.foo); + + +} +``` + +In example 1 where `State` is created via `new State(this.foo)`, `State` may look like this: +```js +class State { + constructor(foo) { + this.#foo = foo; + } + + get doubledFoo() { + return this.#foo * 2; + } +} +``` +This is _non_ reactive, because `#foo` is assigned once _and_ is passed a primitive value, `0` in this case -- and primitive values are not capable of being reactive. + +> [!IMPORTANT] +> Reactivity requires an abstraction around the value we wish to be reactive. This is where [Cell](https://github.com/emberjs/rfcs/pull/1071) comes in for representing reactive references to "some value" (without a class). Note that `@tracked` can be implemented with `Cell` internally. See [RFC #1071](https://github.com/emberjs/rfcs/pull/1071) for details. + +In example 2, where `State` is created via `new State(() => this.foo)`, `State` make look like this: +```js +class State { + constructor(fooFunction) { + this.#fooFn = fooFunction; + } + + get doubledFoo() { + return this.#fooFn() * 2; + } +} +``` +This is reactive because the construction of `State` does not require the value (`foo`) to be evaluated. It defers evaluation until later -- in this case when `doubledFoo` would be evaluated. This deferring of evaluation allows the renderer to find the tracked state while rendering each individual `{{}}`, `()`, or `` region in the template. In the example earlier with tracking frames, where we hand-waived over `x()` "adding to the `currentFrame`, it doesn't matter what is access, or how many things are accessed, because tracked values, when read, push their state into the global `currentFrame` variable. + + +##### The styles of deferring evaluation until _needed_ in JavaScript. + +- [Single-value function](https://limber.glimdown.com/edit?c=JYWwDg9gTgLgBAYQuCA7Apq%2BAzKy4DkAAgOYA2oI6UA9AMbKQZYEDcAUKJLHAN5wwoAQzoBrdABM4AXzi58xcpWo1BI0cFQk2nFD35oZcvCEJF0IAEYqQECcGzBqO9nTJCAzh7gBlGEJh0PnY4OABibAgIADFUDlCGVA9BAFc6GGgACkiY1ABKPgEAC2APADoIqNi4AF45KriZdhC4EnR4ADchMhT0TILeFtCodpSoVGLSipzY-vim6Wb0AA9ueAl0bCEUsng3T28AEQsIOBXA1AlvJBRmeEHQojUxSXrTuoAGDhbkgKC6jAAd18-kCmX6tQAfJNyjk8t9Qpo6CMqFhanAITVoTASrCogBqfEIuAAHkC4HcgUhQz4vBxU1%2BgTKXR66Gki1CoRJlhSMAyE14vEMBDcwDEBBhZSRKMwMHZkOlFllJJoPL5aGpXNUFjAlPQ1MWQA&format=gjs) + ```js + class Demo extends Component { + @tracked foo = 0; + + state = new State(() => this.foo); + + + } + + class State { + constructor(fooFn) { this.#fooFn = fooFn } + + get value() { + return this.#fooFn(); + } + } + ``` +- Multi-value function + ```js + class Demo extends Component { + @tracked one = 0; + @tracked two = 0; + + state = new State(() => ({ one: this.one, two: this.two })); + + + } + + class State { + constructor(fooFn) { this.#fooFn = fooFn } + + get value() { + return this.#fooFn(); + } + } + ``` + + + + +**Why does this matter for auto-tracking?** + +Value evaluation must be lazy for reactivity to be wired up as fine-grainedly. + +For example: + + +### Resources + + +### When _not_ to use a resource + +- When there is no cleanup or lifecycle management needed. + For example, in state containers, you may just want a class. Even if you need to [link](https://github.com/emberjs/rfcs/pull/1067) for service access, managing a class may be easier. + + ```js + function Foo(countFn) { + return resource(({ use }) => { + let nestedState = use(OtherResource(() => countFn())); + + return { + get isLoading() { + return nestedState.isPending; + }, + get result() { + return nestedState.value; + }, + }; + }); + } + + // usage + class Demo extends Component { + @tracked count = 0; + + @use foo = Foo(() => this.count); + + + } + ``` + + instead, this would be behaviorally equivelant, and closer to abstractionless JavaScript: + ```js + class Foo { + constructor(countFn) { this.#countFn = countFn; } + + @use nestedState = OtherResource(() => this.#countFn()); + + get isLoading() { + return this.nastedState.isPending; + } + + get result() { + return this.nestedState.value; + } + } + + // usage + class Demo extends Component { + @tracked count = 0; + + foo = new Foo(() => this.count) + + + } + ``` + When you don't need a resource, your application gets to skip all the infrastructure that makes resources work (at the time of writing: the Helper Manager system). Additionally, when a resource is not needed, your application code stays closer to framework-agnostic concepts, thus improving onboarding for new developers. + + + + ## Drawbacks - The function coloring problem, described here [TC39 Signals Issue#30](https://github.com/tc39/proposal-signals/issues/30). From 3e3403d7b0cfba845a11d6dd6de49e96feccf1df Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:47:46 -0400 Subject: [PATCH 17/27] ope --- text/0000-resources.md | 57 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index 651dc0f50b..b19a1cfb6a 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -1692,7 +1692,7 @@ This is reactive because the construction of `State` does not require the value } } ``` -- Multi-value function +- [Multi-value function](https://limber.glimdown.com/edit?c=JYWwDg9gTgLgBAYQuCA7Apq%2BAzKy4DkAAgOYA2oI6UA9AMbKQZYEDcAUKJLHAN5wwoAQzoBrdABM4AXzi58xcpWo1BI0cFQk2nFD35oZcvCEJF0IAEYqQECcGzBqO9nTJCAzh7gBlGEJh0PnY4OABiCDAYYDQPDlCGVA9BAFc6GGgACkjo2IAxVABKPgEAC2APADoIqJikuABeOBy6jwLWGXYQuBJ0eDR0TOLebtCoPpSoVDKK6pbYocqB%2BM7u3vgYAHcIIeDQsYmpmaqa3KTFrYgV6XYb9nQAD254CXRsIRSyeDdPbwARCwQOCPQKoCTeJAoZjwEahIhqMSSZoYRpwAAMK3hwkRUkuqIxXVCyQCQSaGE2vn8gUyuwaAD44JkDBgAFzHJYYAA0Am2bJg5SqeOkhUKHG6mjo4yoWAAjKjaQz%2BbMBgBqFUrCVSzAwABM8uK9PZlzVYtCAB5AuB3IE6aM%2BLwlVViYEOehpLIAD72x2VZ3oSqXd12s2WFIwDLTXi8QwENzAMQEdmaizamXuunJ6X9DBmmih8NoW37OAhsMR%2B0xuMJpOoSUprA69OZ7U8iC5-MRosl1QWMDW9C2m5AA&format=gjs) ```js class Demo extends Component { @tracked one = 0; @@ -1706,11 +1706,62 @@ This is reactive because the construction of `State` does not require the value class State { constructor(fooFn) { this.#fooFn = fooFn } - get value() { - return this.#fooFn(); + get one() { + return this.#options().one; + } + + get two() { + return this.#options().two; } } ``` + Note that when `one` changes, the whole function passed in to `State` will be considered invalidated -- whet `state.one` is changed, so also will `state.two` be changed. This is simpler, but can be expensive for complex derivations. +- Fine-grained object + ```js + class Demo extends Component { + @tracked one = 0; + @tracked two = 0; + + state = new State(() => { + let parent = this; + + return { + // 'this' in a getter in an object refers to the object, not the parent context. + get innerOne() { + return this.one; + }, + get innerTwo() { + return this.two; + }, + get combined() { + return this.innerOne * this.innerTwo; + } + }); + + + } + + class State { + constructor(optionsFn) { this.#options = optionsFn } + + get #options() { + return this.#options(); + } + + // These are individually reactive + get one() { + return this.#options.one; + } + + get two() { + return this.#options.two; + } + + get combined() { + return this.#options.combined; + } + } + ``` From 6adce3b48799e7a20e794cda08705a9fce977787 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:53:07 -0400 Subject: [PATCH 18/27] ope --- text/0000-resources.md | 106 +++++++++++++---------------------------- 1 file changed, 32 insertions(+), 74 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index b19a1cfb6a..37724cc15b 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -1268,9 +1268,7 @@ const BreakpointInfo = resource(({ use }) => { }); ``` -### Best Practices and Patterns - -#### Resource Composition Patterns +### Patterns and Examples **Hierarchical Composition**: Build complex resources from simpler ones using reactive ownership: @@ -1295,11 +1293,16 @@ const CurrentUser = resource(({ on, owner }) => { session.on('userChanged', handleUserChange); on.cleanup(() => session.off('userChanged', handleUserChange)); + // ".current" is special and is automatically collapsed when rendered. + // otherwise you could return () => userDate.current; return userData; }); ``` -**Parallel Data Loading**: Avoid waterfalls by composing resources that fetch independently: +**Parallel Data Loading**: Avoid waterfalls by composing resources that run async independently: + +> [!NOTE] +> WarpDrive has utilities for this problem as well ```js const DashboardData = resource(({ use }) => { @@ -1312,14 +1315,13 @@ const DashboardData = resource(({ use }) => { user: userData.current, analytics: analytics.current, notifications: notifications.current, - // Derived state based on all three - hasUnreadNotifications: notifications.current.unreadCount > 0 + get hasUnreadNotifications() { + return notifications.current.unreadCount > 0; + } }); }); ``` -#### Error Handling and Resilience - **Graceful Degradation**: Design resources to handle errors gracefully: ```js @@ -1333,10 +1335,13 @@ const ResilientDataLoader = resource(({ on }) => { const maxRetries = 3; const backoffDelay = (attempt) => Math.min(1000 * Math.pow(2, attempt), 10000); + const controller = new AbortController(); + + on.cleanup(() => controller.abort()); const fetchWithRetry = async (attempt = 0) => { try { - const response = await fetch('/api/critical-data'); + const response = await fetch('/api/critical-data', { signal: controller.signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); @@ -1363,8 +1368,6 @@ const ResilientDataLoader = resource(({ on }) => { }); ``` -#### Performance Optimization - **Lazy Evaluation for Expensive Operations**: ```js @@ -1382,7 +1385,9 @@ const ExpensiveComputation = resource(({ use }) => { return { status: 'computed', - result: performExpensiveAnalysis(data.data) + get result() { + return performExpensiveAnalysis(data.result); + } }; }; }); @@ -1396,10 +1401,11 @@ const MemoizedProcessor = resource(({ use }) => { let lastInput = null; let cachedResult = null; + // This function here is intepreted as a `createCache` return () => { const currentInput = input.current; - // Memoize based on input identity/equality + // Memoize based on input value equality if (currentInput !== lastInput) { lastInput = currentInput; cachedResult = expensiveProcessing(currentInput); @@ -1410,9 +1416,7 @@ const MemoizedProcessor = resource(({ use }) => { }); ``` -#### Testing Patterns - -**Resource Testing in Isolation**: +**Unit testing with Resources**: ```js import { module, test } from 'qunit'; @@ -1439,11 +1443,11 @@ module('Unit | Resource | timer', function(hooks) { const instance = TestTimer.create(); instance.link(this.owner); - const initialTime = instance.current.current; + const initialTime = instance.current; await new Promise(resolve => setTimeout(resolve, 250)); - const laterTime = instance.current.current; + const laterTime = instance.current; assert.ok(laterTime > initialTime, 'Timer advances'); instance.destroy(); @@ -1451,63 +1455,21 @@ module('Unit | Resource | timer', function(hooks) { }); ``` -#### Integration Patterns - **Service Integration**: ```js const ServiceAwareResource = resource(({ owner }) => { const session = owner.lookup('service:session'); const router = owner.lookup('service:router'); - const store = owner.lookup('service:store'); // Resource can depend on and coordinate multiple services return () => ({ currentUser: session.currentUser, currentRoute: router.currentRouteName, - // Reactive query based on current context - relevantData: store.query('item', { - userId: session.currentUser?.id, - route: router.currentRouteName - }) }); }); ``` -**Cross-Resource Communication**: - -```js -const NotificationBus = resource(({ on }) => { - const subscribers = cell(new Map()); - const messages = cell([]); - - const subscribe = (id, callback) => { - const current = subscribers.current; - const newMap = new Map(current); - newMap.set(id, callback); - subscribers.set(newMap); - }; - - const publish = (message) => { - messages.set([...messages.current, message]); - - // Notify all subscribers - subscribers.current.forEach(callback => { - callback(message); - }); - }; - - const unsubscribe = (id) => { - const current = subscribers.current; - const newMap = new Map(current); - newMap.delete(id); - subscribers.set(newMap); - }; - - return { subscribe, publish, unsubscribe, messages }; -}); -``` - These patterns demonstrate how resources can be composed to build sophisticated reactive architectures while maintaining clarity, testability, and performance. ## How we teach this @@ -1716,7 +1678,7 @@ This is reactive because the construction of `State` does not require the value } ``` Note that when `one` changes, the whole function passed in to `State` will be considered invalidated -- whet `state.one` is changed, so also will `state.two` be changed. This is simpler, but can be expensive for complex derivations. -- Fine-grained object +- [Fine-grained object](https://limber.glimdown.com/edit?c=JYWwDg9gTgLgBAYQuCA7Apq%2BAzKy4DkAAgOYA2oI6UA9AMbKQZYEDcAUKJLHAN5wwoAQzoBrdABM4AXzi58xcpWo1BI0cFQk2nFD35oZcvCEJF0IAEYqQECcGzBqO9nTJCAzh7gBlGEJh0PnY4OABiCDAYYDQPADFUVjgQuAZUD0EAVzoYaAAKSOjYhIBKPgEAC2APADoIqJj0hLgAXjhCxvjUGXYUknR4eqL0vLLeFNCoAcyobpgq2qHOhNGOUOle0JoaOAAVCvQPIKEpuE17ADdgCUyhMjIATzgpkWiL9D6B9oxR4NDJ6azSrVOodWI1TQYKAAeQwax6n3gMAA7hBfuN-s9AXMFqCGuDIdRdqj4RtEalkJZNJJ0RMsTAZjiQUtwQwrNSJKT2GT0AAPbjwCTobBCTJkeBuTzeAAiFggcD5gVQEm8SBQzHgGLgRDUYkk3yCbQADPCdcI9VIUfLjRwUhkAoa4Bhkb5-IE8r8WgA%2BP7-MhfMAnTDwNrzaq2zFTBlA-h0rY7AhhjwEM7dIRwfowQJQVNwITdCCWABW6ByWOw1G8uUqQULJZyABonRAkQc4IGplgKVhFTU4xmvoSYT8xv2AdHuh3gzU0Oh4ZjpA3%2B5nU1DiWjR5jI9j20GsDUrfP-ovl182VSMBJaVv-lHGcDakPYUEAFQPiGoNck-sbBekkoRqmdBTFQWAAIytHAno%2BkmM4YAA1PB8KaMBFjBgATJB0HvlaiGAQAPIE4DuIEXp0rwvCwfagRweg0iyAAPnwlG4tR6AHqi9FwExFFUW67Hnhy9F0vhliZFmhgUYYBBuMAYgprBKEgcGYH0V6SloV2s74TQYkSagZGYqJ4m5N0UndDJFDye%2BGmgTA6FqbZwYCKiOl6aZhlwDpRFgCR6BkRsQA&format=gjs) ```js class Demo extends Component { @tracked one = 0; @@ -1728,21 +1690,22 @@ This is reactive because the construction of `State` does not require the value return { // 'this' in a getter in an object refers to the object, not the parent context. get innerOne() { - return this.one; + return parent.one; }, get innerTwo() { - return this.two; + return parent.two; }, get combined() { return this.innerOne * this.innerTwo; } + }; }); } class State { - constructor(optionsFn) { this.#options = optionsFn } + constructor(optionsFn) { this.#optionsFn = optionsFn } get #options() { return this.#options(); @@ -1750,11 +1713,11 @@ This is reactive because the construction of `State` does not require the value // These are individually reactive get one() { - return this.#options.one; + return this.#options.innerOne; } get two() { - return this.#options.two; + return this.#options.innerTwo; } get combined() { @@ -1762,19 +1725,14 @@ This is reactive because the construction of `State` does not require the value } } ``` + In this example, `state.one` and `state.two` are _individually_ reactive. and `state.combined` entantgles with both. - -**Why does this matter for auto-tracking?** - -Value evaluation must be lazy for reactivity to be wired up as fine-grainedly. - -For example: - - ### Resources +TODO: (adapt from the ember-resources documentation) + ### When _not_ to use a resource From 48e8370d17b23f5d756c61de304a075f93fe4e9b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:54:42 -0400 Subject: [PATCH 19/27] ope --- text/0000-resources.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/text/0000-resources.md b/text/0000-resources.md index 37724cc15b..f2eceec9bd 100644 --- a/text/0000-resources.md +++ b/text/0000-resources.md @@ -1114,9 +1114,10 @@ Function returns are particularly useful for: The choice between returning cells and returning functions depends on whether the resource primarily holds state (use cells) or computes derived values (use functions). -### Example Use Cases +### Examples + +**Data Fetching with Modern Async Patterns** -**1. Data Fetching with Modern Async Patterns** ```js const RemoteData = resource(({ on }) => { const state = cell({ loading: true, data: null, error: null }); @@ -1164,7 +1165,8 @@ const ProcessedData = resource(({ use }) => { }); ``` -**2. WebSocket Connection with Reactive State Management** +**WebSocket Connection with Reactive State Management** + ```js const WebSocketConnection = resource(({ on, owner }) => { const notifications = owner.lookup('service:notifications'); @@ -1220,7 +1222,8 @@ const WebSocketConnection = resource(({ on, owner }) => { }); ``` -**3. Reactive DOM Event Handling with Debouncing** +**Reactive DOM Event Handling with Debouncing** + ```js const WindowSize = resource(({ on }) => { const size = cell({ @@ -1268,8 +1271,6 @@ const BreakpointInfo = resource(({ use }) => { }); ``` -### Patterns and Examples - **Hierarchical Composition**: Build complex resources from simpler ones using reactive ownership: ```js From 4c59b6eaffd94fd490128c3aec88b61af6c95620 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:55:37 -0400 Subject: [PATCH 20/27] Rename file --- text/{0000-resources.md => 1122-resources.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename text/{0000-resources.md => 1122-resources.md} (99%) diff --git a/text/0000-resources.md b/text/1122-resources.md similarity index 99% rename from text/0000-resources.md rename to text/1122-resources.md index f2eceec9bd..74f8d74d6a 100644 --- a/text/0000-resources.md +++ b/text/1122-resources.md @@ -8,7 +8,7 @@ teams: - learning - typescript prs: - accepted: # Fill this in with the URL for the Proposal RFC PR + accepted: https://github.com/emberjs/rfcs/pull/1122 project-link: suite: --- From c447a91670f40ae14872fc2c88ba250d854b63ec Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:06:21 -0400 Subject: [PATCH 21/27] ope --- text/1122-resources.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/text/1122-resources.md b/text/1122-resources.md index 74f8d74d6a..0b5e07517d 100644 --- a/text/1122-resources.md +++ b/text/1122-resources.md @@ -515,9 +515,12 @@ function resource(fn: ResourceFunction): Resource #### `on.cleanup` +Allows user-defined cleanup to occur when the resource would be torn down. Same behavior as the existing `registerDestructor`. #### `use` +A short-hand for interacting with `invokeHelper` -- allows using nested resources as stable references. Upon update, cleanup is guaranteed to run before the creation of the ..... + #### `link` From 64cc5f6168cfdb9af1d5beb167c803fad89127ae Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:57:23 -0400 Subject: [PATCH 22/27] Add examples of the resource's callback APIs --- text/1122-resources.md | 46 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/text/1122-resources.md b/text/1122-resources.md index 0b5e07517d..b5b58f95a2 100644 --- a/text/1122-resources.md +++ b/text/1122-resources.md @@ -517,15 +517,59 @@ function resource(fn: ResourceFunction): Resource Allows user-defined cleanup to occur when the resource would be torn down. Same behavior as the existing `registerDestructor`. +Example +```gjs +resource(({ on }) => { + on.cleanup(() => console.log('cleaning up')); +}) +``` + #### `use` -A short-hand for interacting with `invokeHelper` -- allows using nested resources as stable references. Upon update, cleanup is guaranteed to run before the creation of the ..... +A short-hand for interacting with `invokeHelper` -- allows using nested resources as stable references. Upon update, cleanup is guaranteed to run before the creation / update of the internal state of the `use`d thing. + +Use returns a read-only cell, with only a `.current` property. + +Example +```gjs +resource(({ use }) => { + let state = use(OtherResource(() => { /* ... */ })) + + return () => { + let active = state.current; + + return active.result; + }; + +}) +``` #### `link` +A shorthand for wiring up the owner and and destruction hierarchies. + +Example +```gjs +resource(({ link }) => { + let state = new State(); + + // enables State to have service injection and use registerDestructor + link(state); +}) + +``` #### `owner` +Enables accessing services and other things on the owner. + +Example +```gjs +resource(({ owner }) => { + let router = owner.lookup('service:router'); +}); +``` + ### Resource Creation and Usage From 3e21027069e72967f18bfdca90217124efb5f008 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:00:29 -0400 Subject: [PATCH 23/27] More links --- text/1122-resources.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/text/1122-resources.md b/text/1122-resources.md index b5b58f95a2..fabcfd5826 100644 --- a/text/1122-resources.md +++ b/text/1122-resources.md @@ -1873,3 +1873,10 @@ n/a - [RFC #1067: link](https://github.com/emberjs/rfcs/pull/1067) - Related(ish) / would be influenced by resources - [RFC #502: Explicit Service Injection](https://github.com/emberjs/rfcs/pull/502) +- The earlier Resource RFC, [RFC #567](https://github.com/emberjs/rfcs/pull/567) + +Broader community content +- [How autotracking works](https://www.pzuraq.com/blog/how-autotracking-works) +- [TC39 Signals Async Discussion](https://github.com/tc39/proposal-signals/issues/30) +- [TC39 Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) + From 36dfe48f2d86326c1bb2f4eec532c2692fa18fb9 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:08:43 -0400 Subject: [PATCH 24/27] example --- text/1122-resources.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/text/1122-resources.md b/text/1122-resources.md index fabcfd5826..5629baed73 100644 --- a/text/1122-resources.md +++ b/text/1122-resources.md @@ -574,6 +574,26 @@ resource(({ owner }) => { ### Resource Creation and Usage + +First, the manual way, as if using resources outside of ember, with no framework whatsoever: + +```js +import { resource } from '@ember/reactive'; +import { setOwner } from '@ember/owner'; + +let thing = resource(() => 2); +let owner = { + lookup: (registrationName) => { /* ... */ } +}; + +// @ts-expect-error - types are a lie due to decorators +let instance = thing.create(); + +instance.link(owner); +assert.strictEqual(instance.current, 2); + +``` + Resources can be used in several ways: **1. In Templates (as helpers)** From 4f0cb8ea7fc8105114b1a9f693bab4e742615b87 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:15:09 -0400 Subject: [PATCH 25/27] Updates --- text/1122-resources.md | 214 ++++++++++++++++++++++++++--------------- 1 file changed, 136 insertions(+), 78 deletions(-) diff --git a/text/1122-resources.md b/text/1122-resources.md index 5629baed73..ea1b7ac9c6 100644 --- a/text/1122-resources.md +++ b/text/1122-resources.md @@ -448,7 +448,9 @@ This unification hopefully will lead to simplification of the implmentation of a ### Overview -A **resource** is a reactive function that represents a value with lifecycle and optional cleanup logic. Resources are created using the `resource()` function and automatically manage their lifecycle through Ember's existing destroyable system, though the exact implementation could change at any time (to be simpler) as we work at simplifying a bunch of internals. +A **resource** is a reactive function that represents a value with lifecycle and optional cleanup logic. Resources are created using the `resource()` function and automatically manage their lifecycle through Ember's existing destroyable system[^future-destruction], though the exact implementation could change at any time (to be simpler) as we work at simplifying a bunch of internals. + +[^future-destruction]: while today in userland, we can implement what we need through destroyables, the real implementation can eventually become low-level enough where we use it to build the renderer in place of our current VM. ```gjs import { cell, resource } from '@ember/reactive'; @@ -498,19 +500,31 @@ function addScript(url) { The `resource()` function takes a single argument: a function that receives a ResourceAPI object and returns a reactive value. +The implementation types: ```ts interface ResourceAPI { on: { cleanup: (destructor: () => void) => void; }; - use: (resource: T) => ReactiveValue; - link: (obj: unknown, parent?: obj: unknown) => void; + use: (resource: T) => ReadOnlyCell; + link: (obj: unknown) => void; owner: Owner; } type ResourceFunction = (api: ResourceAPI) => T; -function resource(fn: ResourceFunction): Resource +function resource(fn: ResourceFunction): ResourceBuilder + +// These APIs are handled by the framework and would not be interacted with by users in app code. +// But would become useful for unit testing purposes. +// However, using rendering/reactivity testing would be better and would not require the use of these interfaces. +interface ResourceBuilder { + create(): Resource; +} + +interface Resource extends ReadOnlyCell { + link(context: object): void +} ``` #### `on.cleanup` @@ -524,6 +538,29 @@ resource(({ on }) => { }) ``` +> [!NOTE] +> If you've seen [Starbeam](https://starbeamjs.com/), you may be aware of an `on.sync`. `on.sync` is not currently possible with today's reactivity system (at the time of writing this RFC). Some updates to how the reactivity system and renderer will be needed before we can add `on.sync`, but it would be in addition to everything described in this RFC and the existing state of this RFC does not conflict with the desire for an `on.sync` later. + +
AbortController example + +```js +const DataLoader = resource(({ on }) => { + const controller = new AbortController(); + const state = cell({ loading: true }); + + on.cleanup(() => controller.abort()); + + fetch('/api/data', { signal: controller.signal }) + .then(response => response.json()) + .then(data => state.set({ loading: false, data })) + .catch(error => state.set({ loading: false, error })); + + return state; +}); +``` + +
+ #### `use` A short-hand for interacting with `invokeHelper` -- allows using nested resources as stable references. Upon update, cleanup is guaranteed to run before the creation / update of the internal state of the `use`d thing. @@ -544,6 +581,27 @@ resource(({ use }) => { }) ``` +> [!TIP] +> Because `use` returns a read-only cell with only a `.current` property, the "value" of the used resource, for clarity, should try to avoid having its own `.current` property, as that would have folks with `state.current.current`, which looks weird. + +
+ +```js +const Now = resource(({ on }) => { + const time = cell(new Date()); + const timer = setInterval(() => time.set(new Date()), 1000); + on.cleanup(() => clearInterval(timer)); + return time; +}); + +const FormattedTime = resource(({ use }) => { + const time = use(Now); + return () => time.current.toLocaleTimeString(); +}); +``` + +
+ #### `link` A shorthand for wiring up the owner and and destruction hierarchies. @@ -575,7 +633,10 @@ resource(({ owner }) => { ### Resource Creation and Usage -First, the manual way, as if using resources outside of ember, with no framework whatsoever: +First, the manual way, as if using resources outside of ember, with no framework whatsoever. + +> [!NOTE] +> This is using the previously mentioned APIs that *could* be useful for unit testing -- but folks should prefer rendering/reactivity testing. ```js import { resource } from '@ember/reactive'; @@ -586,14 +647,66 @@ let owner = { lookup: (registrationName) => { /* ... */ } }; -// @ts-expect-error - types are a lie due to decorators let instance = thing.create(); instance.link(owner); -assert.strictEqual(instance.current, 2); +// if in a test: +// assert.strictEqual(instance.current, 2); +``` + +
preview: how one would do the above with a rendering / reactivity test + +```gjs +import { resource } from '@ember/reactive'; +import { setOwner } from '@ember/owner'; + +module('suite', function (hooks) { + setupRenderingTest(hooks); + /** + * Normally a test would look like this: + */ + test('example', async function () { + let thing = resource(() => 2); + + await render( + + ); + + assert.dom().hasText('2'); + }); + + /** + * When using a manually created and managed resource + */ + test('manual mode', async function () { + let thing = resource(() => 2); + let owner = { + lookup: (registrationName) => { /* ... */ } + }; + + let instance = thing.create(); + + instance.link(owner); + + await render( + + ); + + assert.dom().hasText('2'); + }); +}) ``` +
+ + + + Resources can be used in several ways: **1. In Templates (as helpers)** @@ -612,6 +725,7 @@ const Clock = resource(({ on }) => { ``` **2. With the `@use` decorator** + ```js import { use } from '@ember/reactive'; @@ -640,7 +754,7 @@ This convention makes the code more readable and aligns with the expectation tha #### Helper Manager Integration and the `@use` Decorator -The `@use` decorator builds upon Ember's existing helper manager infrastructure (RFC 625 and RFC 756) to provide automatic invocation of values with registered helper managers. When a resource is created with `resource()`, it receives a helper manager that makes it invokable in templates and enables the `@use` decorator's automatic behavior. +The `@use` decorator builds upon Ember's existing helper manager infrastructure ([RFC #625](https://github.com/emberjs/rfcs/pull/625) and [RFC #756](https://github.com/emberjs/rfcs/pull/756)) to provide automatic invocation of values with registered helper managers. When a resource is created with `resource()`, it receives a helper manager that makes it invokable in templates and enables the `@use` decorator's automatic behavior. Here's how it works: @@ -651,10 +765,10 @@ const Clock = resource(({ on }) => { }); // The @use decorator detects the helper manager and automatically invokes it -@use clock = Clock; // Equivalent to: clock = Clock() +@use clock = Clock; -// Without @use, you need explicit invocation or .current access -clock = use(this, Clock); // Returns object with tracked property: clock.current +// Without @use, you need explicit invocation or .current access as `clock` cannot be "replaced" as decorators allow. +clock = use(this, Clock); ``` The `@use` decorator pattern extends beyond resources to work with any construct that has registered a helper manager. This means that future primitives that integrate with the helper manager system (like certain kinds of computed values, cached functions, or other reactive constructs) will automatically work with `@use` without any changes to the decorator itself. @@ -672,96 +786,40 @@ export default class MyComponent extends Component { } ``` -The `use()` function provides manual resource instantiation when you need more control over the lifecycle or want to avoid the automatic invocation behavior of `@use`. - -**4. Manual instantiation (for library authors)** -```js -const clockBuilder = resource(() => { /* ... */ }); -const owner = getOwner(this); -const clockInstance = clockBuilder.create(); -clockInstance.link(owner); -const currentTime = clockInstance.current; -``` - -### Resource API Details - -**`on.cleanup(destructor)`** - -Registers a cleanup function that will be called when the resource is destroyed. This happens automatically when: -- The owning context (component, service, etc.) is destroyed -- The resource re-runs due to tracked data changes -- The resource is manually destroyed - -```js -const DataLoader = resource(({ on }) => { - const controller = new AbortController(); - const state = cell({ loading: true }); - - on.cleanup(() => controller.abort()); - - fetch('/api/data', { signal: controller.signal }) - .then(response => response.json()) - .then(data => state.set({ loading: false, data })) - .catch(error => state.set({ loading: false, error })); - - return state; -}); -``` - -**`use(resource)` - Resource Composition** - -The `use()` method within a resource function allows composition of resources by consuming other resources with proper lifecycle management. This is different from the top-level `use()` function and the `@use` decorator: - -```js -const Now = resource(({ on }) => { - const time = cell(new Date()); - const timer = setInterval(() => time.set(new Date()), 1000); - on.cleanup(() => clearInterval(timer)); - return time; -}); - -const FormattedTime = resource(({ use }) => { - const time = use(Now); - return () => time.current.toLocaleTimeString(); -}); -``` +The `use()` function provides manual resource instantiation when you need more control over the lifecycle or want to potentially avoid "magic" that yet-to-be-understood decorators provide, like that of `@use` - where `.current` would not be needed in the template. ### Key Differences Between Usage Patterns **Understanding `@use` as an Ergonomic Shorthand** -The `@use` decorator is fundamentally an ergonomic convenience that builds upon Ember's helper manager infrastructure. When you apply `@use` to a property, it doesn't assign the value directly—instead, like `@tracked`, it replaces the property with a getter that provides lazy evaluation and automatic invocation. +The `@use` decorator is fundamentally an ergonomic convenience that builds upon Ember's helper manager infrastructure. When you apply `@use` to a property, it doesn't assign the value directly—instead, like `@tracked`, it replaces the property with a getter that provides lazy evaluation and automatic invocation. This allows you to not need to interact with the `.current` property on resources (and cells as well); ```js export default class MyComponent extends Component { - // This: + // This @use clock = Clock; - // Is equivalent to defining a getter that automatically invokes + // is (roughly) equivalent to defining a getter that automatically invokes // any value with a registered helper manager: + @cached get clock() { - // Detect helper manager and invoke automatically - if (hasHelperManager(Clock)) { - return invokeHelper(this, Clock); - } - return Clock; + return getValue(invokeHelper(this, Clock)); } } ``` -This getter-based approach enables several key benefits: -- **Lazy instantiation**: The resource is only created when first accessed -- **Automatic lifecycle management**: The resource is tied to the component's lifecycle -- **Transparent integration**: Works seamlessly with any construct that has a helper manager +> [!NOTE] +> If one were to do `@use count = Cell(2)`, that would be behaviorally the same as `@tracked count = 2;`, with the exception that `@use` is only ever read-only. **`@use` decorator vs resource `use()` vs top-level `use()`:** 1. **`@use` decorator** - An ergonomic shorthand that leverages Ember's helper manager system: - Replaces the property with a getter (like `@tracked`) for lazy access - - Automatically invokes values with registered helper managers (RFC 625/756) - Works with resources, but also any construct that has a helper manager - Returns the "unwrapped" value directly (no `.current` needed) - - Best for when you want the simplest possible API with automatic lifecycle management + - Best for when you want the simplest possible API with automatic lifecycle management[^lifecycle-downside] + +[^lifecycle-downside] being tied to the component's lifecycle can be a downside depending on the size of your component. For this reason it is important to consider the factoring of your application -- keeping components small, and/or ensuring that when fine-grained destruction is needed, resources are created in the template instead of the JavaScirpt class (but with fine-grained components, js vs template is likely equivelant) 2. **Resource `use()` method** - For resource composition within resource functions: - Available only within the resource function's API object From a5a2a4137095eaf644956f0c9d08937786597a97 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:10:37 -0400 Subject: [PATCH 26/27] Let's go --- text/1122-resources.md | 1874 +++++++++++++++++++++++++++++++--------- 1 file changed, 1469 insertions(+), 405 deletions(-) diff --git a/text/1122-resources.md b/text/1122-resources.md index ea1b7ac9c6..dd9b793e8f 100644 --- a/text/1122-resources.md +++ b/text/1122-resources.md @@ -813,6 +813,8 @@ export default class MyComponent extends Component { **`@use` decorator vs resource `use()` vs top-level `use()`:** +All forms of use can operate on the same structures (resources, helpers, etc) + 1. **`@use` decorator** - An ergonomic shorthand that leverages Ember's helper manager system: - Replaces the property with a getter (like `@tracked`) for lazy access - Works with resources, but also any construct that has a helper manager @@ -834,59 +836,9 @@ export default class MyComponent extends Component { - Useful when you need fine-grained control over instantiation timing - Primarily for library authors or advanced use cases where automatic behavior isn't desired -**`owner`** - -Provides access to the Ember owner for dependency injection: - -```js -const UserSession = resource(({ owner }) => { - const session = owner.lookup('service:session'); - const router = owner.lookup('service:router'); - - return () => ({ - user: session.currentUser, - route: router.currentRouteName - }); -}); -``` - -### Resource Lifecycle - -1. **Creation**: When a resource is first accessed, its function is invoked -2. **Reactivity**: If the resource function reads tracked data, it will re-run when that data changes -3. **Cleanup**: Before re-running or when destroyed, all registered cleanup functions are called -4. **Destruction**: When the owning context is destroyed, the resource and all its cleanup functions are invoked - -### Type Definitions - -```ts -interface ResourceAPI { - on: { - cleanup: (destructor: () => void) => void; - }; - use: (resource: T) => ReactiveValue; - owner: Owner; -} - -type ResourceFunction = (api: ResourceAPI) => T; - -interface Resource { - create(): ResourceInstance; -} - -interface ResourceInstance { - current: T; - link(context: object): void; -} - -function resource(fn: ResourceFunction): Resource; -function use(context: object, resource: Resource): ReactiveValue; -function use(resource: Resource): PropertyDecorator; -``` - ### Relationship to the Cell Primitive -While the `cell` primitive ([RFC #1071](https://github.com/emberjs/rfcs/pull/1071)) is not strictly required for resources to function, resources are significantly more ergonomic and powerful when used together with `cell`. Resources can work with Ember's existing `@tracked` properties, but `cell` provides several advantages: +While the `cell` primitive ([RFC #1071](https://github.com/emberjs/rfcs/pull/1071)) is not strictly required for resources to function, resources are significantly more ergonomic and powerful when used together with `cell` (this is doubly so since Resources need the interfaces and types from the Cell implementation). Resources can work with Ember's existing `@tracked` properties, but `cell` provides several advantages: **Without `cell` (using `@tracked`):** ```js @@ -914,7 +866,7 @@ const Clock = resource(({ on }) => { ``` -**With `cell` (more ergonomic):** +**With `cell`:** ```js import { cell, resource } from '@ember/reactive'; @@ -934,109 +886,7 @@ const Clock = resource(({ on }) => { ``` -**Key advantages of using `cell` with resources:** - -1. **Simpler State Management**: No need to create wrapper classes for tracked properties -2. **Direct Value Returns**: Resources can return cells directly rather than objects with tracked properties -3. **Cleaner APIs**: Consumers get more intuitive interfaces without property access -4. **Better Composition**: Resources that return cells compose more naturally with other resources - -While resources provide significant value on their own by solving lifecycle management and cleanup, the combination with `cell` creates a more complete and developer-friendly reactive primitive system. - -### Integration with Existing Systems - -**Helper Manager Integration** - -Resources integrate with Ember's existing helper manager system. The `resource()` function returns a value that can be used directly in templates through the helper invocation syntax. - -**Destroyable Integration** - -Resources automatically integrate with Ember's destroyable system via `associateDestroyableChild()`, ensuring proper cleanup when parent contexts are destroyed. - -**Ownership Integration** - -Resources receive the owner from their parent context, enabling dependency injection and integration with existing Ember services and systems. - -**Relationship to the Cell Primitive** - -While this RFC does not strictly depend on the `cell` primitive from RFC 1071, resources become significantly more ergonomic when used together with `cell`. Resources can work with any reactive primitive, including existing `@tracked` properties and `@cached` getters, but `cell` provides several advantages: - -1. **Function-based APIs**: Resources are function-based, and `cell` provides a function-based reactive primitive that composes naturally -2. **Encapsulated state**: Unlike `@tracked` which requires classes, `cell` allows creating reactive state without class overhead -3. **Immediate updates**: `cell` provides both getter and setter APIs (`cell.current` and `cell.set()`) that work well in resource contexts - -Without `cell`, developers would need to either: -- Use classes with `@tracked` properties (heavier abstraction) -- Manually manage reactivity with lower-level tracking APIs -- Return functions from resources that close over mutable state - -Example comparison: - -```js -// With cell (ergonomic) -const Clock = resource(({ on }) => { - const time = cell(new Date()); - const timer = setInterval(() => time.set(new Date()), 1000); - on.cleanup(() => clearInterval(timer)); - return time; -}); - -// Without cell (more verbose) -const Clock = resource(({ on }) => { - let currentTime = new Date(); - const timer = setInterval(() => { - currentTime = new Date(); - // Manual invalidation needed - }, 1000); - on.cleanup(() => clearInterval(timer)); - - return () => currentTime; // Must return function for reactivity -}); -``` - -Therefore, while `cell` is not a hard dependency, implementing both RFCs together would provide the best developer experience. - -### Advanced Reactive Concepts - -Ember's resource primitive embodies several advanced reactivity concepts that position it as a modern, best-in-class reactive abstraction. Understanding these concepts helps developers leverage resources most effectively and appreciate how they fit into the broader reactive ecosystem. - -#### Evaluation Models: Lazy by Default, Scheduled When Needed - -Resources follow a **lazy evaluation model** by default, meaning they are only created and evaluated when their value is actually consumed. This aligns with the principle that expensive computations should only occur when their results are needed: - -```js -const ExpensiveComputation = resource(({ on }) => { - console.log('This only runs when accessed'); // Lazy evaluation - const result = cell(performExpensiveCalculation()); - return result; -}); - -// Resource not created yet -@use expensiveData = ExpensiveComputation; - -// Only now is the resource created and evaluated - -``` - -However, certain types of resources benefit from **scheduled evaluation**, particularly those that involve side effects or async operations: - -```js -const DataFetcher = resource(({ on }) => { - const state = cell({ loading: true }); - - // Side effect happens immediately (scheduled evaluation) - // to avoid waterfalls when multiple async resources are composed - fetchData().then(data => state.set({ loading: false, data })); - - on.cleanup(() => { - // Cleanup logic - }); - - return state; -}); -``` - -This hybrid approach—lazy by default, scheduled when beneficial—provides optimal performance while avoiding common pitfalls like request waterfalls in async scenarios. +While this RFC does not strictly depend on the `cell` primitive from [RFC #1071](https://github.com/emberjs/rfcs/pull/1071), resources become significantly more ergonomic when used together with `cell`. #### Reactive Ownership and Automatic Disposal @@ -1058,236 +908,88 @@ const ParentResource = resource(({ use, on }) => { }); ``` -The ownership model ensures that: -- **No Memory Leaks**: Child resources are automatically disposed when parents are destroyed -- **Hierarchical Cleanup**: Disposal propagates down through the resource tree -- **Automatic Lifecycle**: No manual `willDestroy` or cleanup management needed -- **Composable Boundaries**: Resources can create isolated ownership scopes - -#### Push-Pull Reactivity and Glitch-Free Consistency - -Resources participate in Ember's **push-pull reactive system**, which combines the benefits of both push-based (event-driven) and pull-based (demand-driven) reactivity: +
Similaries to "Explicit Resource Management" ```js -const DerivedValue = resource(({ use }) => { - const source1 = use(SourceA); - const source2 = use(SourceB); - - // This derivation is guaranteed to be glitch-free: - // When SourceA changes, this won't re-run with stale SourceB data - return () => source1.current + source2.current; -}); -``` - -**Push Phase**: When tracked data changes, notifications propagate down the dependency graph, marking potentially affected resources as "dirty." - -**Pull Phase**: When a value is actually consumed (in templates, effects, etc.), the system pulls fresh values up the dependency chain, ensuring all intermediate values are consistent. - -This approach guarantees **glitch-free consistency**—user code never observes intermediate or inconsistent states during reactive updates. - -#### Phased Execution and Ember's Rendering Lifecycle - -Resources integrate seamlessly with Ember's three-phase execution model: +function parent() { + using child1 = someChild(); + using child2 = anotherChild(); -1. **Pure Phase**: Resource functions run and compute derived values -2. **Render Phase**: Template rendering consumes resource values -3. **Post-Render Phase**: Effects and cleanup logic execute - -```js -const UIResource = resource(({ on, use }) => { - // PURE PHASE: Calculations and data preparation - const data = use(DataSource); - const formattedData = () => formatForDisplay(data.current); - - // RENDER PHASE: Template consumes formattedData - - // POST-RENDER PHASE: Side effects via cleanup - on.cleanup(() => { - // Analytics, logging, or other side effects - trackResourceUsage('UIResource', data.current); + return () => ({ + child1, + child2, }); - - return formattedData; -}); +} ``` -This phased approach ensures predictable execution order and enables advanced features like React's concurrent rendering patterns. - -#### The Principle: "What Can Be Derived, Should Be Derived" +This looks fairly similar to our resources, and at a high-level, might behave similarily. +But a key difference is that each `use` gets its own cache, so `child1` and `child2` can invalidate independently -- whereas all of `parent` would invalidating in the Explicit Resource Management example. -Resources embody the fundamental reactive principle that **state should be minimized and derived values should be maximized**. This leads to more predictable, testable, and maintainable applications: - -```js -// AVOID: Manual state synchronization -const BadPattern = resource(({ on }) => { - const firstName = cell('John'); - const lastName = cell('Doe'); - const fullName = cell(''); // Redundant state! - - // Manual synchronization - error prone - on.cleanup(() => { - // Complex logic to keep fullName in sync... - }); - - return { firstName, lastName, fullName }; -}); +
-// PREFER: Derived values -const GoodPattern = resource(() => { - const firstName = cell('John'); - const lastName = cell('Doe'); - - // Derived value - always consistent - const fullName = () => `${firstName.current} ${lastName.current}`; - - return { firstName, lastName, fullName }; -}); -``` +The ownership model ensures that: +- Disposal propagates down through the resource tree +- No manual `willDestroy` or cleanup management needed -Resources make it natural to follow this principle by: -- **Encouraging functional composition** through the `use()` method -- **Making derivation explicit** through return values -- **Automating consistency** through reactive dependencies -- **Eliminating manual synchronization** through automatic re-evaluation +### Examples -#### Async Resources and Colorless Async +**Data Fetching with Modern Async Patterns** -For async operations, resources support **colorless async**—patterns where async and sync values can be treated uniformly without pervasive `async`/`await` coloring: +[Demo here with ember-resources](https://limber.glimdown.com/edit?c=JYWwDg9gTgLgBAbzlApgZwgVygYxQGjjwBti4BfOAMyghDgHIUQAjFKAWlQ2zzQYDcAKCFVMAOxwxgEcXABKzCDBQARAIYx1ACmzEAlIiFxkKGNjncsuFNu1JZFQwF4AfEZMmcstPF%2BaUOGciFFJ7OGIIdQATYHEAcwAuOBgoTAI4aM11ZPFMUkJ2Wihc-LJyfWFPIh94b3FUiFJ2ILhxFAB3OABBFmgYAGFZRuaobUrjOEmTWQA6HGIUdTywOxd3epHFqFn1PthxiZNpuAB6U7gAZRwACxRo-Pu4FAA3dWJMTRk5DmozW7g-lgaDgoBA92AAWIAE8UhA4OoXhBgNE4B0AlAqO9iGgTlR-jddFBiIQkGhgPFxO9kptaKNZuTKe8nCcTLMYHdxNpuJBxGhAm4PNVPMAqHBtABCHk%2BFCzCAAa0MCFZwo5tC67S6AFEoMVtAADAASABVjQAFOAAEgQ0r5sv85jQ5GS1tt-IZWkdxpQAA8YOR9UdhSZyCqTKhzFBLOhee6AFYYLlB6oVFXsznaLJaILuB32szhSIxOJJajvfmELPqQq66ClUhOfRpnCaW7aIrQHNC4PnOAAeXEMLgmDAWcCedBYo5gVQAEd0r40eo0OIGPA9v17mHJ%2BKJbSmtsGRSqcRdvsVNElduTHmGQWkEXYglklicRkq-WSc9a1AnFVgxQKoVP%2BJwRhYgKeig-7AUIoZCL2QzgBAaB7IsmTZHAYC0Hw5IJHAvyIsi0QgnOC7wOiKiYtiuL1IuZrYeg-LRBo2bBFYvC2OEmD8k4XbKl4tTIOoHQseorTcbYiggMoajZNoDA3DAMBgGgiTnB0GkMuiYDAOyKC3Kc6g6acYDEMsZj8PoQaTGBUbius3Y1Hy8APlET7xJW2Q1sUFCtFAwmifM2CoA0-7HNUorio%2BJaGLZchkp63HJAw0UJAwFBhTu7Y-rFZjgQlmhJYwHZQAwhDgmgKHxCgyQlbMFVVYE5DCKBeV2fxwoOkVDBoJgOA4WVKpYRAOH3KJyRVrM3D5DAaD1UZ2jACo9CCvY26zBtS3MPg269hg4IItEsTSLIzJVnAdyoHAnb7WYNwlkBVknM1kwvTB8EXMadw8eoV3iMobQoPcTyyHgF1KSpamnPES03JgLDzHQpzMGwUAJqcmI4GgJllKcACMeMAEyEx9oIgqApnMCgDRbhT-SIKYPA2AAYuoUjQLClA0HQxWsOwXDoNYfCCEI7Es2zMAc9oUkyaJExCAAPMtpkBK4kwIAgABSlx9gAch6UAlqKsLaPRI2MWN2SGHkDYAMzkKGCunMrZkqK4QA&format=gjs) ```js -const AsyncData = resource(({ on }) => { - const state = cell({ loading: true, data: null, error: null }); - const controller = new AbortController(); - - on.cleanup(() => controller.abort()); - - fetchData({ signal: controller.signal }) - .then(data => state.set({ loading: false, data, error: null })) - .catch(error => state.set({ loading: false, data: null, error })); - - return state; -}); +import { resource, cell } from '@ember/reactive'; -// Usage is the same whether data is sync or async -const ProcessedData = resource(({ use }) => { - const asyncData = use(AsyncData); +function RemoteData(url) { + return resource(({ on }) => { + const state = cell({ loading: true, data: null, error: null }); + const controller = new AbortController(); - return () => { - const { loading, data, error } = asyncData.current; - if (loading) return 'Loading...'; - if (error) return `Error: ${error.message}`; - return processData(data); - }; -}); -``` - -This approach avoids the "function coloring problem" where async concerns leak throughout the application, instead containing them within specific resources while maintaining uniform composition patterns. - -These advanced concepts work together to make resources not just a convenience feature, but a foundational primitive that enables sophisticated reactive architectures while remaining approachable for everyday use. By implementing these patterns, Ember's resource system positions itself at the forefront of modern reactive programming, providing developers with tools that are both powerful and intuitive. - -### .current Collapsing and Function Returns - -A key ergonomic feature of resources is **automatic `.current` collapsing** in templates and with the `@use` decorator. When a resource returns a `cell` or reactive value, consumers don't need to manually access `.current`: - -```js -const Time = resource(({ on }) => { - const time = cell(new Date()); - const timer = setInterval(() => time.set(new Date()), 1000); - on.cleanup(() => clearInterval(timer)); - return time; // Returns a cell -}); - -// Template usage - .current is automatic - - -// @use decorator - .current is automatic -export default class MyComponent extends Component { - @use time = Time; + on.cleanup(() => controller.abort()); - // No .current needed -} - -// Manual usage - .current is explicit -export default class MyComponent extends Component { - time = use(this, Time); + // Scheduled evaluation - fetch starts immediately to avoid waterfalls + fetch(url, { signal: controller.signal }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response.json(); + }) + .then(data => state.set({ loading: false, data, error: null })) + .catch(error => { + // Only update state if the request wasn't aborted + if (!controller.signal.aborted) { + state.set({ loading: false, data: null, error }); + } + }); - // .current needed + return state; + }); } -``` - -**Alternative Pattern: Function Returns** - -Resources can also return functions instead of cells, which provides a different composition pattern: - -```js -const FormattedTime = resource(({ use }) => { - const time = use(Time); - - // Return a function that computes the formatted time - return () => time.current.toLocaleTimeString(); -}); - -// Usage is identical regardless of return type - -``` - -Function returns are particularly useful for: -- **Derived computations** that transform reactive values -- **Conditional logic** that depends on multiple reactive sources -- **Complex formatting** or data transformation -- **Memoization patterns** where you want to control when computation occurs - -The choice between returning cells and returning functions depends on whether the resource primarily holds state (use cells) or computes derived values (use functions). - -### Examples - -**Data Fetching with Modern Async Patterns** - -```js -const RemoteData = resource(({ on }) => { - const state = cell({ loading: true, data: null, error: null }); - const controller = new AbortController(); - - on.cleanup(() => controller.abort()); - - // Scheduled evaluation - fetch starts immediately to avoid waterfalls - fetch(this.args.url, { signal: controller.signal }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response.json(); - }) - .then(data => state.set({ loading: false, data, error: null })) - .catch(error => { - // Only update state if the request wasn't aborted - if (!controller.signal.aborted) { - state.set({ loading: false, data: null, error }); - } - }); - - return state; -}); // Composable data processing - avoids request waterfalls const ProcessedData = resource(({ use }) => { - const rawData = use(RemoteData); + const rawData = use(RemoteData('https://www.swapi.tech/api/planets')); return () => { const { loading, data, error } = rawData.current; if (loading) return { status: 'loading' }; if (error) return { status: 'error', message: error.message }; - + return { status: 'success', - processedData: data.map(item => ({ + processedData: data.results.map(item => ({ ...item, - timestamp: new Date(item.createdAt).toLocaleDateString() + // some additional data here or something })) }; }; }); + + ``` **WebSocket Connection with Reactive State Management** @@ -1349,6 +1051,8 @@ const WebSocketConnection = resource(({ on, owner }) => { **Reactive DOM Event Handling with Debouncing** +[Demo here with ember-resources](https://limber.glimdown.com/edit?c=JYWwDg9gTgLgBAbzlApgZwgVygYxQGjjwBti4BfOAMyghDgHIUQAjFKAWlQ2zzQYDcAKCE4IAOzTwA6sHEATCAHcAysABeKOAF5k6LLhQAKI0gkUAlDoB8iIXCISpcNBq26SxU-YdwlweRgACwAuPzlFJQA6OXF2WUCg-B8HIJRgAHMgmDD-BWUY8TioAAl0rJhk3zgAQzQwFBwYACUamGAIXIiC2PiA4LgAenD86N7S8uyfcgthH0HhgBEUFixxPHk4TDB5Nq0YCFqANwgAuBQADz5XI61ULhQFdjQfYhR4dpAULBgASXlhA4xJJ4NtdjAUGpNDo4EYrNpbAgUkQ3jUoAAVUDfTAwIyfbF-eSzZH4n7-GFod6Yr4-EzwxHIoFOeD%2BRIwvKRQrFBLBQHVRwguBpTLZdndMZFdhlEUwPnVVyaKKU3FI-m%2BVnBKpqoWTSqM3x1BpNVrtTrhNnDYUVfUzOUUQgAVgADMSHOQ5g4OQUavJ5ABRW7iGAAGWAUke7CMDG4bgYhDBeyhBEQcDAdRuKDCMCgmC0tp8PgkURwqPE2zpNjs1S90VQIAgtwDjxDYYhxSjMc0ca2O0TbldvhLKDR1IJeKxZKJfPzPlQMGw4hcbmEM4WcGWUGAt02LFQNQA1pA5PBuAY8KJmXAAEJ7w%2BnIO-cRUQ66U%2B8YymLaUyyV1UC5w1kmMKYJSRiyKMSYDrO7wLrC9JVoOl5IBqQQUGKEFuMW2CoEGHq%2BMAVCwihcAADxwAA7AAbAAHFYc6wUgu7Dnex5hAwtxQAAni4IA1KQDAUHaBFEf0qFkQAjE6ABMAAsdEwVAi6MbeR5BmxaC8fxgnIsJRjERJlFOi6ejzopKZMQeqk5IwXzyMAmAgAJ7rIsi9FmcpzFWWx8joPuBxgE504rsSQgkRC4DEHs1g%2BAgCAAFIqAA8gAckq2ZyBkBHcUYN6efefxPhAVhlqQcAAMzkOQoWDOFYCRRC1hAA&format=gjs) + ```js const WindowSize = resource(({ on }) => { const size = cell({ @@ -1369,7 +1073,7 @@ const WindowSize = resource(({ on }) => { height, aspectRatio: width / height }); - }, 150); + }, 50); }; window.addEventListener('resize', updateSize, { passive: true }); @@ -1396,34 +1100,6 @@ const BreakpointInfo = resource(({ use }) => { }); ``` -**Hierarchical Composition**: Build complex resources from simpler ones using reactive ownership: - -```js -const UserSession = resource(({ use, owner }) => { - const auth = owner.lookup('service:auth'); - const currentUser = use(CurrentUser); - const preferences = use(UserPreferences); - - return () => ({ - user: currentUser.current, - preferences: preferences.current, - isAdmin: auth.hasRole('admin', currentUser.current) - }); -}); - -const CurrentUser = resource(({ on, owner }) => { - const session = owner.lookup('service:session'); - const userData = cell(session.currentUser); - - const handleUserChange = () => userData.set(session.currentUser); - session.on('userChanged', handleUserChange); - on.cleanup(() => session.off('userChanged', handleUserChange)); - - // ".current" is special and is automatically collapsed when rendered. - // otherwise you could return () => userDate.current; - return userData; -}); -``` **Parallel Data Loading**: Avoid waterfalls by composing resources that run async independently: @@ -1432,18 +1108,26 @@ const CurrentUser = resource(({ on, owner }) => { ```js const DashboardData = resource(({ use }) => { - // All three resources start fetching in parallel + // All three resources start fetching in parallel. + // A key thing is that they are required to be accessed in order to start their request. const userData = use(UserData); const analytics = use(AnalyticsData); const notifications = use(NotificationData); - return () => ({ - user: userData.current, - analytics: analytics.current, - notifications: notifications.current, - get hasUnreadNotifications() { - return notifications.current.unreadCount > 0; - } + // @cached shorthand + return () => { + // Can access data here + let user = userData.current; + + // When any of the 3 requests invalidates, this object will be dirtied as percieved by consumers + return { + user, + analytics: analytics.current, + notifications: notifications.current, + get hasUnreadNotifications() { + return notifications.current.unreadCount > 0; + } + }; }); }); ``` @@ -1466,6 +1150,9 @@ const ResilientDataLoader = resource(({ on }) => { on.cleanup(() => controller.abort()); const fetchWithRetry = async (attempt = 0) => { + // When running code that changes state in the resource body, + // it's crucial to detach from auto-tracking via an `await` or some other means. + // (in the future, on.sync may help out here) try { const response = await fetch('/api/critical-data', { signal: controller.signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -1854,29 +1541,1285 @@ This is reactive because the construction of `State` does not require the value In this example, `state.one` and `state.two` are _individually_ reactive. and `state.combined` entantgles with both. +### Core Primitives -### Resources +conceptual primitives, even below the "reactive" primitives. -TODO: (adapt from the ember-resources documentation) +- values + A value is the most basic kind of reactive primitive. It's a place to store data that can be updated atomically. Updates to values take effect immediately. +- functions + JavaScript has a built in mechanism for computing values based on other values: functions. Supporting functions as a reactive primitive, as well as their arguments, is essential for reducing the number of abstractions folks need to learn. -### When _not_ to use a resource +- functions with cleanup + Many things in a JavaScript app require cleanup, but it is often forgotten about, leading to memory leaks, increased network activity, and increased CPU usage. This includes handling timers, intervals, fetch, sockets, etc. + +### Values -- When there is no cleanup or lifecycle management needed. - For example, in state containers, you may just want a class. Even if you need to [link](https://github.com/emberjs/rfcs/pull/1067) for service access, managing a class may be easier. - - ```js - function Foo(countFn) { - return resource(({ use }) => { - let nestedState = use(OtherResource(() => countFn())); +This is a reactive value. +```js +const greeting = cell('Hello there!'); +``` +It can be read and updated, just like a `@tracked` function. - return { - get isLoading() { - return nestedState.isPending; - }, - get result() { - return nestedState.value; - }, +Here is an [interactive demo](https://tutorial.glimdown.com/2-reactivity/1-values) demonstrating how `cell` can be used anywhere (in this case, in module-space[^module-space]) + +
Code for the demo + +```gjs +import { cell } from '@ember/reactive'; + +const greeting = cell("Hello there!"); + +// Change the value after 3 seconds +setTimeout(() => { + greeting.current = "General Kenobi!"; +}, 3000); + + +``` + +
+ + +[^module-space]: Even though we can define state in module-space, you typically do not want to do so in your apps. Adding state at the module level is a "module side-effect", and tree-shaking tools may not tree-shake if they detect a "module side-effect". Additionally, when state is, in some way, only created within the context of an app, that state is easily reset between tests (assuming that app state is not shared between tests). + +> **Note**
+> Cells do not _replace_ `@tracked` or `TrackedObject` / `TrackedArray` or any other reactive state utility you may be used to, but they are another tool to use in your applications and libraries and are otherwise an implementation detail of the more complex reactive data-structures. + +
Deep Dive: Re-implementing @tracked + +When framing reactivity in terms of "cells", the implementation of `@tracked` could be thought of as an abstraction around a `getter` and `setter`, backed by a private `cell`: + +```js +class Demo { + #greeting = cell('Hello there!'); + + get greeting() { + return this.#greeting.current; + } + set greeting(value) { + this.#greeting.set(value); + } +} +``` + + +And then actual implementation of the decorator, which abstracts the above, is only a handful of lines: + +```js +function tracked(target, key, descriptor) { + let cache = new WeakMap(); + + let getCell = (ctx) => { + let reactiveValue = cache.get(ctx); + + if (!reactiveValue) { + cache.set(ctx, reactiveValue = cell(descriptor.initializer?.())); + } + + return reactiveValue; + }; + + return { + get() { + return getCell(this).current; + }, + set(value) { + getCell(this).set(value); + } + } +} +``` + +Note that this decorator style is using the [Stage 1 / Legacy Decorators](https://github.com/wycats/javascript-decorators/blob/e1bf8d41bfa2591d949dd3bbf013514c8904b913/README.md) + +See also [`@babel/plugin-proposal-decorators`](https://babeljs.io/docs/babel-plugin-proposal-decorators#version) + + +
+ +One huge advantage of this way of defining the lowest level reactive primitive is that we can escape the typical framework boundaries of components, routes, services, etc, and rely every tool JavaScript has to offer. Especially as Starbeam is being developed, abstractions made with these primitives can be used in other frameworks as well. + + +Here is an [interactive demo showcasing `@tracked`](https://tutorial.glimdown.com/2-reactivity/2-decorated-values), but framed in way that builds off of this new "value" primitive. + +
Code for the demo + +```gjs +import { tracked } from '@glimmer/tracking'; + +class Demo { + @tracked greeting = 'Hello there!'; +} + +const demo = new Demo(); + +// Change the value after 3 seconds +setTimeout(() => { + demo.greeting = "General Kenobi!"; +}, 3000); + + +``` + +
+ +### Functions + +This is a reactive function. + +```js +function shout(text) { + return text.toUpperCase(); +} +``` +It's _just a function_. And we don't like to use the word "just" in technical writing, but there are honestly 0 caveats or gotchyas here. + +Used in Ember, it may look like this: +```js +function shout(text) { + return text.toUpperCase(); +} + + +``` + +The function, `shout`, is reactive: in that when the `@greeting` argument changes, `shout` will be re-called with the new value. + + +Here is an interactive demo showcasing how [functions are reactive](https://tutorial.glimdown.com/2-reactivity/4-functions) + +
Code for the demo + +```gjs +import { cell } from '@ember/reactive'; + +const greeting = cell("Hello there!"); +const shout = (text) => text.toUpperCase(); + +// Change the value after 3 seconds +setTimeout(() => { + greeting.current = "General Kenobi!"; +}, 3000); + + +``` + +
+ + +### Functions with cleanup + +_Why does cleanup matter?_ + +Many things in a JavaScript app require cleanup. We need to cleanup in order to: +- prevent memory leaks +- reduce unneeded network activity +- reduce CPU usage + +This includes handling timers, intervals, fetch, sockets, etc. + +_Resources_ are functions with cleanup, but cleanup isn't all they're conceptually concerned with. + +> +> Resources Convert Processes Into Values +> +> Typically, a resource converts an imperative, stateful process. +> That allows you to work with a process just like you'd work with any other reactive value. +> + +For details on resources, see the [Resources chapter](./resources.md). + +Here is an interactive demo showcasing how [resources are reactive functions with cleanup](https://tutorial.glimdown.com/2-reactivity/5-resources) + +
Code for the demo + +```gjs +import { resource, cell } from '@ember/reactive'; + +const Clock = resource(({ on }) => { + let time = cell(new Date()); + let interval = setInterval(() => time.current = new Date(), 1000); + + on.cleanup(() => clearInterval(interval)); + + return () => time.current; +}); + + +``` + +
+ + +# Resources + + +> [!NOTE] +> A resource is a reactive function with cleanup logic. + +Resources are created with an owner, and whenever the owner is cleaned up, the resource is also cleaned up. This is called ownership linking. + +Typically, a component in your framework will own your resources. The framework renderer will make sure that when your component is unmounted, its associated resources are cleaned up. + +
+Resources Convert Processes Into Values + +Typically, a resource converts an imperative, stateful process, such as an asynchronous request or a ticking timer, into a reactive value. + +That allows you to work with a process just like you'd work with any other reactive value. + +This is a very powerful capability, because it means that adding cleanup logic to an existing reactive value doesn't change the code that works with the value. + +The only thing that changes when you convert a reactive value into a resource is that it must be instantiated with an owner. The owner defines the resource's lifetime. Once you've instantiated a resource, the value behaves like any other reactive value. + +
+ +## A Very Simple Resource + +To illustrate the concept, let's create a simple resource that represents the current time. + +```js +import { cell, resource } from "@ember/reactive"; + +export const Now = resource(({ on }) => { + const now = cell(Date.now()); + + const timer = setInterval(() => { + now.set(Date.now()); + }); + + on.cleanup(() => { + clearInterval(timer); + }); + + return now; +}); +``` + +To see this code in action, [checkout the live demo](https://limber.glimdown.com/edit?c=MQAggiDKAuD2AOB3AhtAxgCwFBYCIFMBbWAOwGdoAnVAS1JFgDMRkQAlfM2AV0rXxDQMqEAGt8%2BeGUHU0ohs1yp8AOhKxELaQEduNOVpA1oINMhI4ABtYDmAK2kAbGgDd8WGoXixKJgN4glJw8fPgANKb4jo4gAL4gjJSwhCAA5EQARviUALRBXLz8ZKkA3Dj4AB7evqakFCAAchogALyBwYX4ABRdAfSxAJStAHwgflggteQm6ppt-NFdStCqs10DA2UgE1P10J7ZrSBk%2BNAAkiQrlC7Ijj1DLaPjk5OzKifQS8pqGuubO4MtjtSCo0I58OZuPB7iMxjtJmCIZQLlcbnd9oRsv9JoCdjsgtBeCQQLMyricAAeFZeRzKYY7M4mGhkABcICpB2Gfj8TUQsViFIA9Bj8PShdT4LSVvTrJYgA&format=glimdown). + +> **:bulb:**
+> A resource's return value is a reactive value. If your resource represents a single cell, it's fine to return it directly. It's also common to return a function which returns reactive data -- that depends on reactive state that you created inside the resource constructor. + +When you use the `Now` resource in a component, it will automatically get its lifetime linked to that component. In this case, that means that the interval will be cleaned up when the component is destroyed. + +The `resource` function creates a resource Constructor. A resource constructor: + +1. Sets up internal reactive state that changes over time. +2. Sets up the external process that needs to be cleaned up. +3. Registers the cleanup code that will run when the resource is cleaned up. +4. Returns a reactive value that represents the current state of the resource as a value. + +In this case: + +| internal state | external process | cleanup code | return value | +| ---- | ---- | ---- | ---- | +| `Cell` | `setInterval` | `clearInterval` | `Cell` | + + +
Resource's values are immutable + +When you return a reactive value from a resource, it will always behave like a generic, immutable reactive value. This means that if you return a `cell` from a resource, the resource's value will have `.current` and `.read()`, but not `.set()`, `.update()` or other cell-specific methods. + +If you want your resource to return a value that can support mutation, you can return a JavaScript object with accessors and methods that can be used to mutate the value. + +This is an advanced use-case because you will need to think about how external mutations should affect the running process. + +
+ +## A Ticking Stopwatch + +Here's a demo of a `Stopwatch` resource, similar to the above demo. +The main difference here is that the return value is a function. + +```js +import { resource, cell } from '@ember/reactive'; + +const formatter = new Intl.DateTimeFormat("en-US", { + hour: "numeric", + minute: "numeric", + second: "numeric", + hour12: false, +}); + +export const Stopwatch = resource((r) => { + const time = cell(new Date()); + + const interval = setInterval(() => { + time.set(new Date()); + }, 1000); + + r.on.cleanup(() => { + clearInterval(interval); + }); + + return () => { + const now = time.current; + + return formatter.format(now); + }; +}); +``` + +To see this code in action, [checkout the live demo](https://limber.glimdown.com/edit?c=MQAggiDKAuD2AOB3AhtAxgCwFBYCIFMBbWAOwGdoAnVAS1JFgDMRkQAlfM2AV0rXxDQMqEAGt8%2BeGUHU0ohsyEC0vSvhLRBNQvgA0LEgBMQjWJUKppNTYgzqQao-ko0SAcwB0OAAa%2B3AK2kAGxoAN3wsbXgzTQBvB04ePj0QfiCgkABfE0pYQhAAciIAI2cAWjUuXn4yAoBuHDRSChMzC2hoZxAAXhASfEQQAEkNII9cVHwAFW18ADE21AAKACJ1MoBVSBX9WKwQEAwkgC4QFZJuHRc0Hf2QQlduTtPzy%2BcaG907snwmoxeLlcPrcDkdeABGABMp0YyCCPy%2BmQAlA0sH8WjAECh0Bgegkqsklkt4vRkT0AHwgPYHdGaaCzPFpIJLfqDCadJZIlF3WkgVydSihOF4n7QEYCoXMzkUrQ6DwqSiOTS9Vkgdn4Tn6cEABl13LupHlQXwyAu8CJSJlaGNyEo4uckqW-IdcK5qIOamgvBIIGl3Up1IOqWamhIsEGvXpcoVSvdQc93ta5lQAo8pmT0BZ4e5B0yWGRqIAPJ1CPAgpNyXchpoaGRTsXZuTYrFMUhUJhMplCwB6KP4Ss9ktlitYXzeIA&format=glimdown). + +A description of the `Stopwatch` resource: + +| internal state | external process | cleanup code | return value | +| ---- | ---- | ---- | ---- | +| `Cell` | `setInterval` | `clearInterval` | `string` | + +The internals of the `Stopwatch` resource behave very similarly to the `Now` resource. The main difference is that the `Stopwatch` resource returns the time as a formatted string. + +From the perspective of the code that uses the stopwatch, the return value is a normal reactive string. + +## Reusing the `Now` Resource in `Stopwatch` + +You might be thinking that `Stopwatch` reimplements a whole bunch of `Now`, and you ought to be able to just use `Now` directly inside of `Stopwatch`. + +You'd be right! + +```js +const formatter = new Intl.DateTimeFormat("en-US", { + hour: "numeric", + minute: "numeric", + second: "numeric", + hour12: false, +}); + +const Stopwatch = resource(({ use }) => { + const time = use(Now); + + return () => formatter.format(time.current); +}); +``` + +The `Stopwatch` resource instantiated a `Now` resource using its use method. That automatically links the `Now` instance to the owner of the `Stopwatch`, which means that when the component that instantiated the stopwatch is unmounted, the interval will be cleaned up. + +## Using a Resource to Represent an Open Channel + +Resources can do more than represent data like a ticking clock. You can use a resource with any long-running process, as long as you can represent it meaningfully as a "current value". + +
Compared to other systems: Destiny of Unused Values + +You might be thinking that resources sound a lot like other systems that convert long-running processes into a stream of values (such as observables). + +While there are similarities between Resources and stream-based systems, there is an important distinction: because Resources only produce values on demand, they naturally ignore computing values that would never be used. + +This includes values that would be superseded before they're used and values that would never be used because the resource was cleaned up before they were demanded. + +**This means that resources are not appropriate if you need to fully compute values that aren't used by consumers.** + +In stream-based systems, there are elaborate ways to use scheduling or lazy reducer patterns to get similar behavior. These approaches tend to be hard to understand and difficult to compose, because the rules are in a global scheduler, not the definition of the stream itself. These patterns also give rise to distinctions like "hot" and "cold" observables. + +On the other hand, Resources naturally avoid computing values that are never used by construction. + +TL;DR Resources do not represent a stream of values that you operate on using stream operators. + +> **:key: Key Point**
+>resources represent a single reactive value that is always up to date when demanded. + +This also allows you to use resources and other values interchangably in functions, and even pass them to functions that expect reactive values. + +
+ +Let's take a look at an example of a resource that receives messages on a channel, and returns a string representing the last message it received. + +In this example, the channel name that we're subscribing to is dynamic, and we want to unsubscribe from the channel whenever the channel name changes, but not when we get a new message. + +```js +import { resource, cell } from '@ember/reactive'; + +function ChannelResource(channelName) { + return resource(({ on }) => { + const lastMessage = cell(null); + + const channel = Channel.subscribe(channelName); + + channel.onMessage((message) => { + lastMessage.set(message); + }); + + on.cleanup(() => { + channel.unsubscribe(); + }); + + return () => { + const prefix = `[${channelName}] `; + if (lastMessage.current === null) { + return `${prefix} No messages received yet`; + } /*E1*/ else { + return `${prefix} ${lastMessage.current}`; + } + }; + }); +} +``` + +To see this code in action, [checkout the live demo](https://limber.glimdown.com/edit?c=MQAgMglgtgRgpgJxAUQCYQC4HsEChcAicUWAdgM4YICGGEZIWAZiNSAEpzlYCuCAxnBAYAFrRAI4AB0nk4pDOWEihAIhKUJcQQpBQu5agHM4qrYIgA3OKkalWIAOpwYAZSz8A1nAwgAwmKkpHAANgB0%2BAAG0UYAVkohVnC40FI4vgDeWtx8ggBi1PzYCACeADTZvAJwFYIhISAAviBMCFhQIADkxPAIALSyVYLknQDc%2BAD0AFRTIAAKkhjytgHUQaEgqSHE8hi09PZMOCAwkpaYJSBTE7j8ZJqr6w0AvCAZuCAg5Dww5PwIEHgAC4QAAKfiBYIhABy1H0AEoQM8AHxvD6fEDbXz8aj1GCFTxKV4AbQAuuN0Z8JhMvtAeCFaEJJBZzqQjF8PN5fPpyIYTEopIh6OgcfUSpTMT5NgpEJZcUivj4AJIyhBykKg0GIlFojEYrGbWyvACytBEYTaPFIqFBptEFrWqHaWquIAAHAAGREAahAAEZxnrPgaQHwXiBIiIMBgpOQgdTyAB3ahSCBhVBwSwTFMQCYAEgyELWUNh%2Bka%2BYyEFQjUi%2BCDLR8ENBYfhEr1YVE8lBsjSFCEOp79zgYXiZC1rfrnw7KlIoNQtDYOvek4xopC%2BK85DCUBT4Nx64JSNRa43njnC7CpDhcHhE8njXhgYxjQqfo978fdYxiz49mXQbIY0DGMOAQVBJhSG1Y991PLcpB4cgRHAyCyjbUMKB%2BP4AWBMEoN1ScTwJIkQDJJ9634bZqAQFUljVXFQQgVV1U-etGglNjPjYtj8DuChfEeKFOByaoFUGXI4AKIocBKTUiyeUsbyPfCfwQewxOqTUsgYB8lP-T5eM0BlKCA3kQIVOoNVIekQhYiUDOxSENleATQjCb5fn%2BQE4HBRyYWvWy9TkqEwkA4CTE1Hk%2BUUpc0KMjATKityfFBSKQJY58AoxMgwgouA1h4KRNTwvTV18sIrXcrCvK1Mimkyz4VPsF0YvI%2B5fBkOAmAgAAPBVImJAsgtCBTGlJCNas%2BCAWFBOKEpAnK%2BEkXRnhWkArPqRESr1RqIwLDquu65poSwPQwq4cw4CSWwSh8SIJqaEBpmQP1rhAUI5Hw%2BsdsiPbJAO5oC1ms6FoQJaMBre6OOfJ8H3GWH8AAHiWKApAZJZkXRBH0EsL4MBKbZnlUGAcAzBAQT9KReu4RJUFGEApGoVB0DZcnJCgUZVAxvUMgyFyQiEoY1EFLBUdMRooYRiZsYxyXkdRxkMeiWsgA&format=glimdown) + +`ChannelResource` is a JavaScript function that takes the channel name as a reactive input and returns a resource constructor. + +That resource constructor starts by subscribing to the current value of the `channelName`, and then telling Ember to unsubscribe from the channel when the resource is cleaned up. + +It then creates a cell that holds the last message it received on the channel, and returns a function that returns that message as a formatted string (or a helpful message if the channel hasn't received any messages yet). + +At this point, let's take a look at the dependencies: + +```mermaid +flowchart LR + ChannelResource-->channelName + subgraph ChannelResource + lastMessage + end + output-->channelName + output-->lastMessage + + style channelName fill:#8888ff,color:white + style output fill:#8888ff,color:white + style lastMessage fill:#8888ff,color:white +``` + +Our output depends on the channel name and the last message received on that channel. The lastMessage depends on the channel name as well, and whenever the channel name changes, the resource is cleaned up and the channel is unsubscribed. + +If we receive a new message, the lastMessage cell is set to the new message. This invalidates lastMessage and therefore the output as well. + +```mermaid +flowchart LR + ChannelResource-->channelName + subgraph ChannelResource + lastMessage + end + output-->channelName + output-->lastMessage + + style channelName fill:#8888ff,color:white + style output fill:#ff8888,color:black + style lastMessage fill:#ff8888,color:black +``` + +However, this does not invalidate the resource itself, so the channel subscription remains active. + +On the other hand, if we change the channelName, that invalidates the ChannelResource itself. + +```mermaid +flowchart LR + ChannelResource-->channelName + subgraph ChannelResource + lastMessage + end + output-->channelName + output-->lastMessage + + style channelName fill:#ff8888,color:black + style output fill:#ff8888,color:black + style lastMessage fill:#ff8888,color:black + +``` + +As a result, the resource will be cleaned up and the channel unsubscribed. After that, the resource will be re-created from the new channelName, and the process will continue. + + +> **:key: Key Point**
From the perspective of the creator of a resource, the resource represents a stable reactive value. + +
Under the hood + +Under the hood, the internal `ChannelResource` instance is cleaned up and recreated whenever its inputs change. However, the resource you got back when you created it remains the same. +
+ + +---------------------------------------- + + + +[^starbeam]: These docs have been adapted from the [Starbeam](https://www.starbeamjs.com/guides/fundamentals/resources.html) docs on Resources. + +[^copying]: while ~90% of the content is copied, adjustments have been made for casing of APIs, as well as omissions / additions as relevant to the ember ecosystem right now. Starbeam is focused on _Universal Reactivity_, which in documentation for Ember, we don't need to focus on in this document. Also, mega huge thanks to [@wycats](https://github.com/wycats) who wrote most of this documentation. I, `@nullvoxpopuli`, am often super stressed by documentation writing (at least when stakes are high) -- I am much happier/relaxed writing code, and getting on the same page between our two projects. + +### In Strict Mode / `