Friday, 2 February, 2024 UTC


State management in frontend development deals with maintaining the state or data knowledge across multiple application components. It’s an essential concept while working with frontend JavaScript and TypeScript frameworks and libraries, especially React, React Native, Vue, and Angular.
Dealing with state management in TypeScript brings extra benefits, as TypeScript provides type safety. We can define types in each state management singleton and use the type definition with our modern code editors to show errors and write code with the correct syntax and format.
Although most state management solutions are biased toward React, there are a few for other frontend frameworks, and the number of solutions available continues to rise. In this article, we will compare some commonly used state management solutions in TypeScript and see their basic examples.
Typical requirements for state management solutions
State management plays a crucial role in building modern web applications. It ensures that data is well-organized, updated, and efficiently shared across different parts of the application.
When developers explore various TypeScript state management options, they often seek features that address common challenges while keeping things flexible. Let’s discuss some of the challenges that come with state management and some essential features you might need from the solution you choose.

Dealing with state management challenges

As your projects get bigger, dealing with state complexity can be challenging. Finding a solution that simplifies things without giving up flexibility is crucial.
Maintaining a uniform structure across different application parts is critical for predictable behavior and a user-friendly experience. When handling updates simultaneously and avoiding conflicts, especially in applications with asynchronous tasks, caution and attention to detail are key.
With TypeScript becoming more popular, developers emphasize state management to catch errors early on during compilation rather than waiting for them to appear at runtime.

Essential features in state management solutions

Ideally, your state management solution should make modifying and updating the application state easy, simplifying code debugging and understanding. By centralizing the application state, you gain better control and monitoring, allowing you to track changes better and reducing the risk of inconsistencies.
Embracing a reactive paradigm allows components to automatically update with changes in the underlying state, minimizing the need for manual adjustments to keep the user interface in sync.
Additionally, it’s essential for state management solutions to scale smoothly with the application’s complexity and size, ensuring optimal performance as the project grows.
Finally, developer tools such as time-travel debugging and state inspection add valuable insights into the application’s state at different points in time, enhancing the development process.

Type safety and scalability in TypeScript applications

For TypeScript applications, type safety is of the utmost importance. A state management solution that seamlessly integrates with TypeScript ensures that developers fix type-related errors early in the development process, enhancing code quality and minimizing runtime issues.
As applications scale, the value of TypeScript’s type safety becomes more apparent. A state management solution that scales well with TypeScript promotes efficient collaboration among developers and facilitates the maintenance of large codebases.
Some common TypeScript state management libraries
In the next few sections, we’ll highlight the following TypeScript state management libraries:
  • Redux Toolkit and React Redux
  • MobX
  • NgRx
  • Pinia
  • Recoil
  • React Query
  • Jotai
We’ll go over which frameworks these state management solutions are for and some examples of how to install and use them.

Redux Toolkit and React Redux

We can use React Redux and Redux Toolkit in our React and React Native projects. The React Redux library allows us to seamlessly integrate the Redux state management tool with React apps. Meanwhile, Redux Toolkit further simplifies the configuration and usage of Redux in our projects.
Redux has many features and benefits, especially when using TypeScript with React. Some of these include:
  • Helps you build applications that are reliable across various environments, whether it’s on the web, server-side, or in mobile apps
  • Provides test-friendly development experience
  • Enhances development experience with features like live code editing and a debugger that lets you travel back and forth in time to inspect your application’s state
  • Redux Toolkit provides an easy and simple API interface for using Redux in your application
To install it, simply run the command below:
npm install @reduxjs/toolkit react-redux
Let’s see some examples of how to use Redux Toolkit and React Redux.

Create a Redux store

Let’s create a file named src/lib/store.ts and copy in the following code:
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';

const rootReducer = combineReducers({
  counter: counterReducer,

export default configureStore({
  reducer: rootReducer,

export type IRootReducer = ReturnType<typeof rootReducer>;
This creates a Redux store and automatically configures the Redux DevTools extension so we can inspect the store while developing. We will implement the counterReducer later in this tutorial.
Once the store is created, we can make it available to our Redux Toolkit components by putting a React Redux <Provider> around our application in src/index.tsx. Let’s import the Redux store we just created, add the <Provider> in the <App> component, and pass the store as a prop:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';

import { App } from './App';
import store from './lib/store';

const root = createRoot(document.getElementById('app'));

    <Provider store={store}>
      <App />

Create a Redux state slice

Let’s add a new file named counterSlice.ts in the src/lib/features/counter/ directory. In this file, let’s bring in the createSlice API from the Redux Toolkit.
To make a slice, we should give it a name, an initial state, and one or more reducers to determine how the state can change. Once the slice is created, we can export the Redux action creators it generates and the entire slice’s reducer function.
With Redux Toolkit’s createSlice and createReducer, we can write our state updates like we are making changes directly, even though Redux demands immutable updates:
import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  reducers: {
    increment: (state) => {
      state.value += 1;
    decrement: (state) => {
      state.value -= 1;
    incrementByAmount: (state, action) => {
      state.value += action.payload;

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
The code above sets up a simple counter in a React application and defines a Redux state slice to manage the counter’s state.

Use Redux state and actions in React components

Now, we can use the React Redux hooks to let React components interact with the Redux store. We can read data from the store using useSelector and dispatch actions using useDispatch.
Let’s modify our App.tsx component like below to show the counter and increase and decrease its value:
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { decrement, increment } from './lib/features/counter/counterSlice';
import { IRootReducer } from './lib/store';

export function App() {
  const count = useSelector<IRootReducer, number>(
    (state) => state.counter.value
  const dispatch = useDispatch();

  return (
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
The Counter component we set up here interacts with the counterSlice component we created previously to access the current state of the counter slice and display and modify its values in the component.
You can check out this example in the StackBlitz repo.
Although we focused on the React app in this article, we can use Redux in almost all JavaScript and TypeScript view libraries. For more information on getting started on Redux state management, you can visit the Redux Getting Started page.

MobX with MobX React Lite

MobX is a well-tested library for managing states in a simple and scalable way using functional reactive programming. The idea is to keep the code minimal and straightforward.
If we want to update a field in a record, we can use a regular JavaScript assignment, and MobX will update everything else automatically. It also ensures efficient rendering and tracking of changes to our data at runtime to only update what’s necessary, saving us from manual optimizations like memoization.
What’s remarkable about MobX is its flexibility. We can handle our application state independently of any UI framework. This makes the code more modular, portable, and easy to test. So, in a nutshell, MobX helps us manage state effortlessly and efficiently in your applications.
MobX is versatile — it works in various environments, like browsers and Node.js projects that support ES5.
Regarding React bindings for MobX, you can choose between mobx-react-lite for functional components or mobx-react for functional and class-based components. To install MobX, simply add the correct binding to your Yarn or npm command based on your needs:
npm install --save mobx
npm install --save mobx-react-lite # or, npm install --save mobx-react (based on your preference)

Example usage of MobX for state management in React

Let’s work with another React app, this time to create a timer instead of a counter. Paste this code inside a React component:
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react-lite';

function createTimer() {
  return makeAutoObservable({
    secondsPassed: 0,
    increase() {
      this.secondsPassed += 1;
    reset() {
      this.secondsPassed = 0;

const myTimer = createTimer();

const TimerView = observer(
  }: {
    timer: { secondsPassed: number; increase(): void; reset(): void };
  }) => (
    <button onClick={() => timer.reset()}>
      Seconds passed: {timer.secondsPassed}

export const App = () => <TimerView timer={myTimer} />;

setInterval(() => {
}, 1000);
You can view and interact with the example in this StackBlitz repo.
When we wrap the TimerView React component with the observer, it understands that it needs to update whenever the timer.secondsPassed changes, even if we don’t explicitly mention it. Thanks to the reactivity system in MobX, our component will automatically re-render whenever that specific field is updated.
So, whenever we click on the button or use setInterval, it triggers an action — Timer.increase or myTimer.reset — updating the observable state, or myTimer.secondsPassed. This update then propagates smoothly to all the computations and side effects like TimerView that rely on those changes:
Although we focused on the React app in this article, MobX is supported by some other view libraries as well. For more details, you can visit the MobX documentation.


NgRx is a store management solution for Angular. It’s an RxJS-based global state management solution inspired by Redux that helps you handle the application’s state in a way that makes it easy to maintain and understand. Using a single state and actions lets you clearly express how your application’s state changes over time.
Some notable features of the NgRx Store include:
  • Follows the principle of a single immutable state tree, making it the single source of truth for the entire application
  • Seamlessly integrates with Angular, aligning with Angular’s architecture and providing a consistent way to manage state in Angular applications
  • Utilizes TypeScript for strong typing, improving development tooling, and reducing the likelihood of runtime errors
  • Designed to be testable, making it easier to write unit tests for actions, reducers, effects, and selectors
  • Leverages Angular’s dependency injection system, making it easy to inject the store into components, services, and other parts of the application
To install the NgRx Store in your project, you need to enter the following command in your terminal:
ng add @ngrx/store@latest

Example usage of NgRx for state management

Let’s create a new Angular project using Angular CLI. Please check the Angular documentation for help if you are new to Angular or Angular CLI. After initializing a new Angular project and installing our @ngrx/store library, we can proceed to the next stage.
Note that we’re implementing our simple NgRx state management example using Angular 17, which differs significantly from previous versions. Also, this tutorial differs slightly from the original documentation. If you are using an older Angular version, you should follow the appropriate documentation.
Let’s work on another counting example. Let’s create a new file, counter.actions.ts, in the src/app/ directory and paste the following code:
import { createAction } from '@ngrx/store';

export const increment = createAction('[Counter Component] Increment');
export const decrement = createAction('[Counter Component] Decrement');
export const reset = createAction('[Counter Component] Reset');
The above code describes unique events that are dispatched from components and services. Next, we’ll define a reducer function — src/app/counter.reducer.ts — to handle changes in the counter value based on the provided actions:
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset } from './counter.actions';

export const initialState = 0;

export const counterReducer = createReducer(
  on(increment, (state) => state + 1),
  on(decrement, (state) => state - 1),
  on(reset, (state) => 0)
Now, let’s add the counterReducer in the app.config.ts:
import { ApplicationConfig } from '@angular/core';

import { provideStore } from '@ngrx/store';
import { counterReducer } from './counter.reducer';

export const appConfig: ApplicationConfig = {
  providers: [provideStore({
    count: counterReducer
Let’s create a new component called my-counter using the following command:
ng g c my-counter
This command should create all the files we need, including the my-counter.component.ts file and the my-counter.component.html file. Copy the following code into the my-counter.component.ts file:
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { increment, decrement, reset } from '../counter.actions';
import { CommonModule } from '@angular/common';

  selector: 'app-my-counter',
  templateUrl: './my-counter.component.html',
  standalone: true,
  imports: [CommonModule],
export class MyCounterComponent {
  count$: Observable<number>;
  constructor(private store: Store<{ count: number }>) {
    this.count$ ='count');
  increment() {;
  decrement() {;
  reset() {;
Next, let’s copy the following code in the my-counter.component.html file:
<button (click)="increment()">Increment</button>
<div>Current Count: {{ count$ | async }}</div>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset Counter</button>
Now, let’s add this new component in AppComponent. Paste the following code in your app.component.ts file:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { MyCounterComponent } from './my-counter/my-counter.component';
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet, MyCounterComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
export class AppComponent {
  title = 'angular-project';
Finally, declare it in the app template:
<h1>Hello World!</h1>
The code above sets up a simple counter in an Angular application using the NgRx Store for state management.
For more details, you can go through the NgRx documentation.


Pinia is the official state management library for Vue. It enables seamless state-sharing among components and pages. While the Composition API allows us to share global states easily in SPAs, it might expose our app to security vulnerabilities if it’s rendered on the server side, making Pinia crucial.
Even in smaller applications, Pinia brings several advantages, including:
  • Devtools support
  • A timeline for action and mutation tracking
  • Stores conveniently appearing where they are used in components
  • Time travel for debugging
  • Hot module replacement for modifying stores without reloading the page
  • The ability to keep the existing state during development
Pinia also offers plugins to extend its features, robust TypeScript support, and compatibility with server-side rendering.
To use Pinia, you must first install it using your favorite package manager:
yarn add pinia
# or with npm
npm install pinia
Then, create a Pinia instance — the root store — and pass it to the app as a plugin:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

If you are using Vue 2, you also need to install a plugin and inject the created Pinia at the root of the app:
import { createPinia, PiniaVuePlugin } from 'pinia'

const pinia = createPinia()

new Vue({
  el: '#app',
This will also add support for Devtools. While Vue 3 currently lacks certain features like time traveling and editing due to Vue Devtools limitations, the overall developer experience is significantly enhanced with many Devtools features.

Basic example using Pinia for state management

Let’s keep working on the counting example. Create a file called src/stores/counter.ts and paste in the following code:
// src/stores/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
After that, we can use it in a component as shown below:
<script setup>
import { useCounterStore } from './stores/counter';

const counter = useCounterStore();

  <div>Current Count: {{ counter.count }}</div>
  <button @click="counter.increment()">Increment</button>
As we can see, we are getting the state info from useCounterStore(). From that, we can show the state value through the component and also modify the state values using the store actions.
You can check the complete StackBlitz repo to see and interact with the demo. For more details, please visit the Pinia documentation.


Recoil is another state management library for React, built by Meta. It lets you create a data-flow graph where you flow your shared states (atoms) through functions (selectors) and eventually reach your React components.
Some notable Recoil features include:
  • Introduces the concept of an “atom,” which represents a piece of state. Atoms can be easily created and accessed, providing a simple and efficient way to manage the state
  • Provides selectors, which allow us to derive computed values from atoms. These can be used to create a derived state or perform calculations based on the current state. Atoms are like state units that your components can subscribe to, and selectors can transform this state synchronously or asynchronously
  • Designed to work well with TypeScript, providing type safety for your state. This includes defining the types of your atoms and selectors and enhancing the development experience
  • Supports asynchronous operations, allowing you to handle async data and side effects using asynchronous selectors or effects
Installing Recoil is pretty straightforward, just like the other packages. We can use any of our favorite package managers to install the state management library in our React app:
npm install recoil
# or using yarn
yarn add recoil
# or using bower
bower install --save recoil

Understanding RecoilRoot

Components with states that are managed by Recoil need the RecoilRoot component to appear somewhere in the parent tree. The RecoilRoot component is essential for creating the context for Recoil state management. Without adding this, the Recoil state management will not work.
An excellent place to put this component is in your root component, like so:
import {
} from 'recoil';

export const App = () => {
  return (
      <CharacterCounter />
In this example, we are building a character-counter example. We will take the text input and show the character length as output. We’ll implement the CharacterCounter component in the following section.

The purpose of an atom in Recoil

An atom represents a piece of state. Atoms can be read and written from any component. Components that read the value of an atom are implicitly subscribed to that atom, so any atom updates will result in a re-render of all components subscribed to that atom:
const textState = atom({
  key: 'textState',
  default: '',
Usually in React, we use the useState Hook to read from and write to the local state of a component. But with Recoil, to read from and write to an atom, we need to use useRecoilState() like this:
const [text, setText] = useRecoilState(textState); 
We can read from the text variable and write using the setText function. So, components that need to read from and write to an atom should use useRecoilState() as shown below:
const TextInput = () => {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {

  return (
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}

const CharacterCounter = () => {
  return (
      <TextInput />
      <CharacterCount />

Using selectors in Recoil

A selector represents a piece of derived state, which is a transformation of a state that gets computed based on other states and can change when those states change. We can think of the derived state as the output of passing a state to a pure function that modifies the given state in some way:
const charCountState = selector({
  key: 'charCountState',
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
The code above determines the length of the textState value. We can use the useRecoilValue() Hook to read the value of charCountState and show the character count of the input value in our component:
const CharacterCount = () => {
  const count = useRecoilValue(charCountState);
  return <>Character Count: {count}</>;
You can find the full code for this example in this StackBlitz repo. For more details, you can visit the Recoil documentation.

React Query (TanStack Query)

React Query — officially called TanStack Query — is an asynchronous state management library for React and React Native. It can handle tasks like data fetching, caching, synchronizing, and updating server state in your React apps.
While other state management solutions provide a way to view and update states across the application, React Query provides solutions to manage the data that we get from the API.
We can install React Query by running the following command:
npm i @tanstack/react-query
# or
yarn add @tanstack/react-query

Basic example using React Query for state management

We can fetch the data from an API using the useQuery Hook. Similarly, we can request any POST, PUT, PATCH, or DELETE operation using the useMutation Hook. Here is a simple example in the React App.tsx file, where we will fetch some todo data from a fake API:
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient()

const API_ENDPOINT = ``;

const Todo = () => {
  const { data } = useQuery({
    queryKey: [`${API_ENDPOINT}/todos`],
    queryFn: () => fetch(`${API_ENDPOINT}/todos`).then((res) => res.json()),
  return (
      {(data || []).map((item) => (
        <div key={}>
          <h4 style={{ lineHeight: '100%', marginBottom: 0 }}>{item.title}</h4>
          <p>{item.completed ? 'Done' : 'Not Done Yet'}</p>

export const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <Todo />
The complete code is available in this StackBlitz repo. To learn more, you can go through the React Query documentation.


Jotai is another state management library for React. Like Recoil, it takes an atomic approach to global React state management.
We can create a state by putting together “atoms,” or pieces of state. The renders automatically get smarter based on what these atoms depend on.
This clever trick solves the problem of unnecessary re-renders in the React context, eliminating the need for memoization. It gives developers a smooth experience, similar to using signals while sticking to a clear and straightforward programming style, making it scalable.
To install Jotai in your project, simply use one of the commands below:
# npm
npm i jotai

# yarn
yarn add jotai

# pnpm
pnpm add jotai

Configuring Jotai for your framework

Adding the optional SWC or Babel plugin is recommended to enable React Fast Refresh support for the best developer experience specific to each framework. Let’s see some popular configuration options.
If you want to add the SWC plugin to a Next.js project, do the following:
# npm
npm install --save-dev @swc-jotai/react-refresh

# next.config.js
experimental: {
  swcPlugins: [['@swc-jotai/react-refresh', {}]],
Meanwhile, you can add the Babel plugin to your Next.js project like so:
># .babelrc
  "presets": ["next/babel"],
  "plugins": ["jotai/babel/plugin-react-refresh"]
For Vite, you can add the SWC plugin to a React project with the code below:
# npm
npm install --save-dev @swc-jotai/react-refresh

# vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';

export default defineConfig({
  plugins: [
      plugins: [['@swc-jotai/react-refresh', {}]],
For Gatsby, you can add the Babel plugin with the code below:
# npm
npm install --save-dev babel-preset-gatsby

# .babelrc
  "presets": ["babel-preset-gatsby"],
  "plugins": ["jotai/babel/plugin-react-refresh"]

# gatsby-config.js
flags: {
  DEV_SSR: false,

Basic example using Jotai for state management

Here is a basic example of state management in Next.js with Jotai, where we will manage our state taken from the input, transform its value to uppercase value, and show it to another component.
Let’s first add our atoms in src/lib/atom.ts file:
import { atom } from 'jotai';

export const textAtom = atom('hello');
export const uppercaseAtom = atom((get) => get(textAtom).toUpperCase());
Now, let’s add these two components in our src/components folder: Input component and Uppercase component:
// src/components/Input.tsx
'use client';

import { textAtom } from '@/lib/atoms';
import { useAtom } from 'jotai';

export const Input = () => {
  const [text, setText] = useAtom(textAtom);
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>

  return <input className="border" value={text} onChange={handleChange} />;

// src/components/Uppercase.tsx
'use client';

import { uppercaseAtom } from '@/lib/atoms';
import { useAtom } from 'jotai';

export const Uppercase = () => {
  const [uppercase] = useAtom(uppercaseAtom);
  return <div>Uppercase: {uppercase}</div>;
Now, let’s add these two components to our homepage via the src/app/page.tsx component file:
import { Input } from '@/components/Input';
import { Uppercase } from '@/components/Uppercase';

const Home = () => {
  return (
      <Input />
      <Uppercase />

export default Home;
The complete code is available in this StackBlitz repo. For more information, you can go through the Jotai documentation.
Considerations for choosing a state management solution
While choosing state management libraries for your TypeScript projects, you should consider the following:
  1. Performance considerations: One primary goal while building and developing applications is to enhance UX and performance. Try to choose your state management solution depending on your specific project requirements to improve performance and flexibility
  2. DX and ease of use: You might be comfortable with a particular set of state management libraries or more familiar with some specific frameworks. This is important to keep in mind while choosing a state management solution, as most are primarily biased toward a particular set of frameworks or libraries. It helps to start with the state management library you’re most comfortable with, especially if it is easy for other developers on your team to learn and use
  3. Community support and documentation: Choosing a state management library with the most community support and rich documentation is ideal. Without those resources, you might run into some problems and be unable to find any solutions, which can especially cause trouble as the project grows bigger
With that in mind, here is a comparison table of the solutions we reviewed in this article to help you make your choice:
Tool Library/framework support Popularity Documentation Community support Ease of use/DX
Redux Almost all JS / TS view libraries 60.2K stars on GitHub. Popular among almost all professional React developers Good Extensive, large, and growing community support Easy
MobX Almost all JS / TS view libraries 27K stars on GitHub. Known by some professional and expert developers; less popular than Redux Good Has smaller community support than Redux Hard
NgRx Angular 7.8K stars on GitHub. Popular among almost all professional Angular developers The Angular version is usually updated frequently, but the documentation can be outdated at times Extensive, large, and growing community support Easy
Pinia Vue 12K stars on GitHub. Popular among almost all professional Vue developers Good Community support is growing Easy
Recoil React 19.3K stars on GitHub. Known by some professional React developers; less popular than Redux Good Community support is growing Easy
React Query (TanStack Query) React 38.2K stars on GitHub. Used by many professional React developers Good Extensive, large, and growing community support Easy
Jotai React 16.5K stars on GitHub. Relatively less popular Good Community support is growing Easy
Here are a few resources you can check out for more in-depth comparisons:
  • Jotai vs. Recoil: What are the differences?
  • Comparing Redux vs. Vuex
  • Redux vs. MobX: Which performs better?
  • Pinia vs. Vuex: Which state management library is best for Vue?
In this article, we’ve seen many state management libraries that we can use in any of our TypeScript projects. Our choice of state management solution depends on considerations such as the project library, performance considerations, developer experience, community support, and more.
Generally speaking, state management libraries ease our lives by making our projects scalable, faster, and more robust. State management solutions with TypeScript support improve developer support and minimize bugs and typing errors.
The post Comparing TypeScript state management solutions appeared first on LogRocket Blog.