For a detailed overview, visit the NgRx SignalStore documentation.

State

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

export const BooksStore = signalStore(
  withState(initialState) // 👈
);
 <p>Query: {{ store.filter.query() }}</p>
export const BooksStore = signalStore(
  withState(initialState),
  withComputed(({ books, filter }) => ({ // 👈
    booksCount: computed(() => books().length)
  })
)
export const BooksStore = signalStore(
  withState(initialState),
  withComputed(/* ... */),
  withMethods((store) => ({ // 👈
    updateQuery(query: string): void {
      patchState(store, (state) => ({ filter: { ...state.filter, query } }));
    },
  }))
);


Where SignalStore Falls Short

const BOOKS_STATE = new InjectionToken<BooksState>('BooksState', {
  factory: () => initialState,
});

const BooksStore = signalStore(
  withState(() => inject(BOOKS_STATE))
);
  withMethods((store) => ({
    setLoading(loading: boolean): void {
      patchState(store, { loading });
    },
    updateOrder(order: 'asc' | 'desc'): void {
      patchState(store, (state) => ({ filter: { ...state.filter, order } }));
    },
  }))
withMethods(store => {
  function setLoading(loading: boolean): void {
    patchState(store, { loading });
    // 👇 call another method
    updateQuery(/* ... */);
  }

  function updateQuery(query: string): void {
    patchState(store, (state) => ({ filter: { ...state.filter, query } }));
  }
  // 👇 Expose accessbile methods from the store
  return { setLoading, updateQuery }; 
})

For derived state, use computed signals to minimize redundant calculations (docs). However, with SignalStore every state slice is a signal so you don’t need to create a “selector” for every slice of state like you do with NgRx Store.

Asynchronous updates

withMethods((store, api = inject(HackerNewsDataService)) => ({
  loadNewestStories: rxMethod<void>( // `rxMethod` for managing side effects
    pipe(
      switchMap(() => // any normal RxJs async logic
        api
          .loadStoryEntitiesRange({ start: 0, end: 30 })
          .pipe(tap(stories => patchState(store, addEntities(stories))))
      )
    )
  ),
type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

export const TodosStore = signalStore(
  withEntities<Todo>() // add entity feature
);
import { addEntity, entityConfig, withEntities } from '@ngrx/signals/entities';

type Todo = {
  key: number;
  text: string;
  completed: boolean;
};

const todoConfig = entityConfig({
  entity: type<Todo>(), // type inference
  collection: 'todo', // custom name, for multiple entities
  selectId: (todo) => todo.key, // custom entity identifier
});

export const TodosStore = signalStore(
  withEntities(todoConfig), // add entity feature
  withMethods((store) => ({
    addTodo(todo: Todo): void {
      patchState(store, addEntity(todo, todoConfig)); // uses `addEntity` to from @ngrx
    },
  }))
);