Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
••••••••••••••••••••
Intelligence View
⚡ tsecurity.de Intelligence

A simple pattern for versioned persisted state in React Native

I’ve only been working with React Native for a few weeks. I thought persisted state would be a solved problem. Store some JSON, read it back, done. That a…

Beitrag
0
Seite
0
↗ Quelle (dev.to)
Social ReaktionenReagiere als Erste:r — dein Feedback zählt!

I’ve only been working with React Native for a few weeks. I thought persisted state would be a solved problem. Store some JSON, read it back, done.



That assumption lasted exactly one day.






The problem I ran into (very quickly)



In my first React Native project, I needed to persist user preferences using AsyncStorage. At first, it was trivial:




await AsyncStorage.setItem("state", JSON.stringify(data));






But then, as I progressed through my project implementation:




  • I added a new field

  • I renamed another one

  • I needed defaults for existing users

  • I wanted TypeScript to actually help me

  • I really didn’t want to silently wipe user data



So I went looking for a solution...






What I found




  • State libraries that serialize state, but don’t help you evolve it

  • Migration systems that are either tightly coupled to a framework or runtime-only and loosely typed



What I wanted was boring, explicit, and safe.

So I wrote my own version.



Today, I’m sharing this so others can use it directly or borrow whatever ideas are useful for their own codebases.





The idea: treat persisted state like a schema, not a blob



The mental shift that helped was this:




Persisted state is not “just JSON” — it’s data with a versioned schema.




Once you accept that, the rest becomes fairly mechanical:




  1. Define a schema (with defaults)

  2. Store a version alongside the data

  3. When the schema changes, migrate old data forward

  4. Validate everything



That’s it.





The core principles I stuck to



When I built this, I had a few "non-negotiables":





  • Type safety end to end


  • Explicit migrations (no magic inference)


  • Deterministic upgrades (no “best effort”)


  • Storage-agnostic (AsyncStorage, localStorage, memory)


  • Easy to delete later if I decide I don’t like it



This last point matters more than people admit.





A minimal example



Here’s what persisted state looks like with this approach.





1. Define a schema (Zod)





import { z } from "zod";

export const persistedSchema = z.object({
_version: z.number(),
preferences: z.object({
colorScheme: z.enum(["system", "light", "dark"]).default("system"),
}).default({ colorScheme: "system" }),
});

export type PersistedState = z.infer<typeof persistedSchema>;





Zod gives me:




  • runtime validation

  • compile-time types

  • defaults for free





2. Create storage (AsyncStorage, localStorage, or memory)





import { createPersistedState } from "@sebastianthiebaud/schema-versioned-storage";
import { createAsyncStorageAdapter } from
"@sebastianthiebaud/schema-versioned-storage/adapters/async-storage";

const storage = createPersistedState({
schema: persistedSchema,
storageKey: "APP_STATE",
storage: createAsyncStorageAdapter(),
migrations: [],
getCurrentVersion: () => 1,
});

await storage.init();





At this point, data is loaded, validated, defaults are applied, and migrations (if any) are run





3. Use it (fully typed)





const theme = storage.get("preferences").colorScheme;

await storage.set("preferences", {
colorScheme: "dark",
});





If I mistype a key or value, TypeScript yells at me before runtime.





Migrations are explicit and boring (by design)



When the schema changes, I add a migration.




import type { Migration } from
"@sebastianthiebaud/schema-versioned-storage";

const migration: Migration<PersistedState> = {
metadata: {
version: 2,
description: "Add language preference",
},
migrate: (state: unknown) => {
// `as any` required here since the old schema is no more :-(
const old = state as any;
return {
...old,
_version: 2,
preferences: {
...old.preferences,
language: "en",
},
};
},
};

export default migration;






No inference. No guessing. No “maybe it works”.



If a migration is missing or invalid, initialization fails loudly.






Adapters



Storage APIs differ just enough to be annoying. So I standardized on a tiny adapter interface:




interface StorageAdapter {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
}






That gives me:




  • AsyncStorage for React Native

  • localStorage for web

  • an in-memory adapter for tests



Testing migrations with an in-memory adapter turned out to be way nicer than mocking AsyncStorage.






React integration (optional)



I didn’t want this tied to React, but it is convenient to avoid prop drilling. So there’s an optional context + hook:




<StorageProvider storage={storage}>
<App />
</StorageProvider>

...

const storage = useStorage<PersistedState>();






No magic. Just a thin wrapper around the same storage instance.






The CLI exists purely to remove friction



I also added a small CLI because I got tired of writing boilerplate:




  • generate migration files

  • generate migration indexes

  • hash schemas to detect changes



Example:




npx svs generate-migration --name add-field --from 1 --to 2






Nothing fancy — just fewer footguns.






Is this “the best” solution?



Probably not.



But it is:




  • understandable in one sitting

  • easy to delete if you hate it

  • explicit about how data evolves

  • type-safe in the places that matter



And for me, that’s a win.

If this helps you, great — if not, steal the ideas :-)



I’m sharing this mostly because I couldn’t find something that matched this mental model when I started.



If you use it directly, copy parts of it, or just steal the migration pattern, then that’s a success in my book.



The code is here:

👉 https://github.com/SebastianThiebaud/schema-versioned-storage



Happy to hear how other people are handling persisted state evolution in React Native — I’m still learning too.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten A simple pattern for versioned persisted state in React Native

Thematisch verwandte Begriffe: simple, pattern, versioned, persisted · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag