Overview
StoreProvider is a React component returned by createScopedStore that provides a store instance to its children via React Context. It allows you to create isolated store instances for different parts of your component tree.
Type Definition
type StoreProviderProps<TState extends object> = {
initialValue?: Partial<RemoveReadonly<TState>>
children: ReactNode
}
type StoreProvider<TState> = FunctionComponent<StoreProviderProps<TState>>
Props
initialValue
Partial<RemoveReadonly<TState>>
Optional initial values to merge with the base initial state. This allows you to customize the initial state for each provider instance.
React children that will have access to this store instance.
Usage
Basic Provider
import { createScopedStore } from 'stan-js'
const { StoreProvider, useStore } = createScopedStore({
count: 0,
label: 'Counter'
})
function App() {
return (
<StoreProvider>
<Counter />
</StoreProvider>
)
}
function Counter() {
const { count, actions } = useStore()
return <button onClick={() => actions.setCount(count + 1)}>{count}</button>
}
With Initial Value
const { StoreProvider, useStore } = createScopedStore({
name: '',
email: '',
role: 'user'
})
function App() {
return (
<StoreProvider
initialValue={{
name: 'John Doe',
email: '[email protected]'
}}
>
<UserForm />
</StoreProvider>
)
}
Multiple Instances
const { StoreProvider, useStore } = createScopedStore({
title: '',
content: '',
isDirty: false
})
function App() {
return (
<div>
<h2>Edit Post 1</h2>
<StoreProvider
initialValue={{
title: 'First Post',
content: 'Content 1'
}}
>
<Editor />
</StoreProvider>
<h2>Edit Post 2</h2>
<StoreProvider
initialValue={{
title: 'Second Post',
content: 'Content 2'
}}
>
<Editor />
</StoreProvider>
</div>
)
}
function Editor() {
const { title, content, isDirty, actions } = useStore()
return (
<div>
<input
value={title}
onChange={(e) => {
actions.setTitle(e.target.value)
actions.setIsDirty(true)
}}
/>
<textarea
value={content}
onChange={(e) => {
actions.setContent(e.target.value)
actions.setIsDirty(true)
}}
/>
{isDirty && <span>Unsaved changes</span>}
</div>
)
}
Implementation Details
Store Creation
The provider creates a store instance on mount using useState:
const [store] = useState(() =>
createStore(mergeState(initialState, initialValue ?? {}))
)
This ensures:
- The store is created only once per provider instance
- Initial values are merged with the base initial state
- Each provider has its own isolated store
Dynamic Initial Value Updates
When initialValue prop changes, the provider updates the store:
const isMounted = useRef(false)
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true
return
}
store.batchUpdates(() =>
Object.entries(initialValue ?? {}).forEach(([key, value]) => {
store.actions[getActionKey(key)](value)
})
)
}, [initialValue])
The isMounted ref prevents updates on the initial render, only updating when the prop actually changes.
Context Provider
The component renders a React Context Provider:
return <StoreContext.Provider children={children} value={store} />
Common Patterns
Controlled Initial State
function UserEditor({ userId }: { userId: string }) {
const [userData, setUserData] = useState(null)
useEffect(() => {
fetchUser(userId).then(setUserData)
}, [userId])
if (!userData) return <Loading />
return (
<StoreProvider
initialValue={{
name: userData.name,
email: userData.email
}}
>
<EditForm />
</StoreProvider>
)
}
Nested Providers
const { StoreProvider: AppStoreProvider } = createScopedStore({
theme: 'light',
user: null
})
const { StoreProvider: FormStoreProvider } = createScopedStore({
formData: {},
errors: {}
})
function App() {
return (
<AppStoreProvider initialValue={{ theme: 'dark' }}>
<FormStoreProvider>
<MyForm />
</FormStoreProvider>
</AppStoreProvider>
)
}
List of Providers
function TaskList({ tasks }: { tasks: Task[] }) {
return (
<div>
{tasks.map(task => (
<StoreProvider
key={task.id}
initialValue={{
title: task.title,
completed: task.completed
}}
>
<TaskItem />
</StoreProvider>
))}
</div>
)
}
Conditional Provider
function ConditionalProvider({ useScoped, children }) {
if (useScoped) {
return (
<StoreProvider initialValue={{ isolated: true }}>
{children}
</StoreProvider>
)
}
return <>{children}</>
}
Memoization
If initialValue is an object literal, consider memoizing it:
// Bad - creates new object on every render
<StoreProvider initialValue={{ name: 'John' }}>
// Good - stable reference
const initialValue = useMemo(() => ({ name: 'John' }), [])
<StoreProvider initialValue={initialValue}>
Batch Updates
The provider uses batchUpdates when applying initial value changes to prevent multiple re-renders.
See Also