Skip to content

Commit

Permalink
feat(lib): add deep signals and refactor mutations and lazy
Browse files Browse the repository at this point in the history
  • Loading branch information
SaulMoro committed Feb 28, 2024
1 parent 4279e8b commit c7f3ca0
Show file tree
Hide file tree
Showing 13 changed files with 107 additions and 47 deletions.
15 changes: 11 additions & 4 deletions projects/ngrx-rtk-query/src/lib/build-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import type {
UseQuerySubscription,
} from './types';
import { useStableQueryArgs } from './useSerializedStableValue';
import { shallowEqual } from './utils';
import { shallowEqual, toDeepSignal } from './utils';

/**
* Wrapper around `defaultQueryStateSelector` to be used in `useQuery`.
Expand Down Expand Up @@ -385,8 +385,11 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
skip: arg() === UNINITIALIZED_VALUE,
}));
const queryStateResults = useQueryState(arg, subscriptionOptions);
const queryState = computed(() => ({ ...queryStateResults(), lastArg: arg() }));
const deepSignal = toDeepSignal(queryState);
Object.assign(deepSignal, { fetch: trigger });

return { fetch: trigger, state: queryStateResults, lastArg: arg } as const;
return deepSignal as any;
},
useQuery(arg, options) {
const querySubscriptionResults = useQuerySubscription(arg, options);
Expand All @@ -397,8 +400,10 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
return { selectFromResult, ...options };
});
const queryStateResults = useQueryState(arg, subscriptionOptions);
const queryState = computed(() => ({ ...queryStateResults(), ...querySubscriptionResults }));
const deepSignal = toDeepSignal(queryState);

return computed(() => ({ ...queryStateResults(), ...querySubscriptionResults }));
return deepSignal as any;
},
selector: select as QuerySelector<any>,
};
Expand Down Expand Up @@ -458,8 +463,10 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
};

const finalState = computed(() => ({ ...currentState()(), originalArgs: originalArgs(), reset }));
const deepSignal = toDeepSignal(finalState);
Object.assign(deepSignal, { dispatch: triggerMutation });

return { dispatch: triggerMutation, state: finalState } as const;
return deepSignal as any;
};

return {
Expand Down
12 changes: 4 additions & 8 deletions projects/ngrx-rtk-query/src/lib/types/hooks-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
TSHelpersOverride,
} from '@reduxjs/toolkit/query';
import type { UninitializedValue } from '../constants';
import { DeepSignal } from '../utils';

export interface QueryHooks<Definition extends QueryDefinition<any, any, any, any, any>> {
useQuery: UseQuery<Definition>;
Expand Down Expand Up @@ -69,7 +70,7 @@ export type UseQuery<D extends QueryDefinition<any, any, any, any>> = <
>(
arg: Signal<QueryArgFrom<D> | SkipToken> | QueryArgFrom<D> | SkipToken,
options?: UseQueryOptions<D, R> | Signal<UseQueryOptions<D, R>>,
) => Signal<UseQueryHookResult<D, R>>;
) => DeepSignal<UseQueryHookResult<D, R>>;

export type TypedUseQuery<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseQuery<
QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>
Expand Down Expand Up @@ -237,10 +238,8 @@ export type UseLazyQuery<D extends QueryDefinition<any, any, any, any>> = <
R extends Record<string, any> = UseQueryStateDefaultResult<D>,
>(
options?: UseLazyQueryOptions<D, R> | Signal<UseLazyQueryOptions<D, R>>,
) => {
) => DeepSignal<UseQueryStateResult<D, R> & { lastArg: QueryArgFrom<D> }> & {
fetch: LazyQueryTrigger<D>;
state: Signal<UseQueryStateResult<D, R>>;
lastArg: Signal<QueryArgFrom<D>>;
};

export type TypedUseLazyQuery<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseLazyQuery<
Expand Down Expand Up @@ -465,10 +464,7 @@ export type UseMutation<D extends MutationDefinition<any, any, any, any>> = <
R extends Record<string, any> = MutationResultSelectorResult<D>,
>(
options?: UseMutationStateOptions<D, R>,
) => {
dispatch: MutationTrigger<D>;
state: Signal<UseMutationStateResult<D, R>>;
};
) => DeepSignal<UseMutationStateResult<D, R>> & { dispatch: MutationTrigger<D> };

export type TypedUseMutation<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseMutation<
MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>
Expand Down
52 changes: 52 additions & 0 deletions projects/ngrx-rtk-query/src/lib/utils/deep-signal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* The code in this file is adapted from ngrx/signals
*
* ngrx is an open-source project licensed under the MIT license.
*
* For more information about the original code, see
* https://github.com/ngrx/platform
*/
import { computed, isSignal, Signal as NgSignal, untracked } from '@angular/core';
import { IsKnownRecord } from './tsHelpers';

// An extended Signal type that enables the correct typing
// of nested signals with the `name` or `length` key.
export interface Signal<T> extends NgSignal<T> {
name: unknown;
length: unknown;
}

export type DeepSignal<T> = Signal<T> &
(IsKnownRecord<T> extends true
? Readonly<{
[K in keyof T]: IsKnownRecord<T[K]> extends true ? DeepSignal<T[K]> : Signal<T[K]>;
}>
: unknown);

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) {
if (!(prop in value)) {
return target[prop];
}

if (!isSignal(target[prop])) {
Object.defineProperty(target, prop, {
value: computed(() => target()[prop]),
configurable: true,
});
}

return toDeepSignal(target[prop]);
},
});
}

function isRecord(value: unknown): value is Record<string, unknown> {
return value?.constructor === Object;
}
1 change: 1 addition & 0 deletions projects/ngrx-rtk-query/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './capitalize';
export * from './deep-signal';
export * from './shallow-equal';
export * from './tsHelpers';
16 changes: 16 additions & 0 deletions projects/ngrx-rtk-query/src/lib/utils/tsHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,19 @@ import type { NoInfer } from '@reduxjs/toolkit/dist/tsHelpers';
export function safeAssign<T extends object>(target: T, ...args: Array<Partial<NoInfer<T>>>): T {
return Object.assign(target, ...args);
}

export type IsRecord<T> = T extends object
? T extends unknown[]
? false
: T extends Set<unknown>
? false
: T extends Map<unknown, unknown>
? false
: T extends Function
? false
: true
: false;

export type IsUnknownRecord<T> = string extends keyof T ? true : number extends keyof T ? true : false;

export type IsKnownRecord<T> = IsRecord<T> extends true ? (IsUnknownRecord<T> extends true ? false : true) : false;
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@ import { nanoid } from '@reduxjs/toolkit';
<h1 class="text-xl font-semibold">Main Counter</h1>
<div>
<div class="flex items-center space-x-4">
<button
class="btn-outline btn-primary"
[disabled]="increment.state().isLoading"
(click)="increment.dispatch(1)"
>
<button class="btn-outline btn-primary" [disabled]="increment.isLoading()" (click)="increment.dispatch(1)">
+
</button>
<span class="text-3xl font-bold">{{ countQuery().data?.count || 0 }}</span>
Expand Down
8 changes: 4 additions & 4 deletions src/app/features/counter/counter/counter.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import { pollingOptions } from '../utils/polling-options';
<section class="space-y-4">
<h1 class="text-md font-medium">{{ id }}</h1>
<div class="flex items-center space-x-4">
<button class="btn-outline btn-primary" [disabled]="increment.state().isLoading" (click)="incrementCounter()">
<button class="btn-outline btn-primary" [disabled]="increment.isLoading()" (click)="incrementCounter()">
+
</button>
<span class="text-3xl font-bold" [class.bg-green-100]="countQuery.state().isFetching">{{
countQuery.state().data?.count || 0
<span class="text-3xl font-bold" [class.bg-green-100]="countQuery.isFetching()">{{
countQuery().data?.count || 0
}}</span>
<button class="btn-outline btn-primary" [disabled]="decrement.state().isLoading" (click)="decrementCounter()">
<button class="btn-outline btn-primary" [disabled]="decrement.isLoading()" (click)="decrementCounter()">
-
</button>
Expand Down
4 changes: 2 additions & 2 deletions src/app/features/lazy/counter-row/counter-row.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useDecrementCountByIdMutation, useIncrementCountByIdMutation } from '@a
<div class="mt-4 flex items-center space-x-4">
<button
class="btn-outline btn-primary"
[disabled]="increment.state().isLoading || counterData?.isUninitialized"
[disabled]="increment.isLoading() || counterData?.isUninitialized"
(click)="incrementCounter()"
>
+
Expand All @@ -18,7 +18,7 @@ import { useDecrementCountByIdMutation, useIncrementCountByIdMutation } from '@a
}}</span>
<button
class="btn-outline btn-primary"
[disabled]="decrement.state().isLoading || counterData?.isUninitialized"
[disabled]="decrement.isLoading() || counterData?.isUninitialized"
(click)="decrementCounter()"
>
-
Expand Down
14 changes: 5 additions & 9 deletions src/app/features/lazy/lazy-counter/lazy.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@ const { getCountById } = counterApiEndpoints;
<h1 class="text-xl font-semibold">Start Lazy Counter</h1>
<div>
<input type="text" placeholder="Type counter id" formControlName="id" />
<button
class="btn btn-primary m-4"
type="submit"
[disabled]="form.invalid || countLazyQuery.state().isLoading"
>
{{ countLazyQuery.state().isLoading ? 'Starting...' : 'Start Counter' }}
<button class="btn btn-primary m-4" type="submit" [disabled]="form.invalid || countLazyQuery.isLoading()">
{{ countLazyQuery.isLoading() ? 'Starting...' : 'Start Counter' }}
</button>
<label class="space-x-2 text-sm" for="preferCacheValue">
<input id="preferCacheValue" type="checkbox" formControlName="preferCacheValue" />
Expand All @@ -28,16 +24,16 @@ const { getCountById } = counterApiEndpoints;
</form>
<section class="space-y-4">
<h1 class="text-md font-medium">Current id: {{ countLazyQuery.state().originalArgs || 'Not Started' }}</h1>
<app-counter-row [counterData]="countLazyQuery.state()"></app-counter-row>
<h1 class="text-md font-medium">Current id: {{ countLazyQuery().originalArgs || 'Not Started' }}</h1>
<app-counter-row [counterData]="countLazyQuery()"></app-counter-row>
</section>
</div>
<div class="mt-8 space-y-8">
<section class="space-y-4">
<h1 class="text-md font-medium">Duplicate state (Share state, subscription & selectFromResult)</h1>
<h1 class="text-sm">Use in same component (not subscripted by self)</h1>
<app-counter-row [counterData]="countLazyQuery.state()"></app-counter-row>
<app-counter-row [counterData]="countLazyQuery()"></app-counter-row>
</section>
<section class="space-y-4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ import { useLazyGetEpisodeQuery } from '../services';
<div class="mt-3 self-start text-xs text-gray-500 dark:text-gray-400">
First seen:
<div
*ngIf="episodeQuery.state().isLoading; else episodeName"
*ngIf="episodeQuery.isLoading(); else episodeName"
class="ml-1 inline-block h-4 w-32 animate-pulse rounded bg-indigo-200"
></div>
<ng-template #episodeName>
<span class="inline-block text-indigo-700 hover:text-indigo-800">
{{ episodeQuery.state().data?.name }}
{{ episodeQuery().data?.name }}
</span>
</ng-template>
</div>
Expand Down
16 changes: 7 additions & 9 deletions src/app/features/posts/post-detail/post-detail.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,17 @@ import { useDeletePostMutation, useGetPostQuery, useUpdatePostMutation } from '.
<div class="flex items-center space-x-4">
<button
class="btn-outline btn-primary"
[disabled]="
postQuery().isLoading || deletePostMutation.state().isLoading || updatePostMutation.state().isLoading
"
[disabled]="postQuery().isLoading || deletePostMutation.isLoading() || updatePostMutation.isLoading()"
(click)="toggleEdit()"
>
{{ updatePostMutation.state().isLoading ? 'Updating...' : 'Edit' }}
{{ updatePostMutation.isLoading() ? 'Updating...' : 'Edit' }}
</button>
<button
class="btn-outline btn-primary"
[disabled]="postQuery().isLoading || deletePostMutation.state().isLoading"
[disabled]="postQuery().isLoading || deletePostMutation.isLoading()"
(click)="deletePost()"
>
{{ deletePostMutation.state().isLoading ? 'Deleting...' : 'Delete' }}
{{ deletePostMutation.isLoading() ? 'Deleting...' : 'Delete' }}
</button>
<button class="btn-outline btn-primary" [disabled]="postQuery().isFetching" (click)="postQuery().refetch()">
{{ postQuery().isFetching ? 'Fetching...' : 'Refresh' }}
Expand All @@ -39,10 +37,10 @@ import { useDeletePostMutation, useGetPostQuery, useUpdatePostMutation } from '.
<ng-template #editionSection>
<div class="space-x-4">
<input type="text" [formControl]="postFormControl" />
<button class="btn btn-primary" [disabled]="updatePostMutation.state().isLoading" (click)="updatePost()">
{{ updatePostMutation.state().isLoading ? 'Updating...' : 'Update' }}
<button class="btn btn-primary" [disabled]="updatePostMutation.isLoading()" (click)="updatePost()">
{{ updatePostMutation.isLoading() ? 'Updating...' : 'Update' }}
</button>
<button class="btn btn-primary" [disabled]="updatePostMutation.state().isLoading" (click)="toggleEdit()">
<button class="btn btn-primary" [disabled]="updatePostMutation.isLoading()" (click)="toggleEdit()">
Cancel
</button>
</div>
Expand Down
2 changes: 0 additions & 2 deletions src/app/features/posts/posts-list/posts-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import { useGetPostsQuery } from '../services';
export class PostsListComponent {
postsQuery = useGetPostsQuery();

constructor() {}

trackByFn(_index: number, post: Post): number {
return post.id;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { useAddPostMutation } from '../services';
<input id="name" placeholder="New post name" type="text" [formControl]="postNameFormControl" />
<button
class="btn btn-primary m-4"
[disabled]="postNameFormControl.invalid || addPost.state().isLoading"
[disabled]="postNameFormControl.invalid || addPost.isLoading()"
(click)="addNewPost()"
>
{{ addPost.state().isLoading ? 'Adding...' : 'Add Post' }}
{{ addPost.isLoading() ? 'Adding...' : 'Add Post' }}
</button>
</div>
Expand Down

0 comments on commit c7f3ca0

Please sign in to comment.