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.
src/store/_svelteStore.*
files in your projectmyStore.js
based on src/store/templateStore.js
next to _svelteStore.js
myStore.js
replace all "templateStore" with "myStore"State
as simple JSONstoreIn.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).
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:
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.
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:
reset()
the state and optionally override itSee 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 |
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
}