React Native -tilanhallinta 2026: Zustand, Jotai, TanStack Query ja MMKV käytännössä

Kattava opas React Native -sovellusten tilanhallintaan vuonna 2026. Vertaile Zustand, Jotai, TanStack Query ja Redux Toolkit -kirjastoja käytännön esimerkkien avulla ja löydä oikea tilanhallintastrategia projektiisi.

Johdanto: tilanhallinta React Nativessa vuonna 2026

Käsi ylös, kuka on kyllästynyt kirjoittamaan Redux-boilerplatea? Jos olet React Native -kehittäjä, tiedät mistä puhun. Tilanhallinta on aina ollut yksi niistä aiheista, jotka herättävät eniten keskustelua — ja rehellisesti sanottuna, myös eniten turhautumista.

Mutta vuonna 2026 tilanne on muuttunut aika paljon. Redux ei ole enää automaattinen valinta jokaiseen projektiin, ja kevyemmät kirjastot kuten Zustand ja Jotai ovat vallanneet kehittäjien sydämet. Samaan aikaan TanStack Query on mullistanut palvelintilan hallinnan, ja React Compiler tekee manuaalisesta memoisaatiosta lähes tarpeetonta.

Tässä oppaassa käymme läpi React Native -sovellusten tilanhallinnan kokonaisvaltaisesti. Tarkastelemme neljää keskeistä lähestymistapaa — Zustand, Jotai, TanStack Query ja Redux Toolkit — käytännön esimerkkien, suorituskykyvertailujen ja MMKV-tallennuksen kautta. Olipa kyseessä uusi projekti tai vanhan sovelluksen modernisointi, tämä artikkeli auttaa sinua valitsemaan oikean strategian.

Tilanhallinnan nykytila: mitä on muuttunut?

Vielä muutama vuosi sitten tilanhallinta React Nativessa tarkoitti käytännössä Reduxia. Jokainen projekti alkoi npm install redux react-redux -komennolla, ja kehittäjät kirjoittivat sivukaupalla reducereita, action creatoreita ja middleware-konfiguraatioita. Se toimi, mutta boilerplate-koodin määrä oli valtava.

Nyt tilanne on aivan erilainen. Npm-latausten ja yhteisön kyselyiden perusteella suuntaus on selvä:

  • Zustand on kasvanut yli 30 % vuodessa ja esiintyy noin 40 prosentissa uusista projekteista
  • TanStack Query hoitaa noin 80 % palvelintilan hallinnasta moderneissa sovelluksissa
  • Jotai on noussut erityisesti TypeScript-painotteisissa ja suorituskykykriittisissä sovelluksissa
  • Redux Toolkit säilyttää asemansa suurissa enterprise-sovelluksissa, mutta uusissa projekteissa sen käyttö on vähentynyt selvästi

Merkittävin paradigman muutos on tilan lajittelun ymmärtäminen. Sen sijaan, että kaikki tila tungetaan yhteen globaaliin storeen, moderni lähestymistapa jakaa tilan kolmeen kategoriaan:

  1. Paikallinen tila — komponenttikohtainen tila (useState, useReducer)
  2. Asiakastila (client state) — sovelluksen globaali tila, jota ei haeta palvelimelta (Zustand, Jotai, Redux)
  3. Palvelintila (server state) — palvelimelta haettu ja välimuistissa oleva data (TanStack Query)

Tämä jako on ratkaiseva. Jokainen kategoria vaatii erilaisia työkaluja ja strategioita — ja juuri siksi yhtä "oikeaa" ratkaisua ei ole. Tutustutaan niihin yksi kerrallaan.

Zustand: yksinkertainen ja tehokas tilanhallinta

Zustand (saksaa, tarkoittaa "tilaa") on noin 3 kilotavun kokoinen tilanhallintakirjasto, joka tarjoaa hooks-pohjaisen API:n ilman Providereita, boilerplatea tai monimutkaista konfiguraatiota. Se on noussut yhdeksi suosituimmista valinnoista React Native -kehityksessä — ja hyvästä syystä.

Oma kokemukseni on, että Zustand on se kirjasto, joka saa uudetkin tiimin jäsenet tuottaviksi parissa tunnissa. Se on juuri niin yksinkertainen.

Asennus ja peruskäyttö

Zustandin asennus on suoraviivaista:

npm install zustand
# tai
yarn add zustand

Peruskaupan (store) luominen on erittäin yksinkertaista:

// stores/useAuthStore.ts
import { create } from 'zustand';

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  token: string | null;
  login: (user: User, token: string) => void;
  logout: () => void;
  updateProfile: (updates: Partial<User>) => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  isAuthenticated: false,
  token: null,

  login: (user, token) =>
    set({
      user,
      isAuthenticated: true,
      token,
    }),

  logout: () =>
    set({
      user: null,
      isAuthenticated: false,
      token: null,
    }),

  updateProfile: (updates) =>
    set((state) => ({
      user: state.user ? { ...state.user, ...updates } : null,
    })),
}));

Ja kaupan käyttäminen komponentissa? Yhtä helppoa:

// components/ProfileScreen.tsx
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useAuthStore } from '../stores/useAuthStore';

export function ProfileScreen() {
  const user = useAuthStore((state) => state.user);
  const logout = useAuthStore((state) => state.logout);

  if (!user) return null;

  return (
    <View style={styles.container}>
      <Text style={styles.name}>{user.name}</Text>
      <Text style={styles.email}>{user.email}</Text>
      <TouchableOpacity style={styles.button} onPress={logout}>
        <Text style={styles.buttonText}>Kirjaudu ulos</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20, justifyContent: 'center' },
  name: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  email: { fontSize: 16, color: '#666', marginBottom: 24 },
  button: { backgroundColor: '#FF3B30', padding: 16, borderRadius: 8, alignItems: 'center' },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});

Selektorit ja uudelleenrenderöinnin optimointi

Yksi Zustandin parhaista puolista on selektoripohjainen tilan valinta. Kun käytät selektoria, komponentti renderöityy uudelleen vain silloin, kun valittu tilan osa muuttuu. Tämä tekee valtavan eron suorituskyvyssä.

// Huono: koko store aiheuttaa uudelleenrenderöinnin
const state = useAuthStore();

// Hyvä: vain user-kentän muutokset aiheuttavat uudelleenrenderöinnin
const user = useAuthStore((state) => state.user);

// Useita arvoja shallow-vertailulla
import { useShallow } from 'zustand/react/shallow';

const { user, isAuthenticated } = useAuthStore(
  useShallow((state) => ({
    user: state.user,
    isAuthenticated: state.isAuthenticated,
  }))
);

Benchmark-testeissä monimutkaisessa lomakesovelluksessa (yli 30 yhteenliitettyä kenttää) Zustand laskettuine selektoreineen pudotti keskimääräisen päivitysajan 220 millisekunnista 85 millisekuntiin. Se on yli 60 prosentin parannus — ei mitään pientä juttua.

Middleware: persist, devtools ja immer

Zustand tarjoaa laajan middleware-ekosysteemin. Tässä esimerkki, jossa yhdistetään useita middlewareja:

// stores/useSettingsStore.ts
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

interface SettingsState {
  theme: 'light' | 'dark' | 'system';
  language: string;
  notifications: {
    push: boolean;
    email: boolean;
    sms: boolean;
  };
  setTheme: (theme: 'light' | 'dark' | 'system') => void;
  setLanguage: (language: string) => void;
  toggleNotification: (type: keyof SettingsState['notifications']) => void;
}

export const useSettingsStore = create<SettingsState>()(
  devtools(
    persist(
      immer((set) => ({
        theme: 'system',
        language: 'fi',
        notifications: {
          push: true,
          email: true,
          sms: false,
        },

        setTheme: (theme) =>
          set((state) => {
            state.theme = theme;
          }),

        setLanguage: (language) =>
          set((state) => {
            state.language = language;
          }),

        toggleNotification: (type) =>
          set((state) => {
            state.notifications[type] = !state.notifications[type];
          }),
      })),
      {
        name: 'settings-storage',
      }
    )
  )
);

Immer-middleware on mielestäni yksi Zustandin parhaista ominaisuuksista. Se mahdollistaa muuttumattoman tilan muokkaamisen "mutable"-tyylisellä syntaksilla, mikä tekee monimutkaisista tilapäivityksistä paljon luettavampia.

Jotai: atomipohjainen tilanhallinta

Jotai (japania, tarkoittaa "tilaa") lähestyy tilanhallintaa alhaalta ylöspäin atomien avulla — pieniä, itsenäisiä tilan yksiköitä. Noin 4 kilotavun paketillaan ja hienojakoisella renderöinnin kontrollillaan Jotai on erinomainen valinta suorituskykykriittisiin React Native -sovelluksiin.

Jos Zustand on kuin yksinkertainen kauppa, Jotai on kuin rakennuspalikoiden setti. Kumpikin toimii, mutta eri tavalla.

Atomien perusteet

// atoms/cartAtoms.ts
import { atom } from 'jotai';

// Primitiivinen atomi
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

export const cartItemsAtom = atom<CartItem[]>([]);

// Johdettu (derived) atomi — lasketaan automaattisesti
export const cartTotalAtom = atom((get) => {
  const items = get(cartItemsAtom);
  return items.reduce((total, item) => total + item.price * item.quantity, 0);
});

export const cartItemCountAtom = atom((get) => {
  const items = get(cartItemsAtom);
  return items.reduce((count, item) => count + item.quantity, 0);
});

// Kirjoitusatomi — toiminto, joka muokkaa tilaa
export const addToCartAtom = atom(
  null,
  (get, set, newItem: Omit<CartItem, 'quantity'>) => {
    const items = get(cartItemsAtom);
    const existingItem = items.find((item) => item.id === newItem.id);

    if (existingItem) {
      set(
        cartItemsAtom,
        items.map((item) =>
          item.id === newItem.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        )
      );
    } else {
      set(cartItemsAtom, [...items, { ...newItem, quantity: 1 }]);
    }
  }
);

export const removeFromCartAtom = atom(
  null,
  (get, set, itemId: string) => {
    const items = get(cartItemsAtom);
    set(
      cartItemsAtom,
      items.filter((item) => item.id !== itemId)
    );
  }
);

Atomien käyttö komponenteissa

// components/CartBadge.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useAtomValue } from 'jotai';
import { cartItemCountAtom } from '../atoms/cartAtoms';

export function CartBadge() {
  // Renderöityy uudelleen VAIN kun tuotteiden määrä muuttuu
  const itemCount = useAtomValue(cartItemCountAtom);

  if (itemCount === 0) return null;

  return (
    <View style={styles.badge}>
      <Text style={styles.badgeText}>{itemCount}</Text>
    </View>
  );
}

// components/CartTotal.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useAtomValue } from 'jotai';
import { cartTotalAtom } from '../atoms/cartAtoms';

export function CartTotal() {
  // Renderöityy uudelleen VAIN kun kokonaissumma muuttuu
  const total = useAtomValue(cartTotalAtom);

  return (
    <View style={styles.totalContainer}>
      <Text style={styles.totalLabel}>Yhteensä:</Text>
      <Text style={styles.totalAmount}>{total.toFixed(2)} €</Text>
    </View>
  );
}

Jotain erikoisominaisuudet

Jotain atomipohjainen malli erottuu erityisesti kolmessa tilanteessa:

  • Hienojakoinen uudelleenrenderöinti: Kun tilan osa päivittyy, vain kyseistä atomia käyttävät komponentit renderöityvät uudelleen — ei koko komponenttipuuta
  • Riippuvuusverkko: Johdetut atomit muodostavat automaattisen riippuvuusverkon, jolloin laskelmat päivittyvät vain kun on tarpeen
  • Koodin jakaminen: Atomit voidaan määritellä missä tahansa ja tuoda tarvittaessa — koodin jako tuntuu luonnolliselta

Erityisesti useSetAtom()-hookki ansaitsee maininnan. Komponentit, jotka vain kirjoittavat tilaa mutta eivät lue sitä, eivät renderöidy uudelleen lainkaan tilan muuttuessa. Tämä on pieni mutta tärkeä optimointi.

TanStack Query: palvelintilan hallinta

TanStack Query (entinen React Query) on rehellisesti sanottuna muuttanut täysin tavan, jolla käsittelemme palvelimelta haettua dataa. Sen sijaan, että tallentaisimme API-vastaukset globaaliin storeen ja hallitsisimme lataus-, virhe- ja välimuistitiloja käsin, TanStack Query hoitaa kaiken tämän automaattisesti.

Jos et vielä käytä sitä — kannattaa aloittaa nyt.

Asennus ja konfigurointi

npm install @tanstack/react-query
// App.tsx
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minuuttia
      gcTime: 10 * 60 * 1000,   // 10 minuuttia (aiemmin cacheTime)
      retry: 2,
      refetchOnWindowFocus: false, // React Nativessa ei yleensä tarvita
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* Sovelluksen sisältö */}
    </QueryClientProvider>
  );
}

Datan hakeminen useQuery-hookilla

// hooks/useProducts.ts
import { useQuery } from '@tanstack/react-query';

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
  imageUrl: string;
}

async function fetchProducts(category?: string): Promise<Product[]> {
  const url = category
    ? `https://api.example.com/products?category=${category}`
    : 'https://api.example.com/products';

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error('Tuotteiden hakeminen epäonnistui');
  }

  return response.json();
}

export function useProducts(category?: string) {
  return useQuery({
    queryKey: ['products', category],
    queryFn: () => fetchProducts(category),
    staleTime: 10 * 60 * 1000, // 10 minuuttia
  });
}
// components/ProductList.tsx
import React from 'react';
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';
import { useProducts } from '../hooks/useProducts';

export function ProductList({ category }: { category?: string }) {
  const { data: products, isLoading, error, refetch, isRefetching } = useProducts(category);

  if (isLoading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#007AFF" />
        <Text style={styles.loadingText}>Ladataan tuotteita...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>Virhe: {error.message}</Text>
      </View>
    );
  }

  return (
    <FlatList
      data={products}
      keyExtractor={(item) => item.id}
      refreshing={isRefetching}
      onRefresh={refetch}
      renderItem={({ item }) => (
        <View style={styles.productCard}>
          <Text style={styles.productName}>{item.name}</Text>
          <Text style={styles.productPrice}>{item.price.toFixed(2)} €</Text>
        </View>
      )}
    />
  );
}

Mutaatiot useMutation-hookilla

TanStack Query hoitaa myös datan muokkausoperaatiot todella siististi:

// hooks/useCreateOrder.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';

interface OrderData {
  items: Array<{ productId: string; quantity: number }>;
  shippingAddress: string;
  paymentMethod: string;
}

async function createOrder(orderData: OrderData) {
  const response = await fetch('https://api.example.com/orders', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(orderData),
  });

  if (!response.ok) {
    throw new Error('Tilauksen luominen epäonnistui');
  }

  return response.json();
}

export function useCreateOrder() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: createOrder,
    onSuccess: () => {
      // Päivitä tilaushistoria automaattisesti
      queryClient.invalidateQueries({ queryKey: ['orders'] });
      // Tyhjennä ostoskorin välimuisti
      queryClient.invalidateQueries({ queryKey: ['cart'] });
    },
    onError: (error) => {
      console.error('Tilausvirhe:', error.message);
    },
  });
}

Optimistinen päivitys

Yksi TanStack Queryn tehokkaimmista ominaisuuksista on optimistinen päivitys. Idea on yksinkertainen: päivitä käyttöliittymä heti, ennen kuin palvelin ehtii vahvistaa. Käyttäjälle tämä näkyy välittömänä reagointina.

// hooks/useToggleFavorite.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function useToggleFavorite() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (productId: string) => {
      const response = await fetch(
        `https://api.example.com/favorites/${productId}`,
        { method: 'POST' }
      );
      return response.json();
    },

    // Optimistinen päivitys
    onMutate: async (productId) => {
      // Peruuta käynnissä olevat kyselyt
      await queryClient.cancelQueries({ queryKey: ['favorites'] });

      // Tallenna edellinen tila
      const previousFavorites = queryClient.getQueryData(['favorites']);

      // Päivitä käyttöliittymä optimistisesti
      queryClient.setQueryData(['favorites'], (old: string[] | undefined) => {
        if (!old) return [productId];
        return old.includes(productId)
          ? old.filter((id) => id !== productId)
          : [...old, productId];
      });

      return { previousFavorites };
    },

    // Palauta edellinen tila virheen sattuessa
    onError: (_error, _productId, context) => {
      queryClient.setQueryData(['favorites'], context?.previousFavorites);
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['favorites'] });
    },
  });
}

Tilan pysyvyys: MMKV ja React Native

Mobiilisovelluksissa tilan pysyvyys on kriittinen ominaisuus. Käyttäjäasetukset, kirjautumistiedot ja välimuistissa oleva data täytyy säilyttää sovellusten uudelleenkäynnistysten välillä.

Perinteinen ratkaisu on ollut AsyncStorage. Mutta vuonna 2026? MMKV on selkeä ykkönen.

Miksi MMKV AsyncStoragen sijaan?

MMKV on Tencentin kehittämä ja Marc Rousavyn ylläpitämä erittäin nopea avain-arvo-tallennusratkaisu. Benchmark-testit puhuvat puolestaan: MMKV on 30–100 kertaa nopeampi kuin AsyncStorage todellisissa käyttötilanteissa. Ero johtuu siitä, että MMKV käyttää muistiin kartoitettuja tiedostoja (memory-mapped files), kun taas AsyncStorage perustuu SQLite-tietokantaan.

Ja se nopeusero tuntuu oikeasti — erityisesti sovelluksen käynnistyksessä.

npm install react-native-mmkv

Zustand + MMKV: nopea pysyvä tallennus

Zustandin persist-middlewaren yhdistäminen MMKV:hen on yksi suosituimmista ja tehokkaimmista yhdistelmistä:

// storage/mmkvStorage.ts
import { MMKV } from 'react-native-mmkv';
import { StateStorage } from 'zustand/middleware';

const storage = new MMKV({
  id: 'app-storage',
  encryptionKey: 'your-encryption-key', // Valinnainen salausavain
});

export const mmkvStorage: StateStorage = {
  setItem: (name, value) => {
    storage.set(name, value);
  },
  getItem: (name) => {
    const value = storage.getString(name);
    return value ?? null;
  },
  removeItem: (name) => {
    storage.delete(name);
  },
};
// stores/useAuthStore.ts — pysyvällä tallennuksella
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { mmkvStorage } from '../storage/mmkvStorage';

interface AuthState {
  user: { id: string; name: string; email: string } | null;
  token: string | null;
  isAuthenticated: boolean;
  login: (user: AuthState['user'], token: string) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      login: (user, token) =>
        set({ user, token, isAuthenticated: true }),

      logout: () =>
        set({ user: null, token: null, isAuthenticated: false }),
    }),
    {
      name: 'auth-storage',
      storage: createJSONStorage(() => mmkvStorage),
      // Valitse mitkä kentät tallennetaan
      partialize: (state) => ({
        user: state.user,
        token: state.token,
        isAuthenticated: state.isAuthenticated,
      }),
    }
  )
);

Jotai + MMKV: atomien pysyvyys

Jotai tarjoaa atomWithStorage-funktion, joka voidaan yhdistää MMKV:hen yhtä helposti:

// atoms/persistedAtoms.ts
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import { MMKV } from 'react-native-mmkv';

const storage = new MMKV({ id: 'jotai-storage' });

const mmkvJotaiStorage = createJSONStorage<any>(() => ({
  getItem: (key: string) => {
    const value = storage.getString(key);
    return value ? JSON.parse(value) : null;
  },
  setItem: (key: string, value: any) => {
    storage.set(key, JSON.stringify(value));
  },
  removeItem: (key: string) => {
    storage.delete(key);
  },
}));

// Pysyvä teema-atomi
export const themeAtom = atomWithStorage('theme', 'light', mmkvJotaiStorage);

// Pysyvä kielivalinta
export const languageAtom = atomWithStorage('language', 'fi', mmkvJotaiStorage);

// Pysyvä onboarding-tila
export const hasCompletedOnboardingAtom = atomWithStorage(
  'onboarding-completed',
  false,
  mmkvJotaiStorage
);

React Compiler ja automaattinen memoisaatio

Tässä on asia, joka on muuttanut pelikenttää vuonna 2026: React Compiler. Se analysoi komponentteja staattisesti käännösaikana ja lisää automaattisesti useMemo-, useCallback- ja React.memo-käärintöjä sinne, missä ne ovat tarpeen.

Käytännössä tämä tarkoittaa:

  • 30–60 % vähemmän tarpeettomia uudelleenrenderöintejä tyypillisessä sovelluksessa
  • 20–40 % nopeampi interaktioviive käyttöliittymässä
  • Kehittäjien ei tarvitse enää manuaalisesti kirjoittaa memoisaatiokoukuja

Mutta — ja tämä on tärkeä mutta — React Compiler ei tee tilanhallintakirjastoja tarpeettomiksi. Se optimoi renderöintiä, mutta ei hallitse tilaa itsessään. Zustandin selektorit, Jotain atomit ja TanStack Queryn välimuistitus ovat edelleen välttämättömiä. Compiler vain varmistaa, että renderöintipuolella kaikki toimii optimaalisesti ilman käsityötä.

Katsotaan konkreettinen esimerkki siitä, miten paljon puhtaampaa koodi on Compilerin kanssa:

// Ennen React Compileria — manuaalinen optimointi
import React, { useMemo, useCallback } from 'react';
import { useAuthStore } from '../stores/useAuthStore';

const UserGreeting = React.memo(function UserGreeting() {
  const user = useAuthStore((state) => state.user);

  const greeting = useMemo(() => {
    return `Tervetuloa, ${user?.name ?? 'vieras'}!`;
  }, [user?.name]);

  const handlePress = useCallback(() => {
    console.log('Tervehditty käyttäjää:', user?.name);
  }, [user?.name]);

  return (
    <TouchableOpacity onPress={handlePress}>
      <Text>{greeting}</Text>
    </TouchableOpacity>
  );
});

// React Compilerin kanssa — sama suorituskyky, vähemmän koodia
import { useAuthStore } from '../stores/useAuthStore';

function UserGreeting() {
  const user = useAuthStore((state) => state.user);

  const greeting = `Tervetuloa, ${user?.name ?? 'vieras'}!`;

  const handlePress = () => {
    console.log('Tervehditty käyttäjää:', user?.name);
  };

  return (
    <TouchableOpacity onPress={handlePress}>
      <Text>{greeting}</Text>
    </TouchableOpacity>
  );
}

Redux Toolkit: milloin se on edelleen oikea valinta?

Vaikka Zustand ja Jotai ovat vallanneet alaa, Redux Toolkitia ei kannata haudata. Se on edelleen paras valinta tietyissä tilanteissa — erityisesti enterprise-tason sovelluksissa, joissa on useita kehitystiimejä, monimutkainen tilalogikka ja tarve tiukalle rakenteelle.

Redux Toolkitin edut vuonna 2026

  • Rakenne ja ennustettavuus: Selkeä toiminta-reducer-malli pakottaa yhdenmukaisen koodirakenteen
  • DevTools: Redux DevTools on edelleen alan paras debuggaustyökalu tilanhallinnan osalta (tästä on vaikea kiistellä)
  • RTK Query: Reduxin oma vastaus TanStack Querylle, integroitu suoraan storeen
  • Middleware-ekosysteemi: Laaja valikoima sivuvaikutusten hallintaan
  • Yhteisö ja dokumentaatio: Suurin yhteisö ja kattavin dokumentaatio tilanhallintakirjastoista
// Redux Toolkit -esimerkki modernilla tavalla
import { createSlice, configureStore } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';

interface TodoItem {
  id: string;
  text: string;
  completed: boolean;
  createdAt: string;
}

const todosSlice = createSlice({
  name: 'todos',
  initialState: [] as TodoItem[],
  reducers: {
    addTodo: (state, action) => {
      state.push({
        id: Date.now().toString(),
        text: action.payload,
        completed: false,
        createdAt: new Date().toISOString(),
      });
    },
    toggleTodo: (state, action) => {
      const todo = state.find((t) => t.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    removeTodo: (state, action) => {
      return state.filter((t) => t.id !== action.payload);
    },
  },
});

const store = configureStore({
  reducer: {
    todos: todosSlice.reducer,
  },
});

type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const { addTodo, toggleTodo, removeTodo } = todosSlice.actions;
export default store;

Vertailu ja oikean valinnan tekeminen

Eli minkä kirjaston valitsisit? Se riippuu projektin vaatimuksista. Tässä suora vertailu, joka auttaa päätöksenteossa.

Pakettikoko

  • Zustand: ~3 kt (gzip)
  • Jotai: ~4 kt (gzip)
  • TanStack Query: ~13 kt (gzip)
  • Redux Toolkit: ~30 kt (gzip)

Oppimiskäyrä

  • Zustand: Matala — hooks-pohjainen API, tuttu paradigma
  • Jotai: Matala-keskitaso — atomikonsepti vaatii ajattelutavan muutosta
  • TanStack Query: Keskitaso — välimuistitus ja synkronointi ovat uusia konsepteja monille
  • Redux Toolkit: Keskitaso-korkea — reducers, middleware, normalisointi

Parhaat käyttötapaukset

  • Zustand: Pienet ja keskikokoiset sovellukset, nopea prototypointi, yksinkertainen globaali tila
  • Jotai: Suorituskykykriittiset sovellukset, monimutkaiset riippuvuudet, syvästi sisäkkäiset komponenttipuut
  • TanStack Query: API-painotteiset sovellukset, reaaliaikainen data, offline-first-sovellukset
  • Redux Toolkit: Suuret enterprise-sovellukset, monimutkainen bisneslogiikka, usean tiimin projektit

Suositeltu arkkitehtuuri vuodelle 2026

Nyt pääsemme siihen, mitä olen oppinut useissa projekteissa ja mitä yhteisön parhaat käytännöt suosittelevat. Tässä lähestymistapa, jota suosittelen React Native -sovelluksen tilanhallintaan vuonna 2026.

1. Aloita yksinkertaisesti

Käytä Reactin sisäänrakennettua tilanhallintaa (useState, useReducer, Context) paikalliseen tilaan. Älä ota tilanhallintakirjastoa käyttöön ennen kuin sinulla on selkeä tarve. Vakavasti — useState riittää yllättävän pitkälle.

2. Lisää TanStack Query palvelindatalle

Heti kun sovelluksesi hakee dataa palvelimelta, ota TanStack Query käyttöön. Se hoitaa välimuistituksen, uudelleenhaut, lataustilojen hallinnan ja optimistiset päivitykset — asioita, joita joutuisit muuten rakentamaan käsin. Ja se tekee ne paremmin kuin useimmat meistä tekisivät itse.

3. Valitse Zustand tai Jotai asiakastilalle

Kun sinulla on globaalia asiakastilaa, joka ei ole palvelimelta haettua dataa (esim. käyttöliittymäasetukset, teema, autentikaatio), valitse yksi näistä:

  • Zustand, jos haluat yksinkertaisen store-pohjaisen mallin, joka on helppo omaksua
  • Jotai, jos tarvitset hienojakoista kontrollia uudelleenrenderöintiin ja tilan riippuvuuksiin

4. Lisää MMKV pysyvyyteen

Yhdistä valitsemasi tilanhallintakirjasto MMKV:hen tilan pysyvyyttä varten. Tämä on erityisen tärkeää autentikaatiotilalle, käyttäjäasetuksille ja välimuistissa olevalle datalle.

Esimerkki: täydellinen arkkitehtuuri

// Sovelluksen tilanhallinta-arkkitehtuuri 2026
//
// 1. Paikallinen tila: useState, useReducer
//    - Lomakekenttien arvot
//    - Modaalien näkyvyys
//    - Animaatiotilat
//
// 2. Palvelintila: TanStack Query
//    - API-kyselyt (useQuery)
//    - Mutaatiot (useMutation)
//    - Optimistiset päivitykset
//    - Automaattinen välimuistitus
//
// 3. Globaali asiakastila: Zustand + MMKV
//    - Autentikaatio (useAuthStore)
//    - Asetukset (useSettingsStore)
//    - Käyttöliittymäpreferenssit (useUIStore)
//
// 4. React Compiler hoitaa memoisaation automaattisesti

// app/_layout.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Stack } from 'expo-router';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,
      gcTime: 10 * 60 * 1000,
    },
  },
});

export default function RootLayout() {
  return (
    <QueryClientProvider client={queryClient}>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="auth" options={{ headerShown: false }} />
      </Stack>
    </QueryClientProvider>
  );
}

Käytännön vinkit ja yleisimmät virheet

Tässä muutamia käytännön vinkkejä, jotka ovat syntyneet useissa projekteissa — osittain kantapään kautta.

Vältä yleisimmät sudenkuopat

  1. Älä laita kaikkea globaaliin storeen. Jos tila on vain yhden komponentin tai näkymän käytössä, useState riittää mainiosti. Globaali tila on tarkoitettu tilalle, jota jaetaan useiden komponenttien välillä.
  2. Älä sekoita palvelintilaa ja asiakastilaa. API:sta haettu data kuuluu TanStack Queryyn, ei Zustandiin tai Reduxiin. Tilan sekoittaminen johtaa väistämättä synkronointiongelmiin — luota minuun tässä.
  3. Käytä selektoreita. Sekä Zustandissa että Reduxissa selektorit ovat avain suorituskykyyn. Ilman niitä jokainen tilan muutos aiheuttaa tarpeettomia uudelleenrenderöintejä.
  4. Testaa tilanhallinta erikseen. Zustandin ja Jotain storet ovat puhtaita funktioita, jotka on helppo testata ilman komponentteja:
// __tests__/useAuthStore.test.ts
import { useAuthStore } from '../stores/useAuthStore';

describe('useAuthStore', () => {
  beforeEach(() => {
    // Nollaa tila ennen jokaista testiä
    useAuthStore.setState({
      user: null,
      token: null,
      isAuthenticated: false,
    });
  });

  it('kirjautuminen asettaa käyttäjän ja tokenin', () => {
    const testUser = { id: '1', name: 'Testi', email: '[email protected]' };
    useAuthStore.getState().login(testUser, 'test-token');

    const state = useAuthStore.getState();
    expect(state.user).toEqual(testUser);
    expect(state.token).toBe('test-token');
    expect(state.isAuthenticated).toBe(true);
  });

  it('uloskirjautuminen nollaa tilan', () => {
    const testUser = { id: '1', name: 'Testi', email: '[email protected]' };
    useAuthStore.getState().login(testUser, 'test-token');
    useAuthStore.getState().logout();

    const state = useAuthStore.getState();
    expect(state.user).toBeNull();
    expect(state.token).toBeNull();
    expect(state.isAuthenticated).toBe(false);
  });
});

Suorituskykyvinkit

  • Käytä useShallow Zustandissa kun valitset useita arvoja samasta storesta — tämä estää tarpeettomia uudelleenrenderöintejä objektien viitevertailun takia
  • Käytä useSetAtom Jotaissa komponenteissa, jotka vain kirjoittavat tilaa — tämä estää uudelleenrenderöinnin kokonaan
  • Aseta staleTime TanStack Queryssä oikein — liian lyhyt arvo aiheuttaa turhia verkkopyyntöjä, liian pitkä näyttää vanhentunutta dataa
  • Käytä MMKV:tä AsyncStoragen sijaan — 30–100-kertainen nopeusero näkyy erityisesti sovelluksen käynnistysajassa

Yhteenveto

React Native -tilanhallinnan kenttä vuonna 2026 on kypsempi ja monipuolisempi kuin koskaan. Reduxin yksinvaltakausi on ohi, ja kehittäjillä on käytössään erinomaisia työkaluja jokaiseen tarpeeseen.

Keskeinen viesti on yksinkertainen: jaa tila kategorioihin ja valitse oikea työkalu kuhunkin. TanStack Query palvelindatalle, Zustand tai Jotai globaalille asiakastilalle, useState paikalliselle tilalle ja MMKV pysyvyyteen. React Compiler hoitaa memoisaation automaattisesti, joten voit keskittyä siihen mikä on tärkeää — sovelluslogiikkaan.

Aloita yksinkertaisesti, lisää monimutkaisuutta vasta tarpeen mukaan ja muista: paras tilanhallintaratkaisu on se, jonka tiimisi ymmärtää ja osaa käyttää tehokkaasti. Ja vuonna 2026 hyviä vaihtoehtoja on enemmän kuin koskaan.

Tietoa Kirjoittajasta Editorial Team

Our team of expert writers and editors.