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 π€©
Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights
Not convinced? Well, let me tell you more about it
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 π