β€’

tech

The Client stack for 2026

You can reduce all the complexity of working on the client to a few core dependencies: TanStack Router, Effect, XState. This is how I use them all, to implement everything on the client.


Sandro Maglione

Sandro Maglione

Software

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"
  }
}

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 useState or useEffect at 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 effect for 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 part xstate).

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 πŸ—οΈ


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 πŸ‘‹

Start here.

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