β€’

newsletter

State management in React is a solved problem

XState with state machines and actors provides all the features for a complete state management solution. XState store replaces useReducer for simple usecases.


Sandro Maglione

Sandro Maglione

Software development

State management is a solved problem. More or less πŸ‘€

I mean, great solutions are all out there, just need to find the right one for your usecase πŸ’πŸΌβ€β™‚οΈ

That's the rabbit hole I explored this week πŸ‘‡

Here is what I found πŸ€”


XState is king πŸ‘‘

This shouldn't be much of a surprise (especially if you followed my in the last months πŸ˜‡)

When state gets complex you realize you need the following:

  • Set initial state from dynamic values (e.g. URLSearchParams)
  • Execute async actions (with loading, error, success states)
  • Avoid calling actions in impossible states (e.g. click button again while loading πŸ₯΄)
  • Compose multiple isolated states/stores
  • All type safe and inferred

XState is the only solution that checks everything from the list (and more πŸ”₯)

State machines everywhere (seriously)

State in any UI is a state machine (state chart to be precise 🀝).

All state management solutions are implicit models of state machines πŸ™Œ

Example: a store (think Redux) is technically a state machine with 1 state and some events to update the context. As long as you don't have other states all is fine πŸ‘‡

A single state with multiple update events is a Redux storeA single state with multiple update events is a Redux store

In practice, you have other states most of the times πŸ’πŸΌβ€β™‚οΈ

There is more 🀩

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.

No state, only context

When no states are needed, XState is (possibly) too much πŸ€”

Since I don't like installing 2 different state management packages, I fallback to useReducer πŸ‘‡

Reducing dependencies is one of the best methods to improve long-term maintainability of a project More dependencies πŸ‘‰ More refactoring πŸ‘‰ Hard to replace (when outdated) πŸ‘‰ More libraries to learn for new people It's a huge pain, don’t do it πŸ™Œ

Sandro Maglione
Sandro Maglione
@SandroMaglione

One huge positive result of using @EffectTS_ in your app is the reduction in number of packages installed πŸ”₯ Effect is a full standard library for typescript, with data structures and util functions included Less dependencies to maintain πŸ˜‡

4
Reply

The idea is simple:

  • State is an object with some values
  • Actions is a union of events
  • reducer gets the previous state and an action and returns the next state

That being said, useReducer is not really type-friendly. Why? These are the types for useReducer:

function useReducer<R extends ReducerWithoutAction<any>, I>(
  reducer: R,
  initializerArg: I,
  initializer: (arg: I) => ReducerStateWithoutAction<R>,
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];

function useReducer<R extends ReducerWithoutAction<any>>(
  reducer: R,
  initializerArg: ReducerStateWithoutAction<R>,
  initializer?: undefined,
): [ReducerStateWithoutAction<R>, DispatchWithoutAction];

function useReducer<R extends Reducer<any, any>, I>(
  reducer: R,
  initializerArg: I & ReducerState<R>,
  initializer: (arg: I & ReducerState<R>) => ReducerState<R>,
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

function useReducer<R extends Reducer<any, any>, I>(
  reducer: R,
  initializerArg: I,
  initializer: (arg: I) => ReducerState<R>,
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

function useReducer<R extends Reducer<any, any>>(
  reducer: R,
  initialState: ReducerState<R>,
  initializer?: undefined,
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

No great, not easy to read, error-prone to say the least πŸ’πŸΌβ€β™‚οΈ

@xstate/store to solve useReducer

Recently a new "state management" solution came out: @xstate/store.

Yes, XState again (kind of πŸ™Œ)

@xstate/store reduces state management to its core (while keeping everything type safe and inferred):

  • Define initial state value
  • Define functions (events) to update the state
store.ts
import { fromStore } from "@xstate/store";

interface State {
  price: number | undefined;
}

const initialState = (initial: Partial<State>): State => ({
  price: initial.price,
});

export const store = (initial: Partial<State>) =>
  fromStore(initialState(initial), {
    UpdatePrice: (_, { value }: { value: string }) => {
      const num = Number(value);
      return {
        price: isNaN(num) || num < 250 ? undefined : num,
      };
    },
  });
"use client";

import { store } from "@/lib/store";
import { useActor } from "@xstate/react";

export default function Component({
  price,
}: {
  /// Initial value from `URLSearchParams` πŸͺ„
  price: number | undefined;
}) {
  /// With `fromStore` the store acts like an actor πŸ‘‡
  const [snapshot, send] = useActor(
    store({ price })
  );
  
  return (
    /// ...
  );
}

@xstate/store works great in combination with XState. You can use both useActor and useSelector from @xstate/react to manage the state in your components πŸͺ„

An @xstate/store store is also easy to then refactor to a full machine if it becomes necessary

The role of useState

useState has a really niche scope 🀏

When to use useState then? πŸ€”

Example: a single search text input that when clicked submits a form. Requirements:

  • A single text value
  • A single update event (i.e. setState)
"use client";

import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";

export default function SearchForm() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const pathname = usePathname();

  const [searchText, setSearchText] = useState("");
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        const newSearchParams = new globalThis.URLSearchParams(searchParams);
        newSearchParams.set("search", searchText);
        router.push(`${pathname}?${newSearchParams.toString()}`);
      }}
    >
      <input
        type="search"
        value={searchText}
        onChange={setSearchText}
      />
    </form>
  );
}

Don't know about you, but I am enjoying a lot the current typescript/web ecosystem:

  • XState for frontend state management
  • Effect for backend and actions logic (+ data structures)
  • React 19 (incoming) with server components and server actions

Not sure I need much more than that 🀝

Regardless, always on the lookout for more, I'll let you know when I find something πŸ‘€

See you next πŸ‘‹

Start here.

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.