| |

Angular 85 🅰️ Signal Store

⏱️ Estimated read: 17 min | |

The previous chapters covered the global NgRx store with actions, reducers, and effects, and then ComponentStore as the component-scoped alternative. Signal Store (@ngrx/signals) is the newest entry in the NgRx ecosystem, and it is the one NgRx now recommends for new projects. It combines the structure and composability of NgRx with the reactivity of Angular signals, eliminating actions, reducers, and selectors in favor of a functional, feature-based API.

Signal Store is not a replacement for the classic store. It is a different tool for a different scope. Where the classic store is designed for application-wide state with time-travel debugging and a single source of truth, Signal Store is designed for feature and component state with minimal boilerplate and native signal integration . The two can coexist: the classic store for the global state that benefits from Redux DevTools, and Signal Store for the local state that benefits from signals.

Key point: A Signal Store is a tree of composed features. Each feature contributes some combination of state, computed signals, methods, and lifecycle hooks. Composition happens at definition time with signalStore(...). The store is an Angular provider: inject it with inject(Store) or TestBed.inject(Store). State updates go through patchState(store, updater). Direct mutation does not exist .


Why Signal Store exists

The classic store and ComponentStore both solve real problems. Signal Store solves the problem that neither was designed for: state management that feels native to Angular’s signal-based reactivity.

The boilerplate problem. The classic store requires actions, reducers, selectors, and effects for every feature. A simple counter needs four files. Signal Store reduces this to a single signalStore() call with withState, withComputed, and withMethods. There are no action names to invent, no reducer switch statements, and no selector memoization to configure .

The signals-native problem. The classic store and ComponentStore are built on RxJS. select returns observables. effect returns observables. Angular’s modern change detection is signal-based, and mixing observables with signals requires toSignal or the async pipe. Signal Store’s state is signals by default. Reading state is store.count(), not store.select(selectCount) | async. Computed values are computed(), not memoized selector functions .

The composition problem. ComponentStore uses class inheritance: you extend ComponentStore<T>. Signal Store uses function composition: you compose features with signalStore(withState(...), withComputed(...), withMethods(...)). Features can be reused across stores via signalStoreFeature(), and custom features can declare prerequisites so that misuse is a compile error .

The scoping problem. Signal Store can be provided at root, on a route, or on a component. The scope determines the lifetime. { providedIn: 'root' } for application-wide state. Route providers for feature state that should be discarded when the route deactivates. Component providers for state that lives and dies with a single component tree .

The trade-off. Signal Store is new. Its ecosystem is smaller than the classic store’s. DevTools support is less mature. The Redux pattern of “actions describe events” is absent — Signal Store methods mutate state directly, which is more concise but loses the event log. For applications that need time-travel debugging or an event-sourced architecture, the classic store remains the right choice .


a. Defining a Signal Store

A Signal Store is created with the signalStore function. It takes a sequence of features. The most common features are withState, withComputed, and withMethods. The store is an Angular provider: it can be provided at root, on a route, or on a component.

import { computed, inject } from '@angular/core';
import {
  signalStore,
  withState,
  withComputed,
  withMethods,
  patchState,
} from '@ngrx/signals';
import { BookApi } from './book.api';
import { Book } from './book.model';

type BookSearchState = {
  books: Book[];
  isLoading: boolean;
  filter: { query: string; order: 'asc' | 'desc' };
};

const initialState: BookSearchState = {
  books: [],
  isLoading: false,
  filter: { query: '', order: 'asc' },
};

export const BookSearchStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withComputed(({ books, filter }) => ({
    booksCount: computed(() => books().length),
    sortedBooks: computed(() => {
      const direction = filter().order === 'asc' ? 1 : -1;
      return [...books()].sort(
        (a, b) => direction * a.title.localeCompare(b.title)
      );
    }),
  })),
  withMethods((store, api = inject(BookApi)) => ({
    updateQuery(query: string): void {
      patchState(store, (state) => ({
        filter: { ...state.filter, query },
      }));
    },
    updateOrder(order: 'asc' | 'desc'): void {
      patchState(store, (state) => ({
        filter: { ...state.filter, order },
      }));
    },
    async loadBooks(): Promise<void> {
      patchState(store, { isLoading: true });
      try {
        const books = await api.getBooks();
        patchState(store, { books, isLoading: false });
      } catch {
        patchState(store, { isLoading: false });
      }
    },
  }))
);

The withState feature declares the state shape and initial values. Each property becomes a signal on the store instance: store.books(), store.isLoading(), store.filter(). Nested objects become deep signals: store.filter.query() and store.filter.order() are also readable .

The withComputed feature declares derived values. The factory receives the store’s previously defined state signals and returns a dictionary of computed signals. booksCount and sortedBooks are computed from books and filter. They update automatically when their dependencies change .

The withMethods feature declares methods. The factory receives the store instance and returns a dictionary of methods. The methods call patchState to update the state. loadBooks is an async method that sets isLoading, calls the API, and patches the result. The API is injected within the factory function, so it is available to every method that needs it .

The component consumes the store by injecting it:

import { Component, inject } from '@angular/core';
import { BookSearchStore } from './book-search.store';

@Component({
  selector: 'book-search',
  template: `
    <p>Count: {{ store.booksCount() }}</p>
    <input
      [value]="store.filter.query()"
      (input)="store.updateQuery($event.target.value)"
    />
    @for (book of store.sortedBooks(); track book.id) {
      <div>{{ book.title }}</div>
    }
  `,
})
export class BookSearchComponent {
  readonly store = inject(BookSearchStore);
}

The component reads signals directly: store.booksCount(), store.filter.query(), store.sortedBooks(). There are no observables, no async pipe, no subscriptions. The signals are read in the template, and Angular’s change detection handles the rest .


b. Entity Management with withEntities

The @ngrx/signals/entities package provides withEntities, the Signal Store equivalent of the classic store’s Entity adapter. It normalizes a collection into an entity map and provides update functions for CRUD operations. The state shape is similar: { ids: [], entityMap: {} }, with entities as a computed array of the entities in order .

import { computed } from '@angular/core';
import { signalStore, withMethods, patchState } from '@ngrx/signals';
import {
  withEntities,
  setAllEntities,
  addEntity,
  updateEntity,
  removeEntity,
  entityConfig,
} from '@ngrx/signals/entities';
import { Account } from './account.model';

const accountConfig = entityConfig({
  entity: type<Account>(),
  collection: 'accounts',
  selectId: (account) => account.code,
});

export const AccountStore = signalStore(
  { providedIn: 'root' },
  withEntities(accountConfig),
  withComputed(({ accountsEntities }) => ({
    activeAccounts: computed(() =>
      accountsEntities().filter((a) => a.isActive)
    ),
    totalBalance: computed(() =>
      accountsEntities().reduce((sum, a) => sum + a.balance, 0)
    ),
  })),
  withMethods((store) => ({
    setAccounts(accounts: Account[]): void {
      patchState(store, setAllEntities(accounts, accountConfig));
    },
    addAccount(account: Account): void {
      patchState(store, addEntity(account, accountConfig));
    },
    updateBalance(code: string, balance: number): void {
      patchState(
        store,
        updateEntity({ id: code, changes: { balance } }, accountConfig)
      );
    },
    removeAccount(code: string): void {
      patchState(store, removeEntity(code, accountConfig));
    },
  }))
);

The entityConfig declares the entity type, an optional collection name (for multiple entity collections in one store), and a custom selectId function. The withEntities(accountConfig) feature adds the entity state. The store exposes accountsIds(), accountsEntityMap(), and accountsEntities() as signals .

The update functions — setAllEntities, addEntity, updateEntity, removeEntity — are passed to patchState. They produce the partial state that patchState merges. The accountConfig is passed to both withEntities and every update function. This repetition is load-bearing: an updater called without the config falls back to reading entity.id, which may be undefined for entities with custom IDs .

The entity selectors are computed from the normalized state. accountsEntities() returns the entities in the order of the ids array. Derived collections — activeAccounts, totalBalance — are computed signals that read from accountsEntities() and recalculate when the entity state changes.


c. Async Side Effects with rxMethod

Signal Store’s state is signals. Its side effects are handled by rxMethod, a function from @ngrx/signals/rxjs-interop that bridges signals and RxJS. rxMethod is used inside withMethods to define methods that perform async operations with RxJS operators .

import { inject } from '@angular/core';
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { tapResponse } from '@ngrx/operators';
import { pipe, switchMap, tap } from 'rxjs';
import { TaskApi } from './task.api';

export const TasksStore = signalStore(
  { providedIn: 'root' },
  withState({ tasks: [], isLoading: false, error: null as string | null }),
  withMethods((store, api = inject(TaskApi)) => ({
    loadTasks: rxMethod<void>(
      pipe(
        tap(() => patchState(store, { isLoading: true, error: null })),
        switchMap(() =>
          api.getTasks().pipe(
            tapResponse({
              next: (tasks) => patchState(store, { tasks, isLoading: false }),
              error: (error: Error) =>
                patchState(store, { isLoading: false, error: error.message }),
            })
          )
        )
      )
    ),
  }))
);

The rxMethod<void> declares a method that takes no input. The pipe configures the stream: tap sets the loading state, switchMap calls the API, and tapResponse handles the success and error cases. The tapResponse operator from @ngrx/operators keeps the stream alive after an error, which is essential — without it, an error would terminate the method permanently .

rxMethod can also accept a signal or an observable as input. When the input changes, the method re-executes. This is how reactive search is implemented:

search: rxMethod<string>(
  pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((query) =>
      api.search(query).pipe(
        tapResponse({
          next: (results) => patchState(store, { results }),
          error: () => patchState(store, { results: [] }),
        })
      )
    )
  )
)

The search method is called with a signal: store.search(querySignal). Every time querySignal changes, search runs again. The debounceTime and switchMap operators handle the common race conditions: debounceTime waits for the user to stop typing, and switchMap cancels the previous request when a new one arrives .

For simple side effects that do not need RxJS operators, signalMethod (introduced in NgRx v19) provides a lighter alternative. It accepts a signal or a value, runs a synchronous function, and does not require RxJS. The choice between them is whether the operation needs operators like debounceTime, switchMap, or retry .


Complete Example Session

This session builds a complete task management Signal Store with entity management, async effects, and a component that consumes it.

// ============================================
// PART 1: THE MODEL
// ============================================

// task.model.ts
export interface Task {
  id: string;
  title: string;
  completed: boolean;
}

// ============================================
// PART 2: THE API SERVICE
// ============================================

// task.api.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Task } from './task.model';

@Injectable({ providedIn: 'root' })
export class TaskApi {
  private http = inject(HttpClient);

  getTasks(): Observable<Task[]> {
    return this.http.get<Task[]>('/api/tasks');
  }

  createTask(task: Omit<Task, 'id'>): Observable<Task> {
    return this.http.post<Task>('/api/tasks', task);
  }
}

// ============================================
// PART 3: THE SIGNAL STORE
// ============================================

// tasks.store.ts
import { computed, inject } from '@angular/core';
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { withEntities, setAllEntities, addEntity, updateEntity, removeEntity } from '@ngrx/signals/entities';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { tapResponse } from '@ngrx/operators';
import { pipe, switchMap, tap } from 'rxjs';
import { TaskApi } from './task.api';
import { Task } from './task.model';

type TasksState = {
  isLoading: boolean;
  error: string | null;
  filter: 'all' | 'active' | 'completed';
};

export const TasksStore = signalStore(
  { providedIn: 'root' },
  withState<TasksState>({
    isLoading: false,
    error: null,
    filter: 'all',
  }),
  withEntities<Task>(),
  withComputed(({ entities, filter }) => ({
    filteredTasks: computed(() => {
      const all = entities();
      switch (filter()) {
        case 'active': return all.filter((t) => !t.completed);
        case 'completed': return all.filter((t) => t.completed);
        default: return all;
      }
    }),
    total: computed(() => entities().length),
    completedCount: computed(() => entities().filter((t) => t.completed).length),
  })),
  withMethods((store, api = inject(TaskApi)) => ({
    setFilter(filter: TasksState['filter']): void {
      patchState(store, { filter });
    },
    addTask(task: Task): void {
      patchState(store, addEntity(task));
    },
    toggleTask(id: string): void {
      const task = store.entityMap()[id];
      if (!task) return;
      patchState(store, updateEntity({ id, changes: { completed: !task.completed } }));
    },
    removeTask(id: string): void {
      patchState(store, removeEntity(id));
    },
    clearCompleted(): void {
      const completedIds = store.entities()
        .filter((t) => t.completed)
        .map((t) => t.id);
      for (const id of completedIds) {
        patchState(store, removeEntity(id));
      }
    },
    loadTasks: rxMethod<void>(
      pipe(
        tap(() => patchState(store, { isLoading: true, error: null })),
        switchMap(() =>
          api.getTasks().pipe(
            tapResponse({
              next: (tasks) => patchState(store, setAllEntities(tasks), { isLoading: false }),
              error: (error: Error) =>
                patchState(store, { isLoading: false, error: error.message }),
            })
          )
        )
      )
    ),
  }))
);
// ============================================
// PART 4: THE COMPONENT
// ============================================

// tasks.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { TasksStore } from './tasks.store';

@Component({
  selector: 'tasks-page',
  template: `
    @if (store.isLoading()) {
      <p class="loading">Loading...</p>
    }
    @if (store.error(); as error) {
      <p class="error">{{ error }}</p>
    }

    <p>Total: {{ store.total() }} — Completed: {{ store.completedCount() }}</p>

    <div class="filters">
      <button (click)="store.setFilter('all')">All</button>
      <button (click)="store.setFilter('active')">Active</button>
      <button (click)="store.setFilter('completed')">Completed</button>
    </div>

    <ul>
      @for (task of store.filteredTasks(); track task.id) {
        <li>
          <input
            type="checkbox"
            [checked]="task.completed"
            (change)="store.toggleTask(task.id)"
          />
          <span [class.completed]="task.completed">{{ task.title }}</span>
          <button (click)="store.removeTask(task.id)">×</button>
        </li>
      }
    </ul>

    <button (click)="store.clearCompleted()">Clear completed</button>
    <button (click)="store.loadTasks()">Reload</button>
  `,
})
export class TasksPageComponent implements OnInit {
  readonly store = inject(TasksStore);

  ngOnInit(): void {
    this.store.loadTasks();
  }
}
// ============================================
// PART 5: THE TEST
// ============================================

// tasks.store.spec.ts
import { TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { TasksStore } from './tasks.store';
import { TaskApi } from './task.api';

describe('TasksStore', () => {
  let store: TasksStore;
  let api: jasmine.SpyObj<TaskApi>;

  beforeEach(() => {
    api = jasmine.createSpyObj('TaskApi', ['getTasks', 'createTask']);

    TestBed.configureTestingModule({
      providers: [
        TasksStore,
        { provide: TaskApi, useValue: api },
      ],
    });

    store = TestBed.inject(TasksStore);
  });

  it('loads tasks into the entity state', async () => {
    const mockTasks = [
      { id: '1', title: 'Write tests', completed: false },
    ];
    api.getTasks.and.returnValue(of(mockTasks));

    store.loadTasks();
    // rxMethod is async; use a tick or await
    await new Promise((resolve) => setTimeout(resolve, 0));

    expect(store.entities().length).toBe(1);
    expect(store.isLoading()).toBe(false);
  });

  it('filters active tasks', () => {
    store.addTask({ id: '1', title: 'A', completed: false });
    store.addTask({ id: '2', title: 'B', completed: true });
    store.setFilter('active');

    expect(store.filteredTasks().length).toBe(1);
    expect(store.filteredTasks()[0].completed).toBe(false);
  });

  it('toggles a task', () => {
    store.addTask({ id: '1', title: 'A', completed: false });
    store.toggleTask('1');

    const task = store.entityMap()['1'];
    expect(task?.completed).toBe(true);
  });

  it('handles load errors', async () => {
    api.getTasks.and.returnValue(throwError(() => new Error('Network error')));

    store.loadTasks();
    await new Promise((resolve) => setTimeout(resolve, 0));

    expect(store.isLoading()).toBe(false);
    expect(store.error()).toBe('Network error');
  });
});

The five parts cover the model, the API service, the Signal Store with entity management and rxMethod, the consuming component, and the test.


Quick Reference

The Core Features

FeaturePurpose
signalStore(...)Define a store
withState(initial)Declare state signals
withComputed(fn)Declare derived signals
withMethods(fn)Declare methods and effects
withProps(fn)Add static properties
withHooks(fn)Lifecycle hooks (onInit, onDestroy)
withEntities(config)Add entity collection
signalStoreFeature(...)Create a reusable custom feature

The Entity Update Functions

FunctionPurpose
setAllEntities(items, config)Replace all
addEntity(item, config)Add one
updateEntity({ id, changes }, config)Update one
removeEntity(id, config)Remove one
removeEntities(ids, config)Remove many
updateAllEntities(fn, config)Update all

The Async Side Effect Tools

ToolRxJS RequiredUse Case
async methodNoSingle awaited call
rxMethod(input, pipe)YesDebounce, switchMap, cancellation
signalMethod(input, fn)NoSimple synchronous side effects

The Scope Options

ScopeDeclarationLifetime
Root{ providedIn: 'root' }Application
RouteRoute providersRoute activation
ComponentComponent providersComponent

Best Practices

✅ Do This:

// Type the state explicitly
withState<TasksState>(initialState)                            // ✅
// Use patchState for all updates
patchState(store, { isLoading: true })                         // ✅
// Use tapResponse in rxMethod
tapResponse({ next: ..., error: ... })                         // ✅
// Pass entityConfig to every updater
setAllEntities(tasks, accountConfig)                            // ✅
// Scope the store to its lifetime
{ providedIn: 'root' }  // or route/component providers        // ✅

❌ Don’t Do This:

// Don't mutate state directly
store.tasks().push(task)                                       // ❌
// Don't forget patchState in rxMethod
switchMap(() => api.get())  // no patchState                     // ❌
// Don't omit catchError or tapResponse
switchMap(() => api.get())  // stream dies on error             // ❌
// Don't put HTTP calls in the store without a service
withMethods((store) => ({ load() { inject(HttpClient).get(...) } })) // ❌ use a service

Common Pitfalls

PitfallWhy It HappensFix
State doesn’t updateMutated instead of patchStateUse patchState
Effect stream diesNo tapResponse or catchErrorUse tapResponse
Entity update wrongentityConfig not passedPass config to every updater
Infinite looprxMethod writes to a signal it readsGuard with a condition
Test hangsrxMethod async not awaitedAwait the tick or use fakeAsync
Store not sharedProvided in two scopesProvide in only one

Real-World Examples

1. Basic Signal Store

export const CounterStore = signalStore(
  { providedIn: 'root' },
  withState({ count: 0 }),
  withMethods((store) => ({
    increment: () => patchState(store, (s) => ({ count: s.count + 1 })),
  }))
);

2. Computed from State

withComputed(({ count }) => ({ double: computed(() => count() * 2) }))

3. Async Method

load: rxMethod<void>(pipe(switchMap(() => api.get().pipe(tapResponse({ ... })))))

4. Entity Collection

withEntities<User>({ selectId: (u) => u.id })

5. Entity Update

patchState(store, addEntity(user))

6. Custom Entity Config

const config = entityConfig({ entity: type<User>(), selectId: (u) => u.code });

7. Store Scope

{ providedIn: 'root' }  // or providers: [Store]

8. Lifecycle Hook

withHooks({ onInit() { ... }, onDestroy() { ... } })

9. Reusable Feature

function withLogger() { return signalStoreFeature(withMethods(...)); }

10. Test Injection

store = TestBed.inject(TasksStore);

Visual

The Signal Store Composition

┌──────────────────────────────────────────────┐
│  SIGNAL STORE COMPOSITION                    │
│                                              │
│  signalStore(                                │
│    { providedIn: 'root' },                   │
│    withState(initialState),                  │
│    withComputed(fn),                         │
│    withMethods(fn),                          │
│    withEntities(config),                     │
│    withHooks(fn),                            │
│  )                                           │
│                                              │
│  Each feature contributes:                   │
│    ├─ state signals                          │
│    ├─ computed signals                       │
│    ├─ methods                                │
│    └─ lifecycle hooks                        │
│                                              │
│  The result is a single injectable provider. │
│                                              │
└──────────────────────────────────────────────┘

The Entity State Shape

┌──────────────────────────────────────────────┐
│  SIGNAL STORE ENTITY STATE                   │
│                                              │
│  withEntities<Task>()                        │
│    ├─ ids: Signal<string[]>                  │
│    ├─ entityMap: Signal<Record<string, Task>>│
│    └─ entities: Signal<Task[]>               │
│                                              │
│  Derived:                                    │
│    computed(() => entities().length)         │
│    computed(() => entities().filter(...))    │
│                                              │
│  Updates:                                    │
│    patchState(store, addEntity(task))        │
│    patchState(store, updateEntity({ id, changes }))│
│                                              │
└──────────────────────────────────────────────┘

The rxMethod Flow

┌──────────────────────────────────────────────┐
│  rxMethod FLOW                               │
│                                              │
│  store.loadTasks()                           │
│       │                                      │
│       ▼                                      │
│  pipe(                                       │
│    tap(() => patchState({ isLoading: true })),│
│    switchMap(() => api.get()),               │
│    tapResponse({                             │
│      next: (data) => patchState({ data }),   │
│      error: (err) => patchState({ error }),  │
│    })                                        │
│  )                                           │
│       │                                      │
│       ▼                                      │
│  rxMethod auto-unsubscribes on destroy.      │
│                                              │
└──────────────────────────────────────────────┘

The Scope Decision

┌──────────────────────────────────────────────┐
│  SCOPE DECISION                              │
│                                              │
│  App-wide?                                   │
│    └─ { providedIn: 'root' }                 │
│                                              │
│  Route-specific?                             │
│    └─ providers: [Store] on the route        │
│                                              │
│  Component-specific?                         │
│    └─ providers: [Store] on the component    │
│                                              │
│  The narrower the scope, the sooner the      │
│  state is destroyed.                         │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Package@ngrx/signals
Entity package@ngrx/signals/entities
RxJS interop@ngrx/signals/rxjs-interop
Store definitionsignalStore(...)
StatewithState(initial)
ComputedwithComputed(fn)
MethodswithMethods(fn)
EntitieswithEntities(config)
Async effectsrxMethod(input, pipe)
State updatepatchState(store, updater)
ScopeRoot, route, or component
TestTestBed.inject(Store)

Key takeaways:

  • Signal Store is NgRx’s recommended approach for new feature and component state. It combines the structure of NgRx with Angular’s signal reactivity. There are no actions, reducers, or selectors — state is signals, computed values are computed(), and methods call patchState .
  • A Signal Store is a tree of composed features. signalStore(withState(...), withComputed(...), withMethods(...)) composes the store from independent features. Reusable features are built with signalStoreFeature(). Composition replaces inheritance .
  • State updates go through patchState. Direct mutation does not exist. patchState(store, partial) merges a partial state, patchState(store, (state) => partial) derives the partial from the current state, and custom updaters like addEntity produce the partial .
  • withEntities provides normalized collection management. The state is { ids: [], entityMap: {} }. The update functions — setAllEntities, addEntity, updateEntity, removeEntity — are passed to patchState. The entityConfig must be passed to every updater .
  • rxMethod handles async side effects with RxJS. It is used inside withMethods to define methods that accept a value, signal, or observable. The input changes trigger re-execution. tapResponse handles success and error without terminating the stream .
  • The store is an Angular provider. Provide it at root for application state, on a route for feature state, or on a component for component-scoped state. The scope determines the lifetime .
  • Signal Store does not replace the classic store. The classic store remains the right choice for complex event-driven architectures with time-travel debugging. Signal Store is for most feature and component state. The two can coexist in the same application .

Remember: Signal Store is the modern NgRx API for state that belongs to a feature or a component. It is signals-native, composable, and has far less boilerplate than the classic store. State lives in withState, derived values in withComputed, mutations in withMethods, collections in withEntities, and async effects in rxMethod. Updates go through patchState. The store is an Angular provider, so its lifetime is whatever scope you give it. Use it for the 80% of state that does not need the full Redux machinery. Keep the classic store for the 20% that does.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!