Svelte 5 State in Classes Simplifies My Code
Table of Contents
- Svelte 5 State in Classes Simplifies My Code
- The short version: class fields are now reactive
- The commits that triggered the shift
- Before/after: stores vs class state
- Example: a GameManager with real state and real methods
- Before/after: automation manager
- Smaller managers stay tiny
- A currency manager that acts like a real service
- Practical tips I learned
- Wrap-up
Svelte 5 made a small change that had a big impact on my code: you can place $state directly in class fields. That single capability let me replace a pile of tiny store files with focused managers that read like plain TypeScript. This article walks through the refactor I did in Atom Clicker and why it made the code easier to reason about.
This is for my project Atom Clicker, a Svelte 5 incremental game where you build atoms, unlock realms, and automate upgrades. The state surface is large, so clean state management matters a lot.
If you are new to Svelte 5 runes, the two key docs are:
The short version: class fields are now reactive
Svelte 5 does not proxy class instances. Instead, you declare reactive fields using $state inside the class. The compiler turns those fields into getters and setters, so reads and writes stay ergonomic:
- You write
this.atoms += 1instead ofstore.update(...). - You keep all state and methods in one class, which makes features easier to find.
- You can still use $derived and $effect in the same class for computed values and side effects.
The commits that triggered the shift
These are the changes that introduced class-based state managers in Atom Clicker:
- GameManager refactor consolidated scattered stores into one class.
- AutoBuy, AutoUpgrade, Realm managers moved automation and UI state into classes.
- Component updates replaced
gameStoreusage withgameManager. - Currencies manager simplified currency logic and iteration.
The common theme is that managers became small, cohesive modules with their own state and behavior.
Before/after: stores vs class state
Before, my state lived in scattered stores with helper functions. The shape below is a simplified example of the old Svelte store pattern:
After the refactor, state and behavior live side by side in a class, and the class fields are reactive:
The big win is that all related logic sits in one class, without the indirection of store helpers.
Example: a GameManager with real state and real methods
Because $state can live in class fields, a manager feels like plain TypeScript instead of a Svelte store wrapper. Here is a trimmed down version from GameManager.svelte.ts:
This is still just a TypeScript class, but the fields are reactive. Components can import a single instance and read or write values directly. That is the main simplification: no more wrapping everything in a store shape.
Before/after: automation manager
Before, automation behavior often lived in a store file that exported timers and update functions. With class state, the same logic becomes a compact service. You can see the full implementation in autoBuy.svelte.ts:
This keeps the lifecycle, timers, and reactive fields together, which is much easier to maintain.
Smaller managers stay tiny
State in classes is just as useful for tiny, focused modules. RealmManager and AutoBuyManager both benefit from this. They hold their own reactive fields, and expose normal methods:
The code reads like standard OOP, but the UI stays reactive because $state and $effect are doing the Svelte work behind the scenes.
A currency manager that acts like a real service
A manager class also makes it easy to centralize a data structure with clear methods, as seen in CurrenciesManager.svelte.ts:
This removed a lot of repeated logic and made it easier to iterate over all currencies in one place.
Practical tips I learned
- Put these managers in
.svelte.tsfiles, which is where Svelte 5 lets you use runes outside components. - Export an instance like
export const gameManager = new GameManager();instead of exporting a$statevariable that is directly reassigned (which is restricted). - If you pass a class method directly as a callback, you can lose
this. Use() => manager.method()or an arrow method on the class. - If a large object does not need deep reactivity, consider $state.raw to avoid proxy overhead.
- Experiment with the Svelte 5 Playground to see the compiled output of your classes.
Wrap-up
Svelte 5 class state let me collapse a tangle of stores into a few straightforward managers. The refactor in Atom Clicker made the code easier to navigate, and the UI still updates as expected with simple property writes. If you already like organizing logic in classes, $state finally makes that style feel native in Svelte.