Skip to content

RFC(@ngrx/signals): Resource Integration Alternative #5129

@kobi2294

Description

@kobi2294

Which @ngrx/* package(s) are relevant/related to the feature request?

signals

Information

Introduction

This RFC proposes a different integration model between Angular Resource and SignalStore.

Today, Resource combines two responsibilities in one object:

  1. async orchestration (loading lifecycle, race handling, reload semantics, errors)
  2. state ownership (value and related status state)

That coupling is convenient, but it limits flexibility in real store-driven apps.

Teams often need explicit domain policies:

  • what value should be visible during reload
  • what value should be visible on source changes
  • how error states recover
  • whether local overrides should win over remote updates

It also makes composition harder when remote data must stay in sync with local state,
such as selected items, optimistic edits, or UI-specific flags.

SignalStore already provides a predictable model for state transitions:
methods represent events, patchState and updaters define transitions,
and computed signals derive read models.

The core idea is to keep that boundary:
Resource should handle transport and asynchrony,
while SignalStore remains the single owner of application state.

This keeps Resource strengths, reduces RxJS-heavy boilerplate,
and preserves explicit, testable, domain-specific state transitions.

Syntax

The API can be offered in three variants, ordered from minimal code and minimal flexibility to more verbose code with maximum control.

Basic Usage

In the simplest form, the consumer defines the resource and the feature takes care of wiring it into the store.

withResources((store) => ({
	user: httpResource(() => `https://myhost/api/user/${store.userId()}`),
}))

This variant would:

  1. add core state fields such as userValue, userStatus, and userError
  2. add computed signals such as userHasValue and userIsLoading
  3. add a private method such as _userReload
  4. update the state through patchState, using the resource values as-is

Custom Updaters

The second variant gives the consumer control over how resource snapshots are translated into state changes, while the feature still performs the actual patchState call.

withResources({
	user: withUpdaters(
		httpResource(() => `https://myhost/api/user/${store.userId()}`),
		(snapshot, state) => newPartialState
	),
})

This is flexible, but a single updater can easily turn into a long chain of status checks. To keep that logic structured, a helper such as resourceUpdaters(...) can provide defaults and let the consumer override only the cases they care about.

withResources({
	user: withUpdaters(
		httpResource(() => `https://myhost/api/user/${store.userId()}`),
		resourceUpdaters(
			resolved((value, state) => ({ userValue: value })),
			error((error, state) => ({ userValue: DEFAULT_USER, userError: error })),
			loading((state) => ({ userValue: state.userValue })),
			reloading((state) => ({ userValue: undefined })),
			local((value, state) => ({ userValue: value }))
		)
	),
})

Each function (such as resolved, error, loading) is a pre-built function that checks for a specific condition and returns either a pratial state, or undefined. The resourceUpdaters function composes them together. This syntax minimizes boilerplate but is also highly extendable.

Consumers should not be required to provide every handler. Any missing case can fall back to the default behavior. It is also possible to provide helpers that cover multiple states, such as busy(...) for both loading and reloading, or newValue(...) for both local and resolved values.

Custom Update Callback

The final variant gives the consumer full control over how state is updated. Instead of the feature calling patchState internally, it invokes user-provided callbacks.

withResources({
	user: withHandlers(
		httpResource(() => `https://myhost/api/user/${store.userId()}`),
		resourceHandlers({
			onResolved: (value, state, store) => updateState(store, 'userLoaded', { userValue: value }),
			onError: (error, state, store) => updateState(store, 'userError', { userError: error }),
		})
	),
})

This should be treated as a last resort. It enables advanced integrations, such as devtools-aware updates, but it also opens the door to anti-patterns. It's important to minimize the logic in the callbacks only to the intended use - update store state in response to resource lifecycle changes.

Describe any alternatives/workarounds you're currently using

No response

I would be willing to submit a PR to fix this issue

  • Yes
  • No

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions