Template for client side svelte store (unofficial)
live demo: https://svelte.dev/repl/a76e9e11af784185a39020fec02b7733?version=3.31.2
npm install
npm run dev
Navigate to localhost:5000 and open dev-tools.
- Copy all
src/store/_svelteStore.*
files in your project- For production builds rollup uses tree-shaking to ignore the debug version 👍
- Create a new file
myStore.js
based onsrc/store/templateStore.js
next to_svelteStore.js
- In
myStore.js
replace all "templateStore" with "myStore" - Delete everything below "Demo-Actions"
- Define initial state in
State
as simple JSON - Write actions that call
storeIn.update(actionName, updaterFn)
Svelte Store aims for separation of concerns by covering everything needed to run a client-side application without any UI. Think of it as the CLI to your Web-App.
For detailed insight of changes or the current state , all you need is your browsers dev-tools. No plugins, zero 0️⃣ dependencies (besides svelte).
↔️ Track state diffs- 🔍 Inspect current state
⚠️ Type warnings- 📌 Persistent storage with a single switch
- ♾️ Infinite loop detection
- 🃏 Testable Actions
- 🔉 Audible activity
See what has been changed over time. This is a debugging feature and deactivated in prod-mode.
See the full state tree to understand the current state behind the GUI. This is a debugging feature and deactivated in prod-mode.
Learn more about Storage-Inspector:
- Firefox: https://developer.mozilla.org/en-US/docs/Tools/Storage_Inspector
- Chrome: https://developer.chrome.com/docs/devtools/storage/sessionstorage/
The initial State
of a SvelteStore also acts as type definition for the top level fields. If an action updates a field with another type, a warning will be shown in dev-tools console. No replacement for TypeScript, but free basic type checks. This is a debugging feature and deactivated in prod-mode.
Learn more about native JS types at Mozilla Developer Network: typeof
The state can optionally persisted in localStorage by creating a store with the persist
flag. Useful for data, that should be rememberd after a page reload or across tabs.
const [storeIn, storeOut] = useStore(new State(), {
name: "templateStore",
persist: true,
})
SvelteStore can break unwanted endless circles of action calls after about 3 seconds, if an action gets called with an interval of < 150 ms.
This feature can be turned off in _svelteStore.js
with settings.loopGuard: false
. This is a debugging feature and deactivated in prod-mode.
screen recording of stopping infinite loops with a confirm dialog about reloading the window
If the users confirms the reload, the window is asked to reload and an error is thrown, to break e.g. for
loops. If the dialog is canceled, the action gets ignored for 150 ms, so a long task may finish.
SvelteStore gives you a hand getting startet with unit tests for actions. It's a good advice, to keep the "reset" action from the templateStore, so you can reset or override the default state before every test.
The setup in this demo-app is based on this article / testing-library.com and uses jest.
Go for a test ride with npm test
or npm run test:watch
to automatically rerun the tests on file-save ⚡.
To write a new test for an action:
- Arrange:
reset()
the state and optionally override it - Act: Call an action an safe the returned new state
- Assert: Write an expactation for the new state
See templateStore.test.js for some examples.
When settings.tickLog
in _svelteStore.js
is turned on, every action makes a "tick"/"click" sound. Inspired by detectors for radio-activity ☢️, this way you simply hear, when too much is going on. Louder clicks mean more updates at the same time. Of course only in dev-mode.
No debugging-functions in production / test-runs, to improve performance. _svelteStore.js
returns the debug version only if process.env.NODE_ENV === 'debug'
.
command | NODE_ENV value |
config by |
---|---|---|
npm run dev |
debug | rollup.config.js |
npm run build |
prod | rollup.config.js |
npm test |
test | jest |
- IMMUTABLE
- PURE UPDATES
When your actions change something (state Object, a list inside state, etc...), make a shallow copy of it!
good:
let { list } = state;
list = [...list]; // shallow copy with spread syntax
list.push(1234);
return { ...state, list };
bad: mutation
let { list } = state;
// mutated objects won't be detected as a change
list.push(1234);
return { ...state, list };
bad: deep copy
let { list } = deepCopy(state);
// EVERY object will look like a change
// Svelte must re-render everything instead just "list"
list.push(1234);
return { ...state, list };
The callbacks for storeIn.update
must not have side-effects and return a shallow-copy-state.
Every update modifies state, so if you want to bundle multiple actions, run them one by one - not nested:
good:
export const multiAction1 = () => {
actionA()
actionB()
// Return last update
return storeIn.update('actionC', function (state) {
let { xy } = state
…
return { ...state, xy}
});
}
export const multiAction2 = () => {
let state = storeOut.get()
let { xy } = state
// A or B depending on current state
if (xy) {
actionA()
} else {
storeIn.update('actionB', function (state) {
let { xy } = state
…
return { ...state, xy}
});
}
// Return last update
return actionC()
}
export const multiAction3 = async () => {
// Follow the state of "xy"
let state = storeOut.get()
let { xy } = state // xy = true
if (xy) await asyncActionA()
// Re-assign updated state when using it
// Beware that this practise may leads to bugs (see bad multiAction3 below)
state = storeIn.update('actionB', function (state) {
let { xy } = state
xy = await api.fetch(xy) // xy = false
return { ...state, xy}
});
xy = state.xy // xy = false (!! don't forget to re-assign)
if (xy) return asyncActionC()
return state
}
bad:
export const multiAction1 = () => {
// Nested actions are side-effects
return storeIn.update('actionA', function (state) {
let { xy } = state
state = actionB() //! Don't call functions inside "update"
actionC() // ...messes up state easily; not pure
…
return { ...state, xy}
});
}
export const multiAction3 = async () => {
// Follow the state of "xy"
let state = storeOut.get()
let { xy } = state // xy = true
if (xy) await asyncActionA()
state = storeIn.update('actionB', function (state) {
let { xy } = state
xy = await api.fetch(xy) // xy = false
return { ...state, xy}
});
// xy is still what it was before actionB.
// (See good: multiAction3 above)
if (xy) return asyncActionC() // xy = true (expected false)
return state
}