Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin-status): ✨ enhance store status plugin and improve naming #78

Merged
merged 1 commit into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,14 @@ export const fetchBooksEffect = createEffect(
},
{
setLoadingStatus: true, // indicates that the store is loading while the effect runs
setUnmodifiedStatus: true, // it should mark the store as unmodified upon completion
setInitializedStatus: true, // it should mark the store as initialized upon completion
}
);
// And then run it
myBookStore.runEffect(fetchBooksEffect).subscribe();
const loadingSignal = isLoading(myBookStore); // true while effect is running
const isModifiedSignal = isModified(myBookStore); // true after store update
const initializedSignal = initialized(myBookStore); // true after initializing effect completion
const modifiedSignal = modified(myBookStore); // true after store update
```

## Sample Application
Expand Down
32 changes: 21 additions & 11 deletions docs/docs/plugins/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sidebar_position: 3

# Store Status

Enable your store to track the loading and modification status. By utilizing the `StoreStatusPlugin`, you can monitor whether your store is currently loading (running an effect) and if it has been modified.
Enable your store to track the loading, initialization and modification status. By utilizing the `StoreStatusPlugin`, you can monitor whether your store is currently loading (running an effect) and if it has been initialized or modified.

## Enabling Store Status

Expand Down Expand Up @@ -148,36 +148,46 @@ const effectThatLoadsBooks = createEffect(
);
```

## Tracking Modification Status
## Tracking Initialization Status

The `isModified` function returns a Signal indicating whether the specified store has been modified.
The `initialized` function returns a Signal indicating whether the specified store has been initialized by an initializing effect.

```typescript
const modifiedSignal = isModified(store);
const initializedSignal = initialized(store);
```

A store is initially **unmodified**. Any command (`set`, `update`, `mutate`) applied to the store will mark it as **modified**. Additionally, an effect created with the `setUnmodifiedStatus` flag can reset the store's modification status back to **unmodified**.
A store is initially **not initialized** and any command (`set`, `update`, `mutate`) applied to the store will not change this status.
Only an effect created with the `setInitializedStatus` flag can set the store's initialization status to **initialized**.

```typescript
const effectThatResetsStore = createEffect(
const effectThatInitializesStore = createEffect(
'Load Books',
(store: BookStore): Observable<unknown> => {
return inject(BookService);
loadBooks().pipe(tap(books => store.setBookData(books.items)));
},
// highlight-start
{
setUnmodifiedStatus: true,
setInitializedStatus: true,
}
// highlight-end
);
```

### Manually Marking as Unmodified
## Tracking Modification Status

The `modified` function returns a Signal indicating whether the specified store has been modified.

```typescript
const modifiedSignal = modified(store);
```

A store is initially **unmodified**. Any command (`set`, `update`, `mutate`) applied to the store will mark it as **modified**. Additionally, an effect created with the `setInitializedStatus` flag can reset the store's modification status back to **unmodified**.

## Reset store status

In exceptional cases, you can manually mark a store as unmodified using the `markAsUnmodified` function. But in most scenarios, consider using the `setUnmodifiedStatus`
flag on the relevant effects to automatically manage the modification status.
In some cases, it could be necessary to manually reset the store status to `unmodified` and `deinitialized` using the `resetStoreStatus` function.

```typescript
markAsUnmodified(store);
resetStoreStatus(store);
```
5 changes: 3 additions & 2 deletions packages/signalstory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,14 @@ export const fetchBooksEffect = createEffect(
},
{
setLoadingStatus: true, // indicates that the store is loading while the effect runs
setUnmodifiedStatus: true, // it should mark the store as unmodified upon completion
setInitializedStatus: true, // it should mark the store as initialized upon completion
}
);
// And then run it
myBookStore.runEffect(fetchBooksEffect).subscribe();
const loadingSignal = isLoading(myBookStore); // true while effect is running
const isModifiedSignal = isModified(myBookStore); // true after store update
const initializedSignal = initialized(myBookStore); // true after initializing effect completion
const modifiedSignal = modified(myBookStore); // true after store update
```

## Sample Application
Expand Down
158 changes: 156 additions & 2 deletions packages/signalstory/src/__tests__/store-plugin-status.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { delay, lastValueFrom, of, tap } from 'rxjs';
import { Store } from '../lib/store';
import { createEffect } from '../lib/store-effect';
import {
initialized,
isAnyEffectRunning,
isEffectRunning,
isLoading,
isModified,
markAsHavingNoRunningEffects,
markAsUnmodified,
modified,
resetStoreStatus,
runningEffects,
useStoreStatus,
} from '../lib/store-plugin-status/plugin-status';
Expand Down Expand Up @@ -374,6 +377,119 @@ describe('isModified', () => {
});
});

describe('modified', () => {
let store: Store<{ value: number }>;

beforeEach(() => {
store = new Store<{ value: number }>({
initialState: { value: 10 },
plugins: [useStoreStatus()],
});
});

it('should be unmodified initially', () => {
// assert
expect(modified(store)()).toBe(false);
});

it('should mark the store as modified after a command is processed', () => {
// act
store.set({ value: 10 });

// assert
expect(modified(store)()).toBe(true);
});

it('should mark the store as modified after an effect is processed', async () => {
// arrange
const effect = createEffect(
'dummyEffect',
(store: Store<{ value: number }>) =>
of(30).pipe(
tap(val =>
store.mutate(x => {
x.value = val;
})
)
)
);

// act
await lastValueFrom(store.runEffect(effect));

// assert
expect(modified(store)()).toBe(true);
});

it('should mark the store as unmodified after an effect with setInitializedStatus is processed', async () => {
// arrange
const effect = createEffect(
'dummyEffect',
(store: Store<{ value: number }>) =>
of(30).pipe(
tap(val =>
store.mutate(x => {
x.value = val;
})
)
),
{ setInitializedStatus: true }
);

// act
await lastValueFrom(store.runEffect(effect));

// assert
expect(modified(store)()).toBe(false);
});
});

describe('initialized', () => {
let store: Store<{ value: number }>;

beforeEach(() => {
store = new Store<{ value: number }>({
initialState: { value: 10 },
plugins: [useStoreStatus()],
});
});

it('should be deinitialized initially', () => {
// assert
expect(initialized(store)()).toBe(false);
});

it('should not mark the store as initialized after a command is processed', () => {
// act
store.set({ value: 10 });

// assert
expect(initialized(store)()).toBe(false);
});

it('should mark the store as initialized after an effect with setInitializedStatus is processed', async () => {
// arrange
const effect = createEffect(
'dummyEffect',
(store: Store<{ value: number }>) =>
of(30).pipe(
tap(val =>
store.mutate(x => {
x.value = val;
})
)
),
{ setInitializedStatus: true }
);

// act
await lastValueFrom(store.runEffect(effect));

// assert
expect(initialized(store)()).toBe(true);
});
});

describe('markAsUnmodified', () => {
let store: Store<{ value: number }>;

Expand All @@ -387,13 +503,51 @@ describe('markAsUnmodified', () => {
it('should manually mark the store as unmodified', () => {
// arrange
store.set({ value: 20 });
expect(isModified(store)()).toBe(true); // sanity check
expect(modified(store)()).toBe(true); // sanity check

// act
markAsUnmodified(store);

// assert
expect(isModified(store)()).toBe(false);
expect(modified(store)()).toBe(false);
});
});

describe('resetStoreStatus', () => {
let store: Store<{ value: number }>;

beforeEach(() => {
store = new Store<{ value: number }>({
initialState: { value: 10 },
plugins: [useStoreStatus()],
});
});

it('should reset the store modification status', () => {
// arrange
store.set({ value: 20 });
expect(modified(store)()).toBe(true); // sanity check

// act
resetStoreStatus(store);

// assert
expect(modified(store)()).toBe(false);
});

it('should reset the store initialization status', () => {
// arrange
const dummyEffect = createEffect('Dummy', () => {}, {
setInitializedStatus: true,
});
store.runEffect(dummyEffect);
expect(initialized(store)()).toBe(true); // sanity check

// act
resetStoreStatus(store);

// assert
expect(modified(store)()).toBe(false);
});
});

Expand Down
14 changes: 12 additions & 2 deletions packages/signalstory/src/lib/store-effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ export interface StoreEffectConfig {
setLoadingStatus?: boolean;

/**
* Indicates whether the effect sets unmodified status.
* @deprecated Use `setInitializedStatus` instead. This method is deprecated and will be removed in the next major release (v18).
*/
setUnmodifiedStatus?: boolean;

/**
* Indicates whether the effect sets initialized status.
* Only applicable if the `StoreStatus` plugin is used.
* Defaults to false.
*/
setUnmodifiedStatus?: boolean;
setInitializedStatus?: boolean;
}

/**
Expand Down Expand Up @@ -92,8 +97,13 @@ export function createEffect<
withInjectionContext:
!arg || arg === true || (arg.withInjectionContext ?? true),
setLoadingStatus: (arg as StoreEffectConfig)?.setLoadingStatus ?? false,
//TODO: remove the following line for next major update
setUnmodifiedStatus:
(arg as StoreEffectConfig)?.setUnmodifiedStatus ?? false,
setInitializedStatus:
(arg as StoreEffectConfig)?.setInitializedStatus ??
(arg as StoreEffectConfig)?.setUnmodifiedStatus ??
false,
},
};
}
Loading
Loading