Kas tie nanostores ir kodėl turėtum apie juos žinoti
Jei esi dirbęs su React, Vue ar Svelte projektais, tikrai esi susidūręs su state management problema. Redux, MobX, Vuex, Pinia – sąrašas ilgas, o sprendimai dažnai atrodo pernelyg sudėtingi paprastoms užduotims. Čia ir ateina į pagalbą nanostores – minimalistinė biblioteka, kuri žada atomic state management be bereikalingų komplikacijų.
Nanostores sukūrė Andrey Sitnik, tas pats žmogus, kuris mums davė PostCSS ir Autoprefixer. Bibliotekos dydis – vos 334 baitai (taip, baitai, ne kilobaitai). Tai ne klaida – tai tikrai tokia maža. O svarbiausia, ji veikia su bet kokiu framework’u ar net be jų.
Pagrindinė idėja paprasta: vietoj vieno didžiulio store, kurį naudoja visa aplikacija, sukuri mažus, atomines stores, kurios atsakingos tik už vieną konkretų duomenų gabalą. Tai panašu į tai, kaip React Hooks pakeitė class components – mažiau boilerplate, daugiau lankstumumo.
Kaip tai veikia praktikoje
Pradėkime nuo paprasčiausio pavyzdžio. Tarkime, nori saugoti vartotojo vardą:
„`javascript
import { atom } from ‘nanostores’
export const userName = atom(‘Vardenis’)
„`
Viskas. Sukūrei store. Dabar gali jį naudoti bet kuriame komponente:
„`javascript
import { useStore } from ‘@nanostores/react’
import { userName } from ‘./stores’
function UserGreeting() {
const name = useStore(userName)
return
Sveiki, {name}!
}
„`
Norint pakeisti reikšmę, tiesiog:
„`javascript
userName.set(‘Jonas’)
„`
Palyginimui, su Redux tam pačiam rezultatui reikėtų sukurti action types, action creators, reducers, ir dar prisijungti prie store su connect arba useSelector. Su nanostores viskas telpa keliose eilutėse.
Computed stores ir reaktyvumas
Dalykai tampa įdomesni, kai pradedi naudoti computed stores. Tarkime, turi keletą store ir nori sukurti naują, kuris automatiškai perskaičiuoja reikšmę pagal kitus:
„`javascript
import { atom, computed } from ‘nanostores’
export const firstName = atom(‘Jonas’)
export const lastName = atom(‘Jonaitis’)
export const fullName = computed([firstName, lastName], (first, last) => {
return `${first} ${last}`
})
„`
Kai tik pasikeičia firstName arba lastName, fullName automatiškai atsinaujina. Tai reaktyvumas be jokių papildomų pastangų. Nereikia galvoti apie dependencies, memoization ar kitas komplikacijas.
Dar vienas praktiškas pavyzdys – skaičiuoklė su PVM:
„`javascript
export const priceWithoutVAT = atom(100)
export const vatRate = atom(0.21)
export const priceWithVAT = computed(
[priceWithoutVAT, vatRate],
(price, rate) => price * (1 + rate)
)
export const vatAmount = computed(
[priceWithoutVAT, vatRate],
(price, rate) => price * rate
)
„`
Keičiant bazinę kainą ar PVM tarifą, visos kitos reikšmės perskaičiuojamos automatiškai. Komponentai, kurie naudoja šias stores, taip pat automatiškai atsinaujina.
Maps – sudėtingesniems objektams
Atom stores puikiai tinka primityvams, bet realybėje dažnai dirbi su objektais. Tam yra maps:
„`javascript
import { map } from ‘nanostores’
export const userProfile = map({
name: ‘Jonas’,
email: ‘[email protected]’,
age: 28,
settings: {
theme: ‘dark’,
notifications: true
}
})
„`
Keisti galima tiek visą objektą, tiek atskirus laukus:
„`javascript
// Keisti vieną lauką
userProfile.setKey(‘age’, 29)
// Keisti kelis laukus
userProfile.set({
…userProfile.get(),
name: ‘Jonas Jonaitis’,
age: 30
})
// Nested objektams
userProfile.setKey(‘settings’, {
…userProfile.get().settings,
theme: ‘light’
})
„`
Svarbu suprasti, kad setKey sukelia re-render’ą tik tiems komponentams, kurie naudoja tą konkretų lauką. Tai vienas iš nanostores privalumų – optimizacija įmontuota iš karto.
Async stores ir duomenų gavimas
Realios aplikacijos retai apsiriboja statiniais duomenimis. Dažniausiai reikia gauti duomenis iš API. Nanostores turi elegantišką sprendimą ir tam:
„`javascript
import { atom, onMount } from ‘nanostores’
export const userData = atom(null)
export const isLoading = atom(false)
export const error = atom(null)
onMount(userData, () => {
isLoading.set(true)
fetch(‘https://api.example.com/user’)
.then(response => response.json())
.then(data => {
userData.set(data)
isLoading.set(false)
})
.catch(err => {
error.set(err.message)
isLoading.set(false)
})
})
„`
onMount callback’as vykdomas, kai bent vienas komponentas pradeda klausytis store. Tai reiškia, kad duomenys gaunami tik tada, kai jų tikrai reikia – ne anksčiau. Tai lazy loading principas, pritaikytas state management.
Dar geresnis variantas – sukurti reusable funkciją async duomenų gavimui:
„`javascript
export function createAsyncStore(fetchFn) {
const data = atom(null)
const loading = atom(false)
const error = atom(null)
const load = async () => {
loading.set(true)
error.set(null)
try {
const result = await fetchFn()
data.set(result)
} catch (err) {
error.set(err.message)
} finally {
loading.set(false)
}
}
onMount(data, () => {
load()
})
return { data, loading, error, reload: load }
}
// Naudojimas
export const posts = createAsyncStore(() =>
fetch(‘/api/posts’).then(r => r.json())
)
„`
Integracijos su framework’ais
Vienas didžiausių nanostores privalumų – ji veikia su bet kuo. Yra oficialūs adapteriai React, Preact, Vue, Svelte, Solid, Angular ir net vanilla JS.
React integracijos pavyzdys:
„`javascript
import { useStore } from ‘@nanostores/react’
function Counter() {
const count = useStore(counterStore)
return (
)
}
„`
Vue 3 su Composition API:
„`javascript
import { useStore } from ‘@nanostores/vue’
export default {
setup() {
const count = useStore(counterStore)
return { count }
}
}
„`
Svelte dar paprasčiau:
„`svelte
„`
Pastebėk, kaip Svelte naudoja $ sintaksę – tai veikia natūraliai su nanostores, nes biblioteka palaiko store contract, kurį Svelte naudoja.
Persistence ir localStorage
Dažna užduotis – išsaugoti state tarp sesijų. Nanostores neturi įmontuoto persistence mechanizmo, bet jį lengva pridėti:
„`javascript
import { atom } from ‘nanostores’
export function persistentAtom(key, initial) {
const store = atom(initial)
// Užkrauti iš localStorage
if (typeof window !== ‘undefined’) {
const saved = localStorage.getItem(key)
if (saved !== null) {
try {
store.set(JSON.parse(saved))
} catch (e) {
console.error(‘Failed to parse stored value:’, e)
}
}
}
// Išsaugoti kiekvieną kartą keičiantis
store.subscribe(value => {
if (typeof window !== ‘undefined’) {
localStorage.setItem(key, JSON.stringify(value))
}
})
return store
}
// Naudojimas
export const userPreferences = persistentAtom(‘preferences’, {
theme: ‘light’,
language: ‘lt’
})
„`
Ši funkcija automatiškai sinchronizuoja store su localStorage. Galima išplėsti ir pridėti debouncing, kad per daug dažnai nerašytų į localStorage, arba naudoti sessionStorage vietoj localStorage.
Realaus projekto architektūra
Kaip organizuoti stores didesnėje aplikacijoje? Štai viena iš gerų praktikų:
„`
src/
stores/
auth/
user.js
session.js
permissions.js
products/
list.js
filters.js
cart.js
ui/
theme.js
sidebar.js
notifications.js
index.js
„`
Kiekviena sritis turi savo aplanką su susijusiomis stores. Pagrindinis index.js failas eksportuoja viską:
„`javascript
// stores/index.js
export * from ‘./auth/user’
export * from ‘./auth/session’
export * from ‘./products/list’
// ir t.t.
„`
Tokia struktūra leidžia lengvai naršyti ir prižiūrėti kodą. Kiekviena store faile gali būti ne tik pati store, bet ir susijusios funkcijos:
„`javascript
// stores/products/cart.js
import { map, computed } from ‘nanostores’
export const cartItems = map({})
export const addToCart = (productId, quantity = 1) => {
const items = cartItems.get()
cartItems.setKey(productId, (items[productId] || 0) + quantity)
}
export const removeFromCart = (productId) => {
const items = { …cartItems.get() }
delete items[productId]
cartItems.set(items)
}
export const cartTotal = computed(cartItems, (items) => {
return Object.values(items).reduce((sum, qty) => sum + qty, 0)
})
export const clearCart = () => {
cartItems.set({})
}
„`
Taip visos cart operacijos yra vienoje vietoje, lengvai testuojamos ir perpanaudojamos.
Performance ir optimizacijos
Vienas iš dažniausių klausimų – ar nanostores greitesnė už Redux ar kitas alternatyvas? Trumpas atsakymas – taip, dažniausiai taip. Ilgesnis atsakymas – priklauso nuo use case.
Nanostores privalumai performance požiūriu:
- Mažas bundle size – 334 baitai reiškia, kad beveik neįtakoja aplikacijos dydžio
- Granular subscriptions – komponentai klauso tik tų stores, kurios jiems reikia
- No unnecessary re-renders – jei store reikšmė nepasikeitė, re-render’o nebus
- Lazy loading – stores aktyvuojasi tik kai reikia
Bet yra ir niuansų. Jei turi labai sudėtingą state su daug tarpusavio priklausomybių, Redux su immer ar MobX gali būti efektyvesni. Nanostores puikiai tinka small-to-medium aplikacijoms arba kaip papildymas prie framework’o built-in state management.
Praktinis patarimas – naudok React DevTools Profiler arba panašius įrankius pamatyti, ar tikrai yra performance problemų. Dažnai optimizuoti reikia ne state management, o komponentų render logiką.
Kada naudoti ir kada ne
Nanostores puikiai tinka:
- Micro-frontends, kur reikia dalintis state tarp skirtingų framework’ų
- Mažoms ir vidutinėms aplikacijoms, kur Redux būtų overkill
- Bibliotekoms, kurios nori eksportuoti reactive state
- SSR projektams – nanostores veikia ir serveryje
- Kai bundle size yra kritiškas
Galbūt neverta naudoti:
- Labai didelėse enterprise aplikacijose su sudėtingu state
- Kai komanda jau turi gilų Redux ar MobX žinių bagažą
- Jei reikia time-travel debugging ar panašių advanced features
- Kai framework’as jau turi puikų built-in sprendimą (pvz., Svelte stores)
Bet net ir šiais atvejais nanostores gali būti naudinga kaip papildymas – pvz., dalintis state tarp skirtingų aplikacijos dalių.
Ką reikia žinoti prieš pradedant
Jei nusprendei išbandyti nanostores savo projekte, štai keletas praktinių patarimų, kurie sutaupys laiko:
1. Pradėk nuo mažo
Nereikia iš karto migruoti viso state į nanostores. Pradėk nuo vienos mažos funkcionalumo dalies – pvz., theme switcher ar notification system. Pamatysi, kaip veikia, ir tada galėsi plėsti.
2. TypeScript draugiškas
Nanostores puikiai veikia su TypeScript. Tiesiog apibrėžk tipus:
„`typescript
import { atom, map } from ‘nanostores’
interface User {
id: number
name: string
email: string
}
export const currentUser = atom
export const settings = map<{
theme: 'light' | 'dark'
language: string
}>({
theme: ‘light’,
language: ‘en’
})
„`
3. Testing yra paprastas
Kadangi stores yra tiesiog JavaScript objektai, testuoti lengva:
„`javascript
import { counterStore } from ‘./stores’
test(‘counter increments’, () => {
counterStore.set(0)
counterStore.set(counterStore.get() + 1)
expect(counterStore.get()).toBe(1)
})
„`
4. DevTools palaikymas
Nors nanostores neturi dedicated DevTools, galima naudoti paprastą debug helper’į:
„`javascript
if (process.env.NODE_ENV === ‘development’) {
const stores = { counterStore, userStore, cartStore }
Object.entries(stores).forEach(([name, store]) => {
store.subscribe(value => {
console.log(`[${name}]`, value)
})
})
}
„`
5. Dokumentacija ir community
Oficiali dokumentacija yra gera, bet kompaktiška. GitHub issues ir discussions yra aktyvūs – jei kyla klausimų, greičiausiai kažkas jau yra klausęs. Bibliotekos autorius Andrey Sitnik aktyviai dalyvauja diskusijose.
Nanostores nėra hype train ar dar vienas „next big thing”. Tai pragmatiškas įrankis, kuris sprendžia konkrečią problemą – state management be bereikalingo sudėtingumo. Jei tau Redux atrodo per sunkus, o Context API per ribotas, nanostores gali būti tas sweet spot, kurio ieškojei. Biblioteka jau naudojama production’e įvairiose aplikacijose, įskaitant kai kuriuos Shopify projektus.
Svarbiausia – nebijok eksperimentuoti. Sukurk mažą demo projektą, pabandyk skirtingus patterns, pamatyk, ar tai tinka tavo workflow. Galbūt nanostores taps tavo go-to sprendimu state management užduotims, o galbūt ne – bet bent jau išplėsi savo įrankių arsenalą ir supratimą, kaip state management gali veikti skirtingai.
