Passive user preferences with persisted stores in Svelte.

Mat Ryer · 9 Sep 2020

Passive user preferences with persisted stores in Svelte.

Mat Ryer · 9 Sep 2020

The case for passive preferences

In Pace.dev users customise their experience passively as they interact with the UI.

We achieve this by remembering their settings for next time in the browser’s data store. This little trick turns out to deliver a pretty powerful user experience punch.

A few examples of this include our Send on enter toggle in comment boxes, the light/dark mode setting, and the number of cards to display per page.

If the user picks ten cards per page, they are probably on a smaller device. Pace will remember to show them ten cards per page for the foreseeable. Since the storage is in the browser, the same account on different machines will have different preferences by default - which works well for us in our case.

There might be something interesting that a user experience expert could tell us about how this behaviour mirrors the real world; if you open a drawer, it stays open until you close it.

It is a very effective passive way that users can set their preferences.

Stores in Svelte

Stores in Svelte provide a mechanism by which components can subscribe to changes in data. So we can put the number of cards value into a store, and any component that cares about that data will be notified when it changes.

Svelte adds some nice (and more importantly, easy to learn) syntactic sugar around stores, so subscribing is as simple as mentioning the store with a $ prefix.

Using stores in Svelte

In Pace, we have a stores.svelte file that contains code like this:

<script lang='ts' context='module'>

	import { writable } from 'svelte/store'
	export const hasUnreadItems = writable(false)

</script>

The writable function creates a mutable store.

In our components we import hasUnreadItems and refer to it in template code with the $ prefix.

<script lang='ts'>
	import { hasUnreadItems } from '/stores.svelte'
</script>
{#if $hasUnreadItems}
	<div>...</div>
{/if}

In our App.svelte we have some code running that is checking for unread items.

When the event occurs, we use the set method to update the store.

import { hasUnreadItems } from '/stores.svelte'

function onHasUnreadItems() {
	hasUnreadItems.set(true)
}

When we call hasUnreadItems.set, the if condition in the component above will be reevaluated, and the app will update accordingly.

Making stores persistent

Stores in Svelte are in-memory, and so are scoped to the page.

We wanted to persist the values between page refreshes in the simplest way possible.

The store API in Svelte is very minimalistic, which makes it easy to implement ourselves, and even build functionality on top of other stores.

Our persistable function uses a store internally and provides alternative functions for set and update which call out to other functions to persist and retrieve the values.

In this case, we store the values in the IndexedDB.

Here’s the entire code in JavaScript:

/*
 * Svelte persistent store that saves to IndexedDB.
 * 
 * Usage, store.js:
 * export const count = persistable('count', 0)
 */
export function persistable(key, defaultValue) {
	let currentValue = defaultValue
	const { subscribe, set, update } = writable(defaultValue)
	try {
		getUserPreference(key).then(persisted => {
			if (persisted && persisted.Value !== undefined) {
				currentValue = persisted.Value
				set(persisted.Value)
			}
		})
	} catch (error) {
		console.warn(error)
	}
	function persistentSet(value) {
		currentValue = value
		set(value)
		try {
			putUserPreference(key, value)
		} catch (error) {
			console.warn(error)
		}
	}
	function persistentUpdate(fn) {
		persistentSet(fn(currentValue))
	}
	return {
		subscribe,
		set: persistentSet,
		update: persistentUpdate,
	}
}

By intercepting calls to the store, we are able to do additional work before passing execution to the store underneath (the one we created when we called writable).

We get the usual behaviour of the store, as well as calls out to our own persistence functions.

Any errors that occur are warned to the console, but we don’t worry too much; the store will continue to work as normal, it’s just the persistence that has failed. At least it gracefully degrades on browsers without IndexedDB support.

Persisting values

The getUserPreference and putUserPreference functions make up a promise based API that persists, and looks up values by a key.

These days developers have lots of options when it comes to storing data in the browser. We built our solution on top of the IndexedDB browser API using Dexie.js out of a technical curiosity, and are pretty happy with it.

// create a database
let userPreferencesDB = new Dexie('pace-user-preferences')
userPreferencesDB.version(1).stores({
	'user_preferences': '[key],value',
})

export function putUserPreference(key, value) {
	userPreferencesDB['user_preferences'].put({
		key: key,
		value: value,
	})
}

export function getUserPreference(key) {
	return userPreferencesDB['user_preferences'].get([key])
}

You could easily write different putUserPreference and getUserPreference implementations to persist the values elsewhere - even on a remote server if you want to sync across devices.

Using our new API

The persistable function mirrors Svelte’s stores API, which makes it a drop-in replacement for any calls to writable.

To make a store persist, all we need to do is create it with a call to persistable instead, passing in a unique string key for each one.


Learn more about what we're doing at Pace.

A lot of our blog posts come out of the technical work behind a project we're working on called Pace.

We were frustrated by communication and project management tools that interrupt your flow and overly complicated workflows turn simple tasks, hard. So we decided to build Pace.

Pace is a new minimalist project management tool for tech teams. We promote asynchronous communication by default, while allowing for those times when you really need to chat.

We shift the way work is assigned by allowing only self-assignment, creating a more empowered team and protecting the attention and focus of devs.

We're currently live and would love you to try it and share your opinions on what project management tools should and shouldn't do.

What next? Start your 14 day free trial to see if Pace is right for your team


First published on 9 Sep 2020 by Mat Ryer
#sveltejs #javascript #typescript

or you can share the URL directly:

https://pace.dev/blog/2020/09/09/simple-user-preferences-with-persisted-stores-svelte.html

Thank you, we don't do ads so we rely on you to spread the word.

https://pace.dev/blog/2020/09/09/simple-user-preferences-with-persisted-stores-svelte.html


You might also like:

Respond to Ctrl+C interrupt signals gracefully #Golang #Patterns

Context-aware io.Reader for Go #Tech #Golang

Grouper component for Svelte #Tech #Svelte

Subscribe:
Atom RSS JSON