[svelte store][03] Implementing StopWatch
About Svelte Store
in this post, we'll implement a stop watch using get(), readable(), and writable() of the svelte store.

Configure the screen
the stopwatch screen looks like this
StopWatch.svelte<script lang="ts">
</script>
<section class="flex-center">
<div class="time-display flex-center">
<span class="point"></span>
<span>00:00:00</span>
</div>
<div class="buttons">
<button>Start</button>
<button>Stop</button>
</div>
</section>
<style lang="scss">
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
section {
flex-direction: column;
gap: 10px;
.time-display {
width: 100px;
height: 100px;
border: 5px solid #333;
border-radius: 50%;
position: relative;
box-sizing: border-box;
.point {
position: absolute;
width: 8px;
height: 8px;
background-color: red;
border-radius: 50%;
top: 0%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.buttons {
display: flex;
gap: 10px;
}
}
</style>
- the
.time-display
corresponds to the circle of the stopwatch. - the
.point
is a red dot that rotates around the circumference of the circle every second. it is currently located at the top of the circle (top: 0%; left: 50%;
). - start/stop - Start and stop the stopwatch.
note that StopWatch.svelte
has no function.
we will now create a svelte store implementation to manage the state of the stopwatch and apply it to the screen.
stopwatch.store.ts
the svelte store implementation that manages the state of the stopwatch provides the following interface.
TYPESCRIPTexport type Millis = number;
export type StopWatchState = {
running: boolean;
elapsed: Millis;
px: number;
py: number;
};
export const createStopWatch = () => {
let startTime: number = 0;
let timer: number = 0;
const store = writable({
running: false,
elapsed: 0,
px: 50,
py: 0
});
/** format elapsed as "mm:ss.SS"
*/** format elapsed as "mm:ss.SS
function formatTime(state:StopWatchState):string {
return "00:00:00";
}
/** start stopwatch
*/ /** start stopwatch
function start():void {}
/** stop stopwatch
*/** stop
function stop():void {}
return {
store,
stopWatch: {
start,
stop,
formatTime
}
};
}
starTime` - Stores the time the stopwatch was started.
running
, elapsed
, (px, py)
- corresponds to the state of the stopwatch, where (px, py) are the coordinates to represent the red dot on the circle.
the functions start()
and stop()
change the state of the stopwatch and return a svelte store object (const store
) so that the View can detect the change. the function formatTime()
returns the elapsed time as a string "mm:ss.SS".
for readability, the functions are wrapped in stopWatch
to return them.
in StopWatch.svelte
, we use the store object as follows.
StopWatch.svelte<script lang="ts">
import { createStopWatch } from './stopwatch.store';
const { store, stopWatch } = createStopWatch();
</script> </script
<section class="flex-center"> <div class="time-display
<div class="time-display flex-center"> <span class="point
<span class="point"></span>
<span>{stopWatch.formatTime($store)}</span>
</div>
<div class="buttons">
<button on:click={() => stopWatch.start()}>Start</button>
<button on:click={() => stopWatch.stop()}>Stop</button>
</div>
</section>
store
- svelte store object for retrieving the statestopWatch
- provides functions to change the state (start, stop, formatText).
{stopWatch.formatTime($store)}
- uses the {...}
syntax when inserting text in the View, which corresponds to {{...}}
in vue. the variable store
is a svelte store object, so to update the elapsed time reactively, we prefix it with a store ($store
) and pass it as an argument to the function.
stopWatch.formatTime()
implement the formatTime function.
stopwatch.store.tsexport const createStopWatch = () => {
const store = wrtiable(...)
/** /**
* format elapsed as "mm:ss.SS"
*/** format elapsed as "mm:s.SS
function formatTime(state:StopWatchState):string {
const {elapsed} = state;
const seconds = Math.floor(elapsed / 1000);
const mm = pad2(seconds / 60)
const ss = pad2(seconds % 60);
const SS = pad2((elapsed % 1000) / 10)
return `${mm}:${ss}.${SS}`;
}
}
in stopwatch.store.ts
, the variable store is of type Writable<StopWatchState>
.
stopwatch.store.tsimport { ..., writable, type Writable } from 'svelte/store';
export const createStopWatch = () => {
const store:Writable<StopWatchState> = ...;
function formatTime(state:StopWatchState):string { ... }
}
when you call StopWatch.svelte
with a store prefix, such as stopWatch.formatTime($store)
, the type of $store
is StopWatchState
.
stopWatch.start()
Click the Start button to run the stopwatch. implement the function start()
as follows.
start(), stopwatch.store.tsimport { get, ... } from 'svelte/store';
export const createStopWatch = () => {
function start() {
if (get(store).running) {
return;
}
startTime = Date.now();
store.update((state) => {
state.running = true;
updatePosition(state);
return state;
});
timer = setInterval(updateTime, 10);
}
}
get(store).running
- Use the function get()
to access the state StopWatchState
that the svelte store object is enclosing. In svelte components, you can access state information with $store.running
, but in a regular ts(js) file, you must use the function get()
.
store.update((state) => ...)
- starts the stopwatch by setting the elapsed and running variables. After reading it with get(), you can save the changed state with store.set() or call update(...)
to change the state.
updatePosition()` - This function updates the elapsed time of the stopwatch and the position of the red dot.
setInterval(updateTime, 10);- Calls
updateTime`, a function that changes the stopwatch state every 10 milliseconds.
store.update(callback)
there is a caveat when using update() on a svelte store object returned by calling writable()
.
Use of store.update()store.update((state) => {
// 1. modifications here
...
// 2. Do return state;
return state;
});
doSomeTaksk();
- when you register a callback, it receives state information as an argument.
- it must return the state it was passed back.
- You should be careful because Typescript will show compilation errors, but Javascript will not show syntax errors.
- the callback function that changes the state is called synchronously, which means that you run
doSomeTask()
after the callback has fully executed.
When you call
store.set()
orstore.update()
, views that reference the svelte store object are updated reactively.
updatePosition(), updateTime()
these functions are private functions. unlike start() and stop(), they are not exposed in the interface.
TS function updatePosition(state: StopWatchState) {
const angle = (state.elapsed / 1000) * 2 * Math.PI;
const radius = 50;
state.px = 50 + radius * Math.sin(angle);
state.py = 50 - radius * Math.cos(angle);
}
function updateTime() {
if (get(store).running) {
store.update((state) => {
state.elapsed = Date.now() - startTime;
updatePosition(state);
return state;
});
}
}
updatePosition()- maps 1000 milliseconds to 360 degrees of a circle. this value is passed as a css variable in
StopWatch.svelte`.
(px, py) to css variables
in function updatePositon()
, update the position of the red dot.
(px, py) to the component as follows.
StopWatch.svelte(View)<section class="flex-center"
style="--point-x: {$store.px}%; --point-y: {$store.py}%">
</section>
$store.px
,$store.py
- reactively reads in the changed values and automatically updates the css variable values.
in CSS, we modify the top, left properties as follows.
StopWatch.svelte(SCSS)<style lang="scss"><style lang="scss"></style>
section {
...
.time-display {
...
.point {
...
top: var(--point-y);
left: var(--point-x);
...
}
}
}
</style>
running state
disables the START button when the stopwatch is running.
StopWatch.svelte(Html)<section class="flex-center"
style="--point-x: {$store.px}%; --point-y: {$store.py}%">
<div class="buttons">
<button on:click={() => stopWatch.start()}
disabled={$store.running}>{$store.running ? '...' : 'Start'}</button>
<button>Stop</button>
</div>
</section>
disabled={$store.running}` - If the stopwatch is running, the button will be disabled.
{$store.running ? '...' : 'Start'}
- Changes the text of the button if the stopwatch is running.
Final code
StopWatch.svelte<script lang="ts"><script lang="ts">
import { createStopWatch } from './stopwatch.store';
const { store, stopWatch } = createStopWatch();
</script> </script
<section
class="flex-center"
style="--point-x: {$store.px}%; --point-y: {$store.py}%">
<div class="time-display flex-center"> <span class="point"></span>
<span class="point"></span>
<span>{stopWatch.formatTime($store)}</span>
</div>
<div class="buttons">
<button on:click={() => stopWatch.start()}
disabled={$store.running}>{$store.running ?
'...' : 'Start'}</button>
<button on:click={() => stopWatch.stop()}>Stop</button>
</div>
</section>
<style lang="scss">
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
section {
flex-direction: column;
gap: 10px;
.time-display {
width: 100px;
height: 100px;
border: 5px solid #333;
border-radius: 50%;
position: relative;
box-sizing: border-box;
.point {
position: absolute;
width: 8px;
height: 8px;
background-color: red;
border-radius: 50%;
top: var(--point-y);
left: var(--point-x);
transform-origin: center;
transform: translate(-50%, -50%);
}
}
.buttons {
display: flex;
gap: 10px;
}
}
</style>
stopwatch.store.tsimport { get,
readable,
writable,
type Writable } from 'svelte/store';
export type Millis = number;
export type StopWatchState = {
running: boolean;
elapsed: Millis;
px: number;
py: number;
};
export const createStopWatch = () => {
let startTime: number = 0;
let timer: number = 0;
const store:Writable<StopWatchState> = writable({
running: false,
elapsed: 0,
px: 50,
py: 0
});
function pad2(int:number) {
return Math.floor(int).toString().padStart(2, '0');
}
function formatTime(state:StopWatchState) {
const {elapsed} = state;
const seconds = Math.floor(elapsed / 1000);
const mm = pad2(seconds / 60)
const ss = pad2(seconds % 60);
const SS = pad2((elapsed % 1000) / 10)
return `${mm}:${ss}.${SS}`;
}
function updatePosition(state: StopWatchState) {
const angle = (state.elapsed / 1000) * 2 * Math.PI;
const radius = 50;
state.px = 50 + radius * Math.sin(angle);
state.py = 50 - radius * Math.cos(angle);
}
function updateTime() {
if (get(store).running) {
store.update((state) => {
state.elapsed = Date.now() - startTime;
updatePosition(state);
return state;
});
}
}
function start() {
if (get(store).running) {
return;
}
startTime = Date.now();
store.update((state) => {
state.running = true;
updatePosition(state);
return state;
});
timer = setInterval(updateTime, 10);
}
function stop() {
clearInterval(timer);
store.update((state) => {
state.running = false;
state.elapsed = 0;
updatePosition(state);
return state;
});
}
return {
store,
stopWatch: {
start,
stop,
formatTime
}
};
};
readable()
we are exposing a store object in stopwatch.store.ts
to the view that is both readable and writable.
stopwatch.store.ts const store:Writable<StopWatchState> = writable({
running: false,
elapsed: 0,
px: 50,
py: 0
});
return {
store,
...
};
In View, you can also update the store object directly, as follows
AnotherComponent.svelte<script lang="ts"><br
import { createStopWatch } from './stopwatch.store';
const { store, stopWatch } = createStopWatch();
function resetStopWatch = () => {
$store = { running: false, ...} // store.set(...)
}
</script>
by exposing the store object as read-only, we can prevent incorrect operations like the one above.
exposing readable storeimport { get, readable, writable, ... } from 'svelte/store';
export const createStopWatch = () => {
const store = writable({...});
const readableStore = readable(get(store), (set) => {
const unsub = store.subscribe((state) => {
set(state);
});
return unsub;
})
return {
store: readableStore,
...
};
}
the readable function takes an initial value as its first argument.
the second argument registers a callback that can change the internal state of the readable store.
the callback is passed a function (set()
), which can be called to change the state of the readable store by calling set(...)
.
the readableStore
needs to reflect the changed state of the stop watch in real time, so it watches for state changes via store.subscribe.
store.subscribe(..) const unsub = store.subscribe((state) => {
set(state);
});
return unsub;
now the code below, which calls set on AnotherComponent.svelte
, will throw an error.
AnotherComponent.svelte $store = { running: false, ...} // not executed
- unlike writable stores, readable stores do not have a method
set(..)
. - the state of a readable store can only be changed in a callback inside the store.