A "new" client tech stack for 2026 π
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"build": "vite build && tsc",
"serve": "vite preview",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@effect/platform": "^0.92.1",
"@effect/platform-browser": "^0.72.0",
"effect": "^3.18.4",
"@tanstack/react-router": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
"@xstate/react": "^6.0.0",
"xstate": "^5.23.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.16",
"tailwindcss": "^4.1.16",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"vite": "^7.1.7"
}
}This is the client stack for 2026 ποΈ
And the above it's all that I need. I mean, all.
That's how it all fits together π
TanStack Router: client-first
All of my past projects use next.
But most of them with output: "export", a single static export, client-only.
Server components are executed once at build time ποΈ
next focuses a lot on the "server", forcing you to explicit "use client" to switch to "client mode".
Plus, it adds a lot of "magic" on the server, with more than a few gotchas (looking at you cookies() π).
For a more client-first approach, lately I started using more TanStack Router (with TanStack Start lurking as well):
- Ease of
vite, without magic - Powerful type safe routing
- Client by default (no
"use client"needed)
My new default choice for client apps π«‘
Effect everything
All the hard stuff are powered by effect. Even on the client.
A lot depends on how client-heavy is your app.
A stark example is local-first, with the database on the client ποΈ
For example, PGLite:
- Initialize PGLite
- Type safe queries with Effect SQL
- Migrations
- Local storage (
IndexedDb/KeyValueStore)
It all becomes easy with effect in the loop, with solutions for every single step.
Even if you app is less client centric, effect has something for you. Example with local storage (KeyValueStore):
import { KeyValueStore } from "@effect/platform";
import { BrowserKeyValueStore } from "@effect/platform-browser";
import { Effect, Function, Schema } from "effect";
export const LanguageSelection = Schema.Literal("en", "ja");
const _StoreKey = "language";
export class Language extends Effect.Service<Language>()("Language", {
accessors: true,
dependencies: [BrowserKeyValueStore.layerLocalStorage],
effect: Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
return {
getLanguage: store.get(_StoreKey).pipe(
Effect.flatMap(Function.identity),
Effect.flatMap(Schema.decodeUnknown(LanguageSelection)),
Effect.orElse(() => Effect.succeed("ja" as const))
),
setLanguage: (language: typeof LanguageSelection.Type) =>
store.set(_StoreKey, language),
};
}),
}) {
static readonly StoreKey = _StoreKey;
}Effect backend and frontend
If you backend is also effect, then it all becomes even more type-safe and easy.
You can have a shared API signature, and derive a type-safe client API from it.
You can read and learn more in Paddle Billing Payments Full Stack TypeScript App π
Here is a super simple and super type-safe client API:
import { ServerApi } from "@app/api"; // π Shared signature
import { FetchHttpClient, HttpApiClient } from "@effect/platform";
import { Effect } from "effect";
export class ApiClient extends Effect.Service<ApiClient>()("ApiClient", {
dependencies: [FetchHttpClient.layer],
effect: Effect.gen(function* () {
return yield* HttpApiClient.make(ServerApi, {
baseUrl: "http://localhost:8787",
});
}),
}) {}State management everything with XState
The last piece is state management, and xstate has everything.
I mostly stopped using
useStateoruseEffectat all, I create a new state machine by default every time π
I often add the machine inline with the component:
const machine = setup({
types: {},
actors: {},
}).createMachine({});
export default function MyComponent() {
const [snapshot, send] = useMachine(machine);
return null;
}XState covers all the use cases of state management:
- Initial values, sync or async
- Async requests (
fromPromise) - State machine (only valid states)
- Isolated actors (easy to test)
- Actors communication (easy to compose)
Bonus: XState works wonders in combination with
effectfor business logic β¨
And that's all. All:
{
"dependencies": {
// `effect`
"@effect/platform": "^0.92.1",
"@effect/platform-browser": "^0.72.0",
"effect": "^3.18.4",
// Routing
"@tanstack/react-router": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
// State management
"@xstate/react": "^6.0.0",
"xstate": "^5.23.0",
// Framework
"react": "^19.2.0",
"react-dom": "^19.2.0",
}
}Everything else (if anything) is project-specific. I may bring a package to simplify cookies management or headless components, but the core of the logic stays in effect, xstate, TanStack Router.
And you will go a long way with this, and fast π
Bonus: AI is becoming insane at working with
effect(and in partxstate).Especially if you can point the AI to already-written code example, you can just write prompts and let AI+type-safety bring you to a full working app ποΈ
This prompt (nearly) implemented a complete working component with @EffectTS_ and XState Lesson: be precise (exact API requested) and provide examples from the same codebase
We are getting closer and closer to a few major releases. 2026 is going to be another major step forward, wait and see π
See you next π
