Skip to content

Commit f959699

Browse files
feat: add withResource
This adds the implementation for `withResource` for both named and single resources. The implementation is close to the PR in NgRx but doesn't use any internal types. Because of a bug in `withLinkedState` ngrx/platform#4931, we currently have no override protected for `value`. For more information, check out the docs. Co-authored-by: Michael Small <[email protected]>
1 parent c49ca91 commit f959699

File tree

6 files changed

+1134
-7
lines changed

6 files changed

+1134
-7
lines changed

docs/docs/extensions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ It offers extensions like:
1111
- [DataService](./with-data-service): Builds on top of `withEntities` and adds the backend synchronization to it
1212
- [Immutable State Protection](./with-immutable-state): Protects the state from being mutated outside or inside the Store.
1313
- [~Redux~](./with-redux): Possibility to use the Redux Pattern. Deprecated in favor of NgRx's `@ngrx/signals/events` starting in 19.2
14+
- [Resource](./with-resource): Integrates Angular's Resource into SignalStore for async data operations
1415
- [Reset](./with-reset): Adds a `resetState` method to your store
1516
- [Call State](./with-call-state): Add call state management to your signal stores
1617
- [Storage Sync](./with-storage-sync): Synchronizes the Store with Web Storage

docs/docs/with-resource.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
title: withResource()
3+
---
4+
5+
```typescript
6+
import { withResource } from '@angular-architects/ngrx-toolkit';
7+
```
8+
9+
> **⚠️ Important Note**: This extension is very likely to land in NgRx once Angular's `Resource` enters developer preview. The `withResource` extension provides early access to this functionality and will be maintained for compatibility until the official NgRx implementation is available.
10+
11+
`withResource()` is a feature in NgRx SignalStore that connects Angular's Resource API with the store.
12+
The idea: you can use a store to directly manage async data (like loading from an API), and `withResource()` helps you wire that in.
13+
14+
There are two flavors on how you can use it.
15+
16+
**1. Single Resource flavor**
17+
18+
- The Store implements the type `Resource<T>`.
19+
- That means the store itself exposes the standard resource properties and methods:
20+
- `value()`, `status()`, `error()`, `hasValue`, etc.
21+
22+
**2. Named Resources flavor**
23+
24+
- Instead of making the whole store act as a single resource, you can define multiple named resources within the same store.
25+
- Each resource gets its own name, which is used as a prefix for all its properties and methods.
26+
- For example, if you define a resource named `users`, the store will provide:
27+
- `usersValue()`, `usersStatus()`, `usersError()`, `usersHasValue()`
28+
29+
For named resources, there’s an extra option: you can map them back into the Resource type.
30+
This is useful if you want to treat just that part as a “normal” Angular Resource again — for example, to pass it into a component that expects a `Resource<T>`.
31+
32+
## Basic Usage
33+
34+
The extension supports both single resource integration and multiple named resources, giving you flexibility in how you structure your async data management.
35+
36+
### Single Resource
37+
38+
```typescript
39+
import { withResource } from '@angular-architects/ngrx-toolkit';
40+
import { signalStore, withState } from '@ngrx/signals';
41+
import { httpResource } from '@angular/core';
42+
43+
export const UserStore = signalStore(
44+
withState({ userId: 1 }),
45+
withResource((state) => httpResource(() => `/user/${state.userId}`)),
46+
);
47+
```
48+
49+
The store now provides:
50+
51+
- `value()`: The resource's current value
52+
- `status()`: The resource's current status
53+
- `error()`: Any error that occurred
54+
- `isLoading()`: Whether the resource is currently loading
55+
- `hasValue()`: Type guard to check if the resource has a value
56+
- `_reload()`: Method to reload the resource
57+
58+
### Multiple Named Resources
59+
60+
```typescript
61+
export const UserStore = signalStore(
62+
withState({ userId: undefined as number | undefined }),
63+
withResource(({ userId }) => ({
64+
list: httpResource<User[]>(() => '/users', { defaultValue: [] }),
65+
detail: httpResource<User>(() => (userId === undefined ? undefined : `/user/${userId}`)),
66+
})),
67+
);
68+
```
69+
70+
With named resources, each resource gets prefixed properties:
71+
72+
- `listValue()`, `detailValue()`: Resource values
73+
- `listStatus()`, `detailStatus()`: Resource statuses
74+
- `listError()`, `detailError()`: Resource errors
75+
- `listIsLoading()`, `detailIsLoading()`: Loading states
76+
- `listHasValue()`, `detailHasValue()`: Type guards
77+
- `_listReload()`, `_detailReload()`: Reload methods
78+
79+
## Choosing Between Single and Multiple Resources
80+
81+
- **Single resource:** use when your store works with just one data source.
82+
- **Named resources:** use when your store is larger and manages multiple entities or async operations.
83+
84+
## Component Usage
85+
86+
```typescript
87+
@Component({
88+
selector: 'app-user-detail',
89+
template: `
90+
@if (userStore.isLoading()) {
91+
<div>Loading...</div>
92+
} @else if (userStore.error()) {
93+
<p>An error has happened.</p>
94+
} @else if (userStore.hasValue()) {
95+
<h2>{{ userStore.value().name }}</h2>
96+
<p>{{ userStore.value().email }}</p>
97+
}
98+
`,
99+
})
100+
export class UserDetail {
101+
protected readonly userStore = inject(UserStore);
102+
}
103+
```
104+
105+
## Resource Mapping
106+
107+
For named resources, you can use the `mapToResource` utility to get a properly typed `Resource<T>`:
108+
109+
```typescript
110+
import { mapToResource } from '@angular-architects/ngrx-toolkit';
111+
112+
const store = signalStore(
113+
withState({ userId: undefined as number | undefined }),
114+
withResource(({ userId }) => ({
115+
user: httpResource<User>(() => (userId === undefined ? undefined : `/users/${userId}`)),
116+
})),
117+
);
118+
119+
const userResource = mapToResource(store, 'user');
120+
// userResource now satisfies Resource<User>
121+
```

docs/sidebars.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@ const sidebars: SidebarsConfig = {
1616
// By default, Docusaurus generates a sidebar from the docs folder structure
1717
extensionSidebar: [
1818
'extensions',
19-
'with-data-service',
2019
'with-devtools',
21-
'with-redux',
20+
'with-call-state',
21+
'with-conditional',
22+
'with-data-service',
23+
'with-feature-factory',
24+
'with-immutable-state',
25+
'with-reset',
26+
'with-resource',
2227
'with-storage-sync',
2328
'with-undo-redo',
24-
'with-reset',
25-
'with-immutable-state',
26-
'with-feature-factory',
27-
'with-conditional',
28-
'with-call-state',
29+
'with-redux',
2930
],
3031
reduxConnectorSidebar: [
3132
{

libs/ngrx-toolkit/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ export {
4141
} from './lib/storage-sync/with-storage-sync';
4242
export { emptyFeature, withConditional } from './lib/with-conditional';
4343
export { withFeatureFactory } from './lib/with-feature-factory';
44+
export { mapToResource, withResource } from './lib/with-resource';

0 commit comments

Comments
 (0)