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 π)
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 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 π
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 π
The idea is simple:
State
is an object with some valuesActions
is a union of eventsreducer
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 π€
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
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 π)
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 π