Last active
May 23, 2024 09:53
-
-
Save alexamy/d69152eae3619a61567a7e89e52797fe to your computer and use it in GitHub Desktop.
SolidJS store example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createContext, createEffect, on, useContext, type JSXElement } from 'solid-js'; | |
import { createStore } from 'solid-js/store'; | |
import { render } from 'solid-js/web'; | |
// **** This is a complete, operational example of a store that includes custom methods. | |
// **** Copy-paste to https://playground.solidjs.com/ | |
// Store type (with custom methods) | |
export type State = ReturnType<typeof createAppStore>; | |
// Plain store data, don't use optional as much as you can. | |
// If you need to, it's better to provide a default value in `getInitialStore`. | |
// If it's a computed value, `createEffect` will set it on store creation. | |
// It's more practical to limit nesting to a maximum of one level. | |
export interface StoreData { | |
counter: number; | |
doubled: number; | |
cryptic: string; | |
} | |
// Store signals (without custom methods) | |
// Sometimes useful for methods in store file | |
type StoreSignals = ReturnType<typeof createStore<StoreData>>; | |
// Store creation | |
export function createAppStore() { | |
const initial = getInitialStore(); | |
const [store, setStore] = createStore(initial); | |
// Poor man's createMemo | |
createEffect(on(() => store.counter, (counter) => { | |
setStore({ doubled: counter * 2 }); | |
})); | |
// Split methods into a separate function, if they are too complex. | |
// We are deliberately keeping the custom method unaware of the store signals. | |
createEffect(on(() => store.counter, (counter) => { | |
const cryptic = createCryptic(counter); | |
setStore({ cryptic }); | |
})); | |
// Store api | |
function increment() { | |
setStore('counter', (prev) => prev + 1); | |
} | |
function decrement() { | |
setStore('counter', (prev) => prev - 1); | |
} | |
return [store, setStore, { increment, decrement }] as const; | |
} | |
// Initial store data | |
// You can use placeholders for computed values, which will be set by `createEffect`. | |
function getInitialStore(): StoreData { | |
return { | |
counter: 0, | |
doubled: 0, | |
cryptic: '', | |
}; | |
} | |
// Private method, should be independent of store signals. | |
function createCryptic(n: number) { | |
return ((n + 1) * 1024 * 1024).toString(36); | |
} | |
// **** For the sake of simplicity, we don't use a second file for the context. | |
// **** You should split store and context into separate files. | |
// Plain context should be private. | |
const AppContext = createContext<State>({} as State); | |
// Store provider, nothing fancy. | |
export function AppContextProvider(props: { children: JSXElement }) { | |
const store = createAppStore(); | |
return ( | |
<AppContext.Provider value={store}> | |
{props.children} | |
</AppContext.Provider> | |
); | |
} | |
// Custom hook to access store and methods. | |
// Please note that we do not return `setStore` to avoid leaking abstraction. | |
export function useAppContext() { | |
const [store, _setStore, methods] = useContext(AppContext); | |
return { store, ...methods } as const; | |
} | |
// **** Usage example | |
function Counter() { | |
const { store, increment } = useAppContext(); | |
return ( | |
<div> | |
<button type="button" onClick={increment}> | |
{store.counter} | |
</button> | |
<div>Doubled: {store.doubled}</div> | |
<div>Cryptic: {store.cryptic}</div> | |
</div> | |
); | |
} | |
function App() { | |
return ( | |
<AppContextProvider> | |
<Counter /> | |
</AppContextProvider> | |
); | |
} | |
render(() => <App />, document.getElementById("app")!); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment