Stan.js is designed for performance out of the box, but following these patterns will ensure your application runs efficiently at scale.
Re-render Optimization
Stan.js uses a proxy-based selective subscription system that automatically optimizes re-renders.
Only Subscribe to What You Use
Components only re-render when the state values they actually access change:
const { useStore } = createStore ({
counter: 0 ,
message: 'hello' ,
users: [] as Array < string >
})
// This component ONLY subscribes to 'counter'
const Counter = () => {
const { counter , setCounter } = useStore ()
// ^^^^^^^ subscribed
// ^^^^^^^^^^^ NOT subscribed (action only)
return < button onClick ={() => setCounter ( prev => prev + 1 )}>{ counter } </ button >
}
// Updating 'message' or 'users' will NOT re-render Counter
Accessing only setter functions means zero re-renders when state changes. This is perfect for action-only components.
Split Components Strategically
Break components into smaller pieces that access minimal state:
// ❌ Bad: Single component subscribes to everything
const Dashboard = () => {
const { user , posts , comments , notifications } = useStore ()
return (
< div >
< UserProfile user = { user } />
< PostList posts = { posts } />
< CommentList comments = { comments } />
< NotificationBadge count = {notifications. length } />
</ div >
)
}
// ✅ Good: Each component subscribes only to what it needs
const UserProfileContainer = () => {
const { user } = useStore ()
return < UserProfile user = { user } />
}
const PostListContainer = () => {
const { posts } = useStore ()
return < PostList posts = { posts } />
}
const Dashboard = () => (
< div >
< UserProfileContainer />
< PostListContainer />
{ /* ... */ }
</ div >
)
Use getState for One-Time Reads
Read values without subscribing using getState():
import { getState , useStore } from './store'
const FormInput = () => {
const { setMessage } = useStore ()
// No subscription, no re-renders from 'message' changes
return (
< input
defaultValue = { getState ().message}
onChange = { e => setMessage ( e . target . value )}
/>
)
}
Computed Values
Use getters for derived state instead of calculating in components:
// ❌ Bad: Calculation happens on every render
const UserList = () => {
const { users } = useStore ()
const activeUsers = users . filter ( u => u . active ) // Recalculates every render
return < div >{activeUsers. length } active users </ div >
}
// ✅ Good: Calculation happens in the store
const { useStore } = createStore ({
users: [] as Array < User >,
get activeUsers () {
return this . users . filter ( u => u . active ) // Only recalculates when users change
}
})
const UserList = () => {
const { activeUsers } = useStore ()
return < div >{activeUsers. length } active users </ div >
}
From src/vanilla/createStore.ts:186, computed values track dependencies and only update when necessary.
Batch Updates
Group multiple updates to trigger listeners only once:
import { batchUpdates , actions } from './store'
// ❌ Bad: Three separate updates = three re-renders
const resetForm = () => {
actions . setName ( '' )
actions . setEmail ( '' )
actions . setMessage ( '' )
}
// ✅ Good: One batch = one re-render
const resetForm = () => {
batchUpdates (() => {
actions . setName ( '' )
actions . setEmail ( '' )
actions . setMessage ( '' )
})
}
Custom actions are automatically batched :
const { useStore } = createStore (
{ name: '' , email: '' , message: '' },
({ actions }) => ({
resetForm : () => {
actions . setName ( '' )
actions . setEmail ( '' )
actions . setMessage ( '' )
// Automatically batched - only one re-render
}
})
)
Functional Updates
Use functional updates to avoid stale closures:
const { setCounter } = useStore ()
// ❌ Bad: May use stale value in closures
setTimeout (() => {
setCounter ( counter + 1 ) // 'counter' might be stale
}, 1000 )
// ✅ Good: Always uses latest value
setTimeout (() => {
setCounter ( prev => prev + 1 )
}, 1000 )
Memoization Patterns
React.memo for Presentational Components
import { memo } from 'react'
interface UserCardProps {
user : User
}
// Only re-renders when 'user' prop changes
const UserCard = memo (({ user } : UserCardProps ) => (
< div >
< h3 >{user. name } </ h3 >
< p >{user. email } </ p >
</ div >
))
const UserList = () => {
const { users } = useStore ()
return (
< div >
{ users . map ( user => < UserCard key ={ user . id } user ={ user } />)}
</ div >
)
}
useMemo for Expensive Calculations
import { useMemo } from 'react'
const Chart = () => {
const { dataPoints } = useStore ()
// Only recalculate when dataPoints change
const chartData = useMemo (() => {
return expensiveTransformation ( dataPoints )
}, [ dataPoints ])
return < ChartComponent data = { chartData } />
}
In most cases, computed values in the store are better than useMemo in components.
Keep Storage Values Small
LocalStorage is synchronous and blocks the main thread:
// ❌ Bad: Storing large datasets
const { useStore } = createStore ({
allUsers: storage ([]) // Could be thousands of items
})
// ✅ Good: Store only necessary data
const { useStore } = createStore ({
currentUserId: storage ( '' ),
recentUserIds: storage ([] as Array < string >) // Limited to last 10
})
Debounce Storage Writes
For frequently updated values, debounce writes:
import { createStore } from 'stan-js'
import { debounce } from 'lodash'
const { useStore , actions } = createStore ({
draftMessage: ''
})
// Debounced storage write
const saveDraft = debounce (( message : string ) => {
localStorage . setItem ( 'draft' , message )
}, 500 )
const MessageEditor = () => {
const { draftMessage , setDraftMessage } = useStore ()
const handleChange = ( value : string ) => {
setDraftMessage ( value )
saveDraft ( value )
}
return < textarea value = { draftMessage } onChange = { e => handleChange ( e . target . value )} />
}
Effect Optimization
Selective Dependencies
Effects automatically track dependencies:
const { effect } = createStore ({
user: null as User | null ,
posts: [] as Array < Post >,
comments: [] as Array < Comment >
})
// Only runs when 'user' changes, not 'posts' or 'comments'
effect (({ user }) => {
if ( user ) {
logUserActivity ( user . id )
}
})
Dispose Effects When Done
Always clean up effects to prevent memory leaks:
const dispose = store . effect (({ counter }) => {
console . log ( 'Counter:' , counter )
})
// When no longer needed
dispose ()
In React:
useEffect (() => {
const dispose = store . effect (({ counter }) => {
console . log ( 'Counter:' , counter )
})
return dispose // Cleanup on unmount
}, [])
Large Lists
Virtualization
For long lists, use virtualization:
import { FixedSizeList } from 'react-window'
const VirtualizedList = () => {
const { items } = useStore ()
const Row = ({ index , style } : { index : number ; style : React . CSSProperties }) => (
< div style = { style } > {items [index].name}</div>
)
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={35}
width="100%"
>
{Row}
</FixedSizeList>
)
}
Limit data in the store:
const { useStore } = createStore (
{
currentPage: 1 ,
itemsPerPage: 50 ,
allItemIds: [] as Array < string >,
itemsById: {} as Record < string , Item >,
get currentItems () {
const start = ( this . currentPage - 1 ) * this . itemsPerPage
const end = start + this . itemsPerPage
return this . allItemIds . slice ( start , end ). map ( id => this . itemsById [ id ])
}
},
({ actions , getState }) => ({
nextPage : () => {
actions . setCurrentPage ( prev => prev + 1 )
},
prevPage : () => {
actions . setCurrentPage ( prev => Math . max ( 1 , prev - 1 ))
}
})
)
Normalized Data
Normalize data for efficient updates:
// ❌ Bad: Nested data is hard to update
const { useStore } = createStore ({
posts: [
{ id: '1' , title: 'Post 1' , author: { id: 'a1' , name: 'Alice' } },
{ id: '2' , title: 'Post 2' , author: { id: 'a1' , name: 'Alice' } }
]
})
// ✅ Good: Normalized data
const { useStore } = createStore ({
postsById: {
'1' : { id: '1' , title: 'Post 1' , authorId: 'a1' },
'2' : { id: '2' , title: 'Post 2' , authorId: 'a1' }
} as Record < string , Post >,
authorsById: {
'a1' : { id: 'a1' , name: 'Alice' }
} as Record < string , Author >,
get posts () {
return Object . values ( this . postsById )
}
})
// Update a single author efficiently
actions . setAuthorsById ( prev => ({
... prev ,
'a1' : { ... prev [ 'a1' ], name: 'Alice Updated' }
}))
Profiling and Monitoring
Use the React DevTools Profiler to identify unnecessary re-renders:
Open React DevTools
Go to Profiler tab
Start recording
Interact with your app
Stop and analyze which components re-rendered
Custom Logging
const { useStore , effect } = createStore ({
counter: 0
})
// Log all state changes in development
if ( process . env . NODE_ENV === 'development' ) {
effect ( state => {
console . log ( 'State changed:' , state )
})
}
// Track specific actions
const { setCounter } = useStore ()
const trackedSetCounter = ( value : number ) => {
console . time ( 'setCounter' )
setCounter ( value )
console . timeEnd ( 'setCounter' )
}
Best Practices Summary
Selective Subscriptions Access only the state values you need. Avoid destructuring everything.
Computed Values Use getters for derived state instead of calculating in components.
Batch Updates Use batchUpdates or custom actions to group related state changes.
Split Components Break large components into smaller ones that subscribe to minimal state.
Normalize Data Use normalized data structures (by ID) for efficient updates.
Dispose Effects Always clean up effects and subscriptions to prevent memory leaks.
Virtualize Lists Use virtualization libraries for large lists to render only visible items.
Profile Regularly Use React DevTools Profiler to identify and fix performance issues.