[Closed] RFC: NgRx SignalStore #3796
Replies: 27 comments 74 replies
-
Looks absolutely amazing. Would love to try it out in a project I'm working on. |
Beta Was this translation helpful? Give feedback.
-
This looks incredible and beyond my understanding of signals at the moment. With that in mind just a quick question, if I may(I fully recognize might not be the right place for this).
I’m just thinking of the need/want to now move over existing ngrx store to ngrx signals and how difficult that might be. Both questions above are related to this concern. The while sounds fantastic nonetheless does bring up that concern. Thank you for your time. |
Beta Was this translation helpful? Give feedback.
-
Awesome! 1- is there any plan to make the updaters immutable while writing mutable code, like immer? Anyway, the API looks amazing, congrats and thank you 😍 |
Beta Was this translation helpful? Give feedback.
-
Thank you so much for such a detailed RFC! I guess it took a lot of time to create it (at least it would take a couple of eternities for me). A few questions below, none of them are criticism: I used to keep stores and components in separate files. const [provideUsersStore, injectUsersStore] = signalStore(... and then: @Component({
providers: [provideUsersStore()],
})
export class UsersComponent {
readonly usersStore = injectUsersStore(); it means that both To workaround this, the only way is to use rxEffect - is it a dirty function? I mean, does it have an internal state? ComponentStore.effect uses an internal observable (origin$), will rxEffect use something similar? If yes, then how will it be tied to the component's lifecycle? After adding updaters, you have this comment: // available properties and methods:
// - usersStore.update method
// - usersStore.users: Signal<User[]>
// - usersStore.query: Signal<string>
// - usersStore.filteredUsers: Signal<User[]>
// - usersStore.addUsers: (users: User[]) => void Will they be available because of TS types? Again, there is zero hostility in questions, just some curiosity. |
Beta Was this translation helpful? Give feedback.
-
I would pass every parameter to the function as an array in the first argument. It is easier to add more parameters later if needed without a breaking change. E.g. const [provideUsersStore, injectUsersStore] = signalStore([
withState<UsersState>({ users: [], query: '' }),
withComputed(({ users, query }) => ({
filteredUsers: computed(() =>
users().filter(({ name }) => name.includes(query()))
),
}))
]); |
Beta Was this translation helpful? Give feedback.
-
Thanks for the detailed RFC. I have some points:
|
Beta Was this translation helpful? Give feedback.
-
@markostanimirovic looks amazing, about the Custom Store Features I wanted to have something like this in ngrx for so long Im so glad to see it, I did something similar in a library I build called ngrx-traits (link at the end if you want to have a look ), for the functions that return the SignalStoreFeature (those I call traits in my library), it will be very useful if there was a way to know what other SignalStoreFeature features are present to make them smarter, for example, if I create a withLoadEntities that receives an effect that simply calls the backend as param, and it creates a load method that calls the backend and uses withCallState and withEntities to set to loading when the call starts and call setAll and change the status to loaded when is done. |
Beta Was this translation helpful? Give feedback.
-
👆 RFC and the playground project are updated. Summary:
Thanks everyone for the very helpful feedback! 🙌 |
Beta Was this translation helpful? Give feedback.
-
Nested Signals Currently the Angular team does not offer an official API for nested signals and I assume it could be out of scope for them. I see a perfect fit to include this into a state management solution like the two NgRx concepts. Is this something you plan for this lib, @markostanimirovic? |
Beta Was this translation helpful? Give feedback.
-
First of all the idea looks really good!
One thing I didn't like is the naming of Concerning the idea of store vs feature I would like also to see the ability to use features inside other features in order to reuse smaller parts. So feature could use Generally the export interface CounterParams {
initialCount: number;
}
export class CounterStore {
private state = signal(this.params.initialCount);
public count = computed(() => this.state());
public doubleCount = computed(() => this.count() * 2);
constructor(private params: CounterParams) {}
public increment() {
this.state.update(s => s + 1);
}
public decrement() {
this.state.update(s => s - 1);
}
}
export function provideCounterStore(
provideAs: ProvideAs<CounterStore>,
params: CounterParams
): Provider[] {
const [paramsToken, paramsProvider] = createStoreParams(params);
const counterStoreProvider = createStoreProvider(provideAs, CounterStore, [
paramsToken
]);
return [paramsProvider, counterStoreProvider];
}
// Usage
export const [firstCounterToken, firstCounterProviders] = createStoreInstance(
provideCounterStore,
{ initialCount: 0 }
);
export const [secondCounterToken, secondCounterProviders] = createStoreInstance(
provideCounterStore,
{ initialCount: 1 }
); This way you can create multiple instances and because each "feature" is a store you can inject it in other stores and create a "fractal" store graph. Another option is taking the good from both - features will be small reusable parts which are not standalone - like async state or pagination while stores can be created multiple times in the way I mentioned. |
Beta Was this translation helpful? Give feedback.
-
I really like this RFC! Move state management into next level in signal world. |
Beta Was this translation helpful? Give feedback.
-
Info
Hadrien
…On Mon, Mar 13, 2023, 10:28 Evgeniy OZ ***@***.***> wrote:
Converting objects recursively is an expensive operation
(performance-wise).
Not every field contains numbers and booleans.
If every field will be recursively converted into signals, then users will
have to worry about what values they store - if a value will be some big
object, then it will hit the performance.
—
Reply to this email directly, view it on GitHub
<#3796 (reply in thread)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABPAM3S42ISEIG6UBDEMADTW33SDDANCNFSM6AAAAAAVQ2P2CM>
.
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
TL;DR provide
|
Beta Was this translation helpful? Give feedback.
-
This RFC is incredible -- thanks for the work. There is this section of The portion of the code I was referring to is: ,
withEffects(({ addUsers }) => {
const usersService = inject(UsersService);
// read more about 'rxEffect' in the section below
const loadUsers = rxEffect<void>(
pipe(
exhaustMap(() => usersService.getAll()),
tap((users) => addUsers(users))
)
);
return { loadUsers };
}),
Another thing that came to mind:
One more time, thank you for this well thought RFC 👍🏾🙏 |
Beta Was this translation helpful? Give feedback.
-
As signal integration with store and component store will release in v16. Will signal store also have chance in v16 release? Looking forward to it! |
Beta Was this translation helpful? Give feedback.
-
One comment about the collection for the withEntities I think the generated computed signals will be better if it removes the Entity or Entities from the generated property naming if the collection is added
|
Beta Was this translation helpful? Give feedback.
-
Hey everyone 👋 I've updated the SignalStore playground project. Changes:
Features:
If we apply custom features to the export const UsersStore = signalStore(
{ providedIn: 'root' },
withState({ entities: [] as User[] }),
withCallState(),
withFilter(),
withLoadEntities(UsersService),
withHooks({
// re-fetch users every time when filter signal changes
onInit: ({ loadEntitiesByFilter, filter }) => loadEntitiesByFilter(filter),
})
);
const state = signalState({ m: { s: { t: 10 } }, foo: 'bar' });
console.log(state()); // { m: { s: { t: 10 } }, foo: 'bar' }
console.log(state.m()); // { s: { t: 10 } }
console.log(state.m.s()); // { t: 10 }
console.log(state.m.s.t()); // 10 The state.$update({ foo: 'baz' });
const CounterStore = signalStore(
withState({ count: 0 }),
withSignals(({ count }) => ({
doubleCount: selectSignal(() => count() * 2),
})),
); But it also has another signature inspired by type UsersState = { users: string[]; query: string };
const UsersStore = signalStore(
withState<UsersState>({ users: [], query: '' }),
withSignals(({ users, query }) => ({
filteredUsers: selectSignal(
users,
query,
(users, query) => users.filter((user) => user.includes(query))
),
})),
); I'll update the documentation soon. |
Beta Was this translation helpful? Give feedback.
-
Thank you for the RFC! In the current documentation of NGRX and also here I'm missing the example for a preloading of most frequently used data. Use case:
Questions:
Confusions:
IMHO:
constructor() {
this.usersPreloaded = [
{ id: 1, name: 'Alex' },
{ id: 2, name: 'Brandon' },
{ id: 3, name: 'Marko' },
];
}
find(name: string): Observable<User | null> {
return from(usersPreloaded).pipe(
find((user: User): boolean => user.name === name),
// Do the network request here if empty. No, search in usersMock
// instead of course to stick with the current example of the playground
);
} But now I'm confused how to proceed exactly, see the confusions above. |
Beta Was this translation helpful? Give feedback.
-
Hey everyone 👋 I've updated the prototype again. Changes this time:
Before: export function withX() {
// specify the feature input type if needed
const xFeature = signalStoreFeatureFactory<{
state: { y: number }
}>();
return xFeature(
withState({ x: 0 }),
withSignals(({ x, y }) => ({
z: selectSignal(() => x() + y())
}))
);
} After: export function withX() {
return signalStoreFeature(
{ state: type<{ y: number }>() }, // inspired by props<>()
withState({ x: 0 }),
withSignals(({ x, y }) => ({
z: selectSignal(() => x() + y())
}))
);
} Thanks @gabrielguerrero for this suggestion!
export function withStorageSync(
key: string,
storageFactory = () => localStorage
) {
return signalStoreFeature(
withHooks({
onInit({ $state, $update }) {
const storage = storageFactory();
const initialState = storage.getItem(key);
if (initialState) {
$update(JSON.parse(initialState));
}
effect(() => {
const state = $state();
storage.setItem(key, JSON.stringify(state));
});
},
})
);
}
type UsersState = { users: User[]; loading: boolean };
const UsersStore = signalStore(
withState<UsersState>({ users: [], loading: false }),
withMethods((store, usersService = inject(UsersService)) => ({
loadUsers: rxMethod<void>(
pipe(
tap(() => store.$update({ loading: true })),
exhaustMap(() => usersService.getAll()),
tap((users) => store.$update({ users, loading: false }))
)
),
}))
);
@Component({
template: `
<h1>Users</h1>
<p *ngIf="usersStore.loading()">Loading...</p>
<ul>
<li *ngFor="let user of usersStore.users()">
{{ user }}
</li>
</ul>
`
})
export class UsersComponent implements OnInit {
readonly usersStore = inject(UsersStore);
ngOnInit(): void {
this.usersStore.loadUsers();
}
} |
Beta Was this translation helpful? Give feedback.
-
A few questions I asked before but got lost here so I will ask again:
|
Beta Was this translation helpful? Give feedback.
-
I played around with creating a "signal component store" with the Entity Adaptor. I just want to provide a functional prototype of the current state of signals ( in preview ) integrating with current NgRx functionality to provide food for thought. |
Beta Was this translation helpful? Give feedback.
-
I played with the proposed code and it feels good: signal where we need it and RxJS where it excels! Two things that bugged me:
I learned the difference between type TypeEntity = {
id: number;
name: string;
}
interface InterfaceEntity {
id: number;
name: string;
}
type IsRecord<T> = T extends Record<string, unknown> ? true : false;
type TypeEntityIsRecord = IsRecord<TypeEntity>; // true
type InterfaceEntityIsRecord = IsRecord<InterfaceEntity>; // false |
Beta Was this translation helpful? Give feedback.
-
Hi, I still see a complexity of ngrx to work with it. Is it possible to have something like this store in VueJS with angular signal. https://pinia.vuejs.org/ .For me we dont really need all parts such as Action, Selector, Effect, Reducer. these things are unnecessary in my opinion |
Beta Was this translation helpful? Give feedback.
-
Dynamic Data Structures Tanks for your work. In the meantime I also had some time to play around with the current state the implementation. Since I am a frind of using the power of dynamic data structures and since the the state has to extend I would be quite easy to adjust this in a way, that we can also create nested signals for keys which are added/removed at runtime. All we have to do is to get the current value of a signal within the proxy again. This way we can also create signals for key which are added after initialization of the store. A very easy solution for this could look like the following: export function toDeepSignal<T>(signal: Signal<T>): DeepSignal<T> {
const value = untracked(() => signal());
if (!isRecord(value)) {
return signal as DeepSignal<T>;
}
return new Proxy(signal, {
get(target: any, prop) {
// Get the value again to check against the current value not only against the value at the time the proxy was created !!!!!!!!
const valueInner: any = untracked(() => signal());
if (!(prop in valueInner)) {
return target[prop];
}
if (!target[prop]) {
target[prop] = selectSignal(() => target()[prop]);
}
return toDeepSignal(target[prop]);
},
});
} With this small modification we can ...
I am looking forward to some feedback |
Beta Was this translation helpful? Give feedback.
-
I'm going to close this RFC because |
Beta Was this translation helpful? Give feedback.
-
if we are using signalstore in our angular applicatio for state management then how to use singal instace of signalstore in multiple components? |
Beta Was this translation helpful? Give feedback.
-
NgRx Signals
Main goals:
Key principles:
GitHub Repo
StackBlitz Playground
Contents
signalState
$update
MethodselectSignal
signalStore
rxMethod
signalState
The
signalState
function creates nested signals for the provided initial state.All nested signals are created lazily as requested. Besides that, the
signalState
will cache created signals, so they'll be created only the first time when requested.$update
MethodThe
signalState
instance provides the$update
method for updating the state. It accepts a sequence of partial state objects or updater functions that partially update the state.This provides the ability to define reusable updater functions that can be used for any
signalState
instance that has a specific state slice.Unlike the default behavior of Angular signals, all signals created by the
signalState
function use equality check for reference types, not only for primitives. Therefore, the$update
method only supports immutable updates. To perform immutable updates in a mutable way, use theimmer
library:The
immerUpdater
function can be used in the following way:selectSignal
The
selectSignal
function is used to create computed (derived) signals. Unlikecomputed
function from the@angular/core
package, theselectSignal
function applies an equality check for reference types by default. It has the same signature ascomputed
.It also has another signature similar to
createSelector
andComponentStore.select
:signalStore
The
signalStore
function acts as a pipe that accepts a sequence of store features. By combining various store features, we can add state slices, computed signals, methods, and hooks to the signal store.There are 4 base features that can be used to create signal stores or custom signal store features:
withState
,withSignals
,withMethods
, andwithHooks
.The
withState
feature accepts a dictionary of state slices, and converts each slice into a nested signal. All nested signals are created lazily as requested.The
signalStore
function returns a class/token that can be further provided and injected where needed. Similar tosignalState
,signalStore
also provides the$update
method for updating the state.The
withState
feature also has a signature that accepts the initial state factory as an input argument.The
withSignals
feature accepts the computed signals factory as an input argument. Its factory accepts a dictionary of previously defined state and computed signals as an input argument.The
withMethods
feature provides the ability to add methods to the signal store. Its factory accepts a dictionary of previously defined state signals, computed signals, methods, and$update
method as an input argument and returns a dictionary of methods.The last base feature of SignalStore is
withHooks
. It provides the ability to add custom logic on SignalStore init and/or destroy.DI Config
In the previous examples, we saw the default behavior -
signalStore
returns a class/token that can be further provided where needed. However, we can also provide a SignalStore at the root level by using the{ providedIn: 'root' }
config:Defining Stores as Classes
Besides the functional approach, we can also define a store as class in the following way:
Custom Store Features
The
@ngrx/signals
package provides thesignalStoreFeature
function that can be used to create custom SignalStore features.The
withCallState
feature can be further used in any signal store as follows:The
signalStoreFeature
function also provides the ability to specify which state slices, computed signals, and/or methods are required in a store that uses the feature. This can be done using thetype
helper function.If we try to use the
withSelectedEntity
feature in the store that doesn't containentityMap
state slice, the compilation error will be thrown.Besides state, we can also add constraints for computed signals and/or methods in the following way:
More examples of custom SignalStore features:
withImmerUpdate
withStorageSync
withLoadEntities
withImmerUpdate
andwithStorageSync
features can be developed as community plugins in the future.rxMethod
The
rxMethod
function is inspired by theComponentStore.effect
method. It provides the ability to create reactive methods by using RxJS operators. It returns a function that accepts a static value, signal, or observable as an input argument.The
rxMethod
function can be used in the following way:It can be also used to define SignalStore methods:
Entity Management
This package should provide the following APIs:
withEntities
feature that will addentityMap
andids
as state slices, andentities
(entity list) as computed signalsetOne
,setAll
,deleteOne
,deleteMany
, etc.withEntities
function can be also used multiple times for the same store if we want to have multiple collections within the same store:Beta Was this translation helpful? Give feedback.
All reactions