React integration for Nano Stores, a tiny state manager with many atomic tree-shakable stores.
- Small. Around 1 KB. Zero dependencies.
- Fast. With small atomic and derived stores, you do not need to call the selector function for all components on every store change.
- Tree Shakable. The chunk contains only stores used by components in the chunk.
- Was designed to move logic from components to stores.
- It has good TypeScript support.
import { useStore } from '@nanostores/react'
import { $profile } from '../stores/profile.js'
export const Header = ({ postId }) => {
const profile = useStore($profile)
return <header>Hi, {profile.name}</header>
} Nano Stores React is built by Evil Martians, an American design and engineering consultancy for developer tools, AI, and cybersecurity startups.
Use the keys option to re-render only on specific key changes:
export const Header = () => {
const profile = useStore($profile, { keys: 'name' })
return <header>{profile.name}</header>
}Listening to a base key will automatically trigger a re-render if any of its nested properties mutate.
// Will listen for all changes in profile object
const profile = useStore($profile, { keys: ['profile'] })useLoadingStore() suspends the component while the store is loading.
It supports both loading formats: the state machine of Nano Stores Async
and the isLoading key of Logux Client and Nano Stores SQL.
// stores/user.ts
import { computedAsync } from '@nanostores/async'
import { atom } from 'nanostores'
export const $user = computedAsync($userId, userId => {
return fetch(`/api/users/${userId}`).then(response => response.json())
})While the store is in the loading state, React will render the closest
<Suspense> fallback. In the failed state, the error will be thrown
to the closest error boundary. As a result, the component gets the loaded
value without the AsyncValue wrapper.
import { useLoadingStore } from '@nanostores/react'
import { Suspense } from 'react'
import { $user } from '../stores/user.js'
const UserName = () => {
// TypeScript knows that the user is loaded and has no error here
const user = useLoadingStore($user)
return <h1>{user.name}</h1>
}
export const Page = () => (
<ErrorBoundary fallback={<LoadingError />}>
<Suspense fallback={<Spinner />}>
<UserName />
</Suspense>
</ErrorBoundary>
)React has no built-in error boundary. You can write your own class component
or take react-error-boundary.
SSR could be very complicated in React. To avoid hydration errors you need exactly the same stores state in the end of server HTML rendering and during the first DOM render on the client.
For simple solution you can disable any store update on the server
by ssr: 'initial':
export const Header = () => {
const profile = useStore($profile, { ssr: 'initial' })
// Hydrate with initial profile, then render latest client-side value
return <header>{profile.name}</header>
}For advanced cases where you update store values on the server before SSR, and need pages to hydrate with the updated value from the server, set a function that returns the server state: ssr: () => serverState.
// Value of store on server at time of SSR, passed to client somehow...
const profileFromServer = { name: 'A User' }
export const Header = () => {
const profile = useStore($profile, {
ssr:
typeof window === 'undefined'
? // On server, always use up-to-date store value (no SSR handling)
false
: // On client, set server value to avoid error on hydration
() => profileFromServer
})
// Hydrate with profile at time of SSR, then render latest client-side value
return <header>{profile.name}</header>
}A function set on ssr is provided to React's useSyncExternalStore
as the getServerSnapshot option.