Kako Valtio pojednostavljuje React state management

Kako Valtio pojednostavljuje React state managementKako Valtio pojednostavljuje React state management

18. pro. 2025. - 13 min

Roko Ponjarac

Roko Ponjarac

Software Engineer


Upravljanje stanjem u Reactu često može učiniti stvari kompliciranijima nego što trebaju biti. Redux zahtijeva puno boilerplatea. Context API uzrokuje probleme s ponovnim renderiranjem. MobX oduzima puno vremena za učenje. Valtio radi stvari drugačije: koristi direktne mutacije s automatskom reaktivnošću.

Ovaj vodič vam govori sve što trebate znati za početak korištenja Valtia u stvarnim aplikacijama.

Što je Valtio?

Daishi Kato, koji je također napravio Zustand i Jotai, napravio je Valtio, biblioteku za upravljanje stanjem temeljenu na proxyjima od samo 3KB. Automatski prati promjene stanja koristeći JavaScript Proxy objekte.

Glavna ideja je jednostavna:

import { proxy, useSnapshot } from 'valtio';

// Učini stanje reaktivnim
const state = proxy({ count: 0, user: null });

// Mijenjaj direktno—bez akcija, bez dispatcha
state.count++;
state.user = { name: 'John' };

Bez reducera. Bez action creatora. Bez dispatch funkcija. Mijenjate stanje direktno, a Valtio se brine za sve ostalo.

Instalacija

npm install valtio
# ili
yarn add valtio

Dolazi s TypeScript tipovima. Radi s Reactom 16.8 i novijim, Next.js-om i React Nativeom.

Dva osnovna API-ja: proxy i useSnapshot

Postoje dvije glavne stvari koje Valtio radi. Ako naučite ove, moći ćete riješiti 90% situacija.

proxy – Napravi reaktivno stanje

Funkcija proxy omata vaš state objekt:

import { proxy } from 'valtio';

interface AppState {
  user: User | null;
  theme: 'light' | 'dark';
  notifications: Notification[];
}

export const appState = proxy<AppState>({
  user: null,
  theme: 'light',
  notifications: [],
});

Korištenje TypeScript interfejsa omogućuje autocomplete i pronalazak grešaka prije pokretanja koda.

useSnapshot – Čitaj stanje u komponentama

useSnapshot omogućuje komponentama pristup stanju. Ovaj hook čini da komponenta sluša promjene:

import { useSnapshot } from 'valtio';

import { appState } from './store';

function Header() {
  const snap = useSnapshot(appState);

  return (
    <header>
      <span>Bok, {snap.user?.name}</span>
      <span>{snap.notifications.length} novih obavijesti</span>
    </header>
  );
}

Snapshot ne možete mijenjati. Promjene se događaju na proxyju, ne na snapshotu.

Mijenjanje stanja

Mijenjajte proxy objekt direktno s bilo kojeg mjesta:

// U event handlerima
function handleLogin(user: User) {
  appState.user = user;
}

// U async funkcijama
async function fetchNotifications() {
  const data = await api.getNotifications();
  appState.notifications = data;
}

// Toggle vrijednosti
function toggleTheme() {
  appState.theme = appState.theme === 'light' ? 'dark' : 'light';
}

Struktura projekta

Za održavanje čiste kodne baze, odvojite storeove od akcija:

src/valtio/
├── users/
│   ├── users.store.ts      # Definicija stanja
│   └── users.actions.ts    # Funkcije za mutacije
├── auth/
│   ├── auth.store.ts
│   └── auth.actions.ts
└── global/
    ├── global.store.ts
    └── global.actions.ts

Ovaj pattern dobro funkcionira s većim projektima i drži povezani kod zajedno.

Potpuni primjer: Users Store

Ovo je store pattern spreman za produkciju:

// src/valtio/users/users.store.ts
import { proxy, useSnapshot } from 'valtio';

interface UsersStore {
  users: User[];
  selectedUser?: User;
  isLoading: boolean;
  error?: string;
  modalOpen: boolean;
}

export const usersStore = proxy<UsersStore>({
  users: [],
  selectedUser: undefined,
  isLoading: false,
  error: undefined,
  modalOpen: false,
});

export const useUsersStore = () => useSnapshot(usersStore);
// src/valtio/users/users.actions.ts
import { UsersService } from '@/services';

import { usersStore } from './users.store';

export async function fetchUsers() {
  usersStore.isLoading = true;
  usersStore.error = undefined;

  try {
    usersStore.users = await UsersService.getAll();
  } catch (e) {
    usersStore.error = 'Nije moguće učitati korisnike';
  } finally {
    usersStore.isLoading = false;
  }
}

export function selectUser(user: User) {
  usersStore.selectedUser = user;
}

export function toggleModal() {
  usersStore.modalOpen = !usersStore.modalOpen;
}

export function clearSelection() {
  usersStore.selectedUser = undefined;
}

Kako koristiti u komponentama:

import { useEffect } from 'react';

import { fetchUsers, selectUser, toggleModal } from '@/valtio/users/users.actions';
import { useUsersStore } from '@/valtio/users/users.store';

function UsersPage() {
  const { users, isLoading, error, modalOpen } = useUsersStore();

  useEffect(() => {
    fetchUsers();
  }, []);

  if (isLoading) return <Loading />;
  if (error) return <Error message={error} />;

  return (
    <div>
      <button onClick={toggleModal}>Dodaj korisnika</button>
      {users.map(user => (
        <UserCard key={user.id} user={user} onClick={() => selectUser(user)} />
      ))}
      {modalOpen && <UserModal />}
    </div>
  );
}

Vidite kako se akcije pozivaju direktno bez korištenja hookova ili dispatcha? Ne trebate useSnapshot za komponente koje samo pokreću akcije.

Globalni Store: Toast obavijesti

Globalni store upravlja problemima koji utječu na cijelu aplikaciju:

// src/valtio/global/global.store.ts
import { proxy, useSnapshot } from 'valtio';

interface Toast {
  id: string;
  type: 'success' | 'error' | 'info';
  message: string;
}

interface GlobalStore {
  toasts: Toast[];
}

export const globalStore = proxy<GlobalStore>({
  toasts: [],
});

export const useGlobalStore = () => useSnapshot(globalStore);
// src/valtio/global/global.actions.ts
import { globalStore } from './global.store';

export function showToast(type: Toast['type'], message: string) {
  const id = crypto.randomUUID();
  globalStore.toasts.push({ id, type, message });

  // Automatski ukloni nakon 3 sekunde
  setTimeout(() => {
    const index = globalStore.toasts.findIndex(t => t.id === id);
    if (index > -1) globalStore.toasts.splice(index, 1);
  }, 3000);
}

export function dismissToast(id: string) {
  const index = globalStore.toasts.findIndex(t => t.id === id);
  if (index > -1) globalStore.toasts.splice(index, 1);
}

Ovo možete koristiti bilo gdje u aplikaciji:

import { showToast } from '@/valtio/global/global.actions';

// Nakon uspješnog spremanja
showToast('success', 'Promjene spremljene!');

// Nakon greške
showToast('error', 'Nešto je pošlo po zlu');

Ovaj pattern dobro funkcionira s Material-UI pri izradi UI komponenti.

Valtio vs Redux: Usporedba

Isti brojač u Redux Toolkitu:

// Redux: 25+ linija
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => {
      state.value++;
    },
    decrement: state => {
      state.value--;
    },
  },
});

const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});

// Komponenta treba useSelector + useDispatch
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
dispatch(increment());

Isti brojač u Valtiu:

// Valtio: 8 linija
const counter = proxy({ value: 0 });
const increment = () => counter.value++;
const decrement = () => counter.value--;

// Komponenta
const snap = useSnapshot(counter);
// snap.value, increment(), decrement()

Isti rezultat, manje koda.

Spremanje stanja u LocalStorage

import { proxy, subscribe } from 'valtio';

const STORAGE_KEY = 'app_settings';

// Učitaj iz storagea
const saved = localStorage.getItem(STORAGE_KEY);
const initial = saved ? JSON.parse(saved) : { theme: 'light', language: 'en' };

export const settingsStore = proxy(initial);

// Automatski spremi kad se dogode promjene
subscribe(settingsStore, () => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(settingsStore));
});

Ovaj pattern sprema korisničke preferencije između sesija za aplikacije koje podržavaju više jezika.

Korištenje Valtia s Next.js

Valtio radi s Next.js, ali SSR zahtijeva malo rada:

// app/users/page.tsx (Server Component)
import { UsersClient } from './UsersClient';

export default async function UsersPage() {
  const users = await fetchUsers(); // Server fetch
  return <UsersClient initialUsers={users} />;
}
// app/users/UsersClient.tsx
'use client'

import { useEffect } from 'react'
import { usersStore, useUsersStore } from '@/valtio/users/users.store'

export function UsersClient({ initialUsers }) {
  const { users } = useUsersStore()

  useEffect(() => {
    usersStore.users = initialUsers // Hidriraj pri mountanju
  }, [initialUsers])

  return (
    // Vaš JSX ovdje
  )
}

Valtio i next-intl dobro rade zajedno za Next.js prijevode.

Česte greške

Mijenjanje snapshota

// Krivo: snapshot je read-only
const snap = useSnapshot(store);
snap.value = 'new'; // Greška!

// Ispravno: mijenjaj proxy
store.value = 'new';

Kreiranje proxyja unutar komponente

// Krivo—kreira novi proxy za svaki render
function Component() {
  const state = proxy({ count: 0 }) // Nemoj!
}

// Ispravno—definiraj izvan
const state = proxy({ count: 0 })

function Component() {
  const snap = useSnapshot(state)
}

Pristupanje proxyju direktno u JSX-u

// Krivo—neće se rerenderirati
return <div>{store.name}</div>

// Ispravno—koristi snapshot
const snap = useSnapshot(store)
return <div>{snap.name}</div>

Best practice

Pravila imenovanja

PatternStavkaPrimjer
Store{feature}StoreusersStore
Hookuse{Feature}StoreuseUsersStore
AkcijeglagolifetchUsers, toggleModal

Struktura storea

interface FeatureStore {
  // Podaci
  items: Item[];
  selected?: Item;

  // Loading/Error
  isLoading: boolean;
  error?: string;

  // UI stanje
  modalOpen: boolean;
}

Struktura datoteka

feature/
├── feature.store.ts    # Samo stanje
└── feature.actions.ts  # Sve mutacije

Kombiniranje Valtia sa stranicama baziranim na Markdownu je najlakši način za održavanje jednostavnosti za content stranice.

Authentication Store

Većina aplikacija ima neki oblik autentifikacije. Evo patterna koji možete ponovno koristiti:

// src/valtio/auth/auth.store.ts
import { proxy, useSnapshot } from 'valtio';

interface AuthStore {
  user: User | null;
  token: string | null;
  isLoading: boolean;
}

export const authStore = proxy<AuthStore>({
  user: null,
  token: localStorage.getItem('token'),
  isLoading: true,
});

export const useAuthStore = () => useSnapshot(authStore);
// src/valtio/auth/auth.actions.ts
import { AuthService } from '@/services';

import { authStore } from './auth.store';

export async function login(email: string, password: string) {
  authStore.isLoading = true;

  try {
    const { user, token } = await AuthService.login(email, password);
    authStore.user = user;
    authStore.token = token;
    localStorage.setItem('token', token);
    return true;
  } catch {
    return false;
  } finally {
    authStore.isLoading = false;
  }
}

export function logout() {
  authStore.user = null;
  authStore.token = null;
  localStorage.removeItem('token');
}

export async function checkAuth() {
  if (!authStore.token) {
    authStore.isLoading = false;
    return;
  }

  try {
    authStore.user = await AuthService.me();
  } catch {
    logout();
  } finally {
    authStore.isLoading = false;
  }
}

Za vraćanje sesija, pozovite checkAuth() kada se aplikacija pokrene.

Pretplata izvan Reacta

Valtio vam omogućuje pretplatu na stanje izvan komponenti. Dobro za logiranje, sinkronizaciju ili analitiku:

import { subscribe } from 'valtio';

// Prati sve promjene košarice
subscribe(cartStore, () => {
  analytics.track('cart_updated', {
    items: cartStore.items.length,
    total: cartStore.total,
  });
});

Koristite subscribeKey za informacije o određenim svojstvima:

import { subscribeKey } from 'valtio/utils';

subscribeKey(authStore, 'user', user => {
  if (user) {
    analytics.identify(user.id);
  }
});

Izračunate vrijednosti s derive

Koristite derive za dobivanje vrijednosti iz stanja:

import { proxy } from 'valtio';
import { derive } from 'valtio/utils';

const cartStore = proxy({
  items: [],
  taxRate: 0.08,
});

const cartDerived = derive({
  subtotal: get => get(cartStore).items.reduce((sum, item) => sum + item.price * item.quantity, 0),
  tax: get => get(cartDerived).subtotal * get(cartStore).taxRate,
  total: get => get(cartDerived).subtotal + get(cartDerived).tax,
});

Kada se ovisnosti promijene, izvedene vrijednosti se automatski ažuriraju.

Testiranje storeova

Testiranje Valtia je jednostavno jer su storeovi samo obični objekti:

import { clearSelection, selectUser } from './users.actions';
import { usersStore } from './users.store';

describe('UsersStore', () => {
  beforeEach(() => {
    // Resetiraj stanje prije svakog testa
    usersStore.users = [];
    usersStore.selectedUser = undefined;
  });

  it('odabire korisnika', () => {
    const user = { id: '1', name: 'John' };
    selectUser(user);
    expect(usersStore.selectedUser).toEqual(user);
  });

  it('briše odabir', () => {
    usersStore.selectedUser = { id: '1', name: 'John' };
    clearSelection();
    expect(usersStore.selectedUser).toBeUndefined();
  });
});

Kao i uvijek, koristite Jest za lažiranje API poziva.

Savjeti za performanse

Razbijte velike storeove u manje koji su specifični za određena područja:

// Dobro: Fokusirani storeovi
const usersStore = proxy({ users: [], selected: null })
const productsStore = proxy({ products: [], categories: [] })
const cartStore = proxy({ items: [], total: 0 })

// Izbjegavajte: Jedan ogroman store
const appStore = proxy({ users: [], products: [], cart: [], ... })

Za ograničavanje rerenderiranja, destrukturirajte snapshotove:

// Komponenta se rerenderira samo kada se users promijene
const { users } = useSnapshot(usersStore);

// vs. rerenderiranje na bilo koju promjenu storea
const snap = useSnapshot(usersStore);

Kada koristiti Valtio

Dobar izbor:

  • Male do srednje aplikacije
  • Timovi koji ne žele puno boilerplatea
  • Projekti gdje Redux djeluje pretjerano
  • Brzo prototipiranje

Razmotrite alternative ako:

  • Puno koristite Redux DevTools
  • Vaš tim već dobro poznaje Redux
  • Trebate middleware ekosustav

Spremni za razgovor?

Pošaljite kratki uvod kako bismo dogovorili uvodni poziv. Poziv se fokusira na vaše izazove i ciljeve te ocrtava prve korake prema pravom digitalnom rješenju.

Kako Valtio pojednostavljuje React state management | Workspace