State management in React is a solved problem

Sandro Maglione

Sandro Maglione

Web 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 πŸ‘‡

You should probably avoid useState most of the times πŸ‘‰ XState for complex state logic (events, async actions, multiple states) πŸ‘‰ useReducer for most forms (no states but a single object to update) useState only for single primitive values (rare πŸ™Œ)

8
Reply

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.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.

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

πŸ’‘ Type safe useReducer πŸ’‘ 1️⃣ State interface (with initial state) 2️⃣ Actions (union of events) 3️⃣ Reducer (previous state + action = return next state) Initialize useReducer with the reducer function and initial state 🀝

Image
Image
12
Reply

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 🀏

You should probably avoid useState most of the times πŸ‘‰ XState for complex state logic (events, async actions, multiple states) πŸ‘‰ useReducer for most forms (no states but a single object to update) useState only for single primitive values (rare πŸ™Œ)

8
Reply

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.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.