Skip to content

Instantly share code, notes, and snippets.

@alexamy
Last active May 23, 2024 09:53
Show Gist options
  • Save alexamy/d69152eae3619a61567a7e89e52797fe to your computer and use it in GitHub Desktop.
Save alexamy/d69152eae3619a61567a7e89e52797fe to your computer and use it in GitHub Desktop.
SolidJS store example
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