In this start-to-finish tutorial, we'll build a production-grade to-do app with Userbase, a new BaaS offering, and Svelte, a fantastic way to build web apps.
This article assumes you have some familiarity with web development, but little to no familiarity with Userbase or Svelte.
We'll start with some background on these technologies, and then dive into the process.
What is Userbase
Userbase just launched as a wild new backend-as-a-service offering which keeps user data encrypted end-to-end, and sports a dramatically simple API.
It allows you to build and ship simple HTML, CSS, and Javascript - an entirely static website - with production-grade user authentication and data storage, without having to worry about any of the backend.
What is Svelte
Svelte has been around for some time, and remains my favorite way to build front-end apps.
Imagine if you had a language where you could declare not just what a variable's value should be, but instead how it should be computed. What if that declaration then kept the value up-to-date at all times, and updated anything that depended on it at the same time.
var values = []
store.subscribe((newValues) => values = newValues)
dynamic var count = values.length
...
<div id="count">{count}</div>
Imagine if our dynamic var
is always updated automatically when values
is updated. And imagine if our div
's contents were then, also, automatically updated.
Wild, right?
Well, that's what Svelte is. Technically its own language that operates within the confines of Javascript syntax, along with the compiler that converts that language into plain Javascript which automatically keeps everything in your app up-to-date as state changes occur.
It's the best of both worlds: a declarative syntax like we enjoy in React, but with the efficiency of no framework (not only do Svelte apps run faster, users don't have to unwittingly download massive amounts of Javascript. Imagine using a UI library in Svelte; it's a dev dependency! Only what you actually use ever gets included in what is sent to users' browsers).
Let's start building
Seriously, this should take less than an hour. Here we go.
Step 1: Create a Userbase account
Head to Userbase.com, click "Try it free", and set up a free account.
It'll start you off with a trial app. Keep this window open, we'll use the "App ID" soon.
Step 2: Create a Svelte project
Install npm on your machine if you haven't before.
In your terminal, run this command to prepare and run a starter Svelte app:
npx degit sveltejs/template TodoApp
cd TodoApp
npm install
npm run dev
That last line, npm run dev
is what you call from the TodoApp directory whenever you want to run your app for development purposes.
Open http://localhost:5000
to see the starter Svelte app running.
Step 3: Build Userbase access into a Svelte store
Svelte has a fantastic stores feature that we'll use here to interact with our user's accounts and data in Userbase.
If you've got Atom installed, you can run atom ./
in a new terminal tab to open the code editor loaded with the TodoApp project files.
3.1 Include the Userbase JS SDK
Open TodoApp/public/index.html
, and include Userbase's JS SDK above the bundle.js line:
<script type="text/javascript" src="https://sdk.userbase.com/1/userbase.js"></script>
The file should now look like this:
TodoApp/public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte app</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'>
<script type="text/javascript" src="https://sdk.userbase.com/1/userbase.js"></script>
<script defer src='/build/bundle.js'></script>
</head>
<body>
</body>
</html>
3.2 Create the Svelte stores for accessing Userbase
Inside TodoApp/src
, a directory already made for you, create a new directory called Stores
. We're going to create two Svelte stores inside that directory.
Create the UserAccount
store
Inside TodoApp/src/Stores
, create a new file called UserAccount.js
. Paste the following into it:
TodoApp/src/Stores/UserAccount.js
import { writable } from 'svelte/store';
/** STATE **/
/// This simply defines the initial state, and serves as a guide for the structure
const state = {
initialized: false,
signedIn: false,
username: undefined,
error: undefined
}
/** ACTION CREATORS **/
/// These are the only things exposed to the outside world
const actionCreators = (store) => {
return {
initialize: () => {
userbase.init({ appId: 'YOUR_APP_ID_HERE' })
.then((session) => {
if (session.user) {
store.dispatch(SignedIn(session.user.username))
} else {
store.dispatch(SignedOut())
}
})
.catch((e) => store.dispatch(Error(e)))
.finally(() => store.dispatch(Initialized()))
},
signUp: (username, password, rememberMe) => {
userbase.signUp({ username, password, rememberMe: rememberMe ? 'local' : '' }) /* TODO: Check into rememberMe functionality */
.then((user) => store.dispatch(SignedIn(username)))
.catch((e) => store.dispatch(Error(e)))
},
signIn: (username, password, rememberMe) => {
userbase.signIn({ username, password, rememberMe: rememberMe ? 'local' : '' }) /* TODO: Check into rememberMe functionality */
.then((user) => store.dispatch(SignedIn(username)))
.catch((e) => store.dispatch(Error(e)))
},
signOut: () => {
userbase.signOut()
.then(() => store.dispatch(SignedOut()))
.catch((e) => store.dispatch(Error(e)))
}
}
}
/** ACTIONS **/
/// These actions are those passed to the reducer by action creators. They are not exported out of this file.
const Initialized = () => ({ name: "Initialized" })
const SignedIn = (username) => ({ name: "SignedIn", username })
const SignedOut = () => ({ name: "SignedOut" })
const Error = (error) => ({ name: "Error", error })
/** REDUCER **/
/// Receives internal actions as sent by action creators
const reducer = (state, action) => {
switch (action.name) {
case "Initialized":
return { ...state,
initialized: true
};
case "SignedIn":
return { ...state,
username: action.username,
signedIn: true,
error: undefined
};
case "SignedOut":
return { ...state,
username: undefined,
signedIn: false,
error: undefined
};
case "Error":
return { ...state,
error: action.error
}
default:
return state;
}
};
/** FLUX STORE **/
/// A simple flux store that allows dispatching to update the state in a svelte store with a reducer
class Store {
constructor(subscribe, updateState, reducer) {
this.updateState = updateState
this.reducer = reducer
this.unsubscribe = subscribe(newState => this.state = newState)
}
dispatch(action) {
this.updateState(s => this.reducer(s, action))
}
}
/** SVELTE STORE **/
function createUserAccountStore() {
const { subscribe, update, set } = writable(state);
const store = new Store(subscribe, update, reducer)
const actions = actionCreators(store)
return {
...actions,
subscribe
};
}
export const userAccountStore = createUserAccountStore();
Make sure to replace YOUR_APP_ID_HERE
with the app ID from Userbase
This code produces a Svelte store which allows the code that uses it to take advantage of all the first-class features of Svelte stores (we'll see all that in just a minute).
Internally, it updates its state with an architecture resembiling a Flux store for better maintainability.
3.3 Create the Todos
store
Inside TodoApp/src/Stores
, create a new file called Todos.js
. Paste the following into it:
TodoApp/src/Stores/Todos.js
import { writable } from 'svelte/store';
/** STATE **/
/// This simply defines the initial state, and serves as a guide for the structure
const state = {
initialized: false,
todos: []
}
const todo = {
title: '',
complete: false
}
/** ACTION CREATORS **/
/// These are the only things exposed to the outside world
const actionCreators = (store) => {
return {
initialize: () => {
userbase.openDatabase({
databaseName: 'todos',
changeHandler: (todos) => store.dispatch(TodosLoaded(todos))
})
.catch((e) => store.dispatch(Error(e)))
.finally(() => store.dispatch(Initialized()))
},
createTodo: (item) => {
item.title = item.title || ""
userbase.insertItem({
databaseName: 'todos',
item
})
.catch((e) => store.dispatch(Error(e)))
},
updateTodo: (todo, item) => {
userbase.updateItem({ databaseName: 'todos', itemId: todo.itemId, item: {
...todo.item,
...item
}})
.catch((e) => store.dispatch(Error(e)))
},
deleteTodo: (itemId) => {
userbase.deleteItem({ databaseName: 'todos', itemId: itemId })
.catch((e) => store.dispatch(Error(e)))
}
}
}
/** ACTIONS **/
/// These actions are those passed to the reducer by action creators. They are not exported out of this file.
const Initialized = () => ({ name: "Initialized" })
const TodosLoaded = (todos) => ({ name: "TodosLoaded", todos })
const Error = (error) => ({ name: "Error", error })
/** REDUCER **/
/// Receives internal actions as sent by action creators
const reducer = (state, action) => {
switch (action.name) {
case "Initialized":
return { ...state,
initialized: true
};
case "TodosLoaded":
return { ...state,
todos: action.todos
};
case "Error":
return { ...state,
error: action.error
}
default:
return state;
}
};
/** FLUX STORE **/
/// A simple flux store that allows dispatching to update the state in a svelte store with a reducer
class Store {
constructor(subscribe, updateState, reducer) {
this.updateState = updateState
this.reducer = reducer
this.unsubscribe = subscribe(newState => this.state = newState)
}
dispatch(action) {
this.updateState(s => this.reducer(s, action))
}
}
/** SVELTE STORE **/
function createTodosStore() {
const { subscribe, update, set } = writable(state);
const store = new Store(subscribe, update, reducer)
const actions = actionCreators(store)
return {
...actions,
subscribe
};
}
export const todosStore = createTodosStore();
This also produces a Svelte store which allows the code that uses it to take advantage of all the first-class features of Svelte stores.
Just like the UserAccount
store, it also updates its internal state with an architecture resembling a Flux store for better maintainability.
Step 4: Build the app interface
Since Userbase is taking care of all the backend needs of our app, we primarily only need to build the interface.
4.1 The fundamentals
Open TodoApp/src/App.svelte
. This is where we are going to begin building our app's interface.
You can see this file already has some code in it. Let's go ahead and replace that with this:
TodoApp/src/App.svelte
<script>
// Library imports
import { onMount } from 'svelte';
// Component imports
import SignUp from './SignUp.svelte';
import SignIn from './SignIn.svelte';
import TodoList from './TodoList.svelte';
// State
import { userAccountStore } from './Stores/UserAccount.js';
$: initialized = $userAccountStore.initialized
$: signedIn = $userAccountStore.signedIn
$: username = $userAccountStore.username
$: error = $userAccountStore.error
// Events
onMount(() => {
userAccountStore.initialize()
})
function signOut() {
userAccountStore.signOut()
}
</script>
<main>
<h1 id="logo">TODO</h1>
<div id="content">
{#if initialized}
{#if signedIn}
<TodoList />
{:else}
<SignIn />
<div class="divider"></div>
<SignUp />
{/if}
{:else}
Loading...
{/if}
{#if error}<div id="error">{error}</div>{/if}
</div>
{#if signedIn}
<div id="logout">
<button on:click={signOut}>Logout {username}</button>
</div>
{/if}
</main>
<style>
main {
text-align: center;
padding: 50px 0;
}
#content {
padding: 20px;
max-width: 360px;
margin: 0 auto;
background: #FFFFFF;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.05), 0 12px 50px 0 rgba(0,0,0,0.15);
border-radius: 12px;
}
#logout {
margin-top: 20px;
}
h1 {
color: #ff3e00;
text-transform: uppercase;
font-size: 4em;
font-weight: 100;
margin: 20px 0;
}
.divider {
border-top: 1px dotted rgba(0,0,0,0.25);
margin: 50px 0px;
}
</style>
This is all pretty straightforward, but a few notes:
onMount
is one of Svelte's lifecycle methods (more here). There are a number of them you can use to run code at specific points in the component's lifecycle.
$:
is how you declare a variable which should automatically stay up to date (instead of var
, let
, or const
). Note: there is even more than just declarations that $:
can do, more on that here.
$anySvelteStore
automatically subscribes to that store (and unsubscribes from it when the component is destroyed), providing its value wherever used.
So putting those two together:
$: username = $userAccountStore.username
This line will always ensure that username is up-to-date with the latest value from our UserAccount store to give us a currently signed-in user's username.
Finally, in Svelte, "HTML" can also contain other Svelte components, as well as variables from your <script>
section.
So we can use that always-updated username
value in our Svelte HTML:
<button on:click={signOut}>Logout {username}</button>
The button will always, automatically reflect the user's current username, even if it changes. Automatically!
Now we did use a number of other Svelte components which don't actually exist yet, so you'll see in your first terminal tab that the compiler is showing an error. Let's fix that by building out the rest of the app's interface.
4.2 The other components
Now let's build out our other components. We're going to create five custom Svelte components. For each, create the new file with the right file name and paste the contents into it.
TodoApp/src/SignIn.svelte
<script>
import { userAccountStore } from './Stores/UserAccount.js';
var username;
var password;
function handleSignIn(e) {
e.preventDefault()
userAccountStore.signIn(username, password, true) // TODO: In future versions, only pass rememberMe as true if the user checks a box
}
</script>
<!-- HTML -->
<h1>Sign In</h1>
<form id="signin-form" on:submit={handleSignIn}>
<input id="signin-username" type="text" required placeholder="Username" bind:value={username}>
<input id="signin-password" type="password" required placeholder="Password" bind:value={password}>
<input type="submit" value="Sign in">
</form>
{#if $userAccountStore.error}
<div id="error">{$userAccountStore.error}</div>
{/if}
<style>
#signin-form input {
display: block;
margin: 10px auto;
}
</style>
TodoApp/src/SignUp.svelte
<script>
import { userAccountStore } from './Stores/UserAccount.js';
var username;
var password;
function handleSignUp(e) {
e.preventDefault()
userAccountStore.signUp(username, password, true) // TODO: In future versions, only pass rememberMe as true if the user checks a box
}
</script>
<!-- HTML -->
<h1>Create an account</h1>
<form id="signup-form" on:submit={handleSignUp}>
<input id="signup-username" type="text" required placeholder="Username" bind:value={username}>
<input id="signup-password" type="password" required placeholder="Password" bind:value={password}>
<input type="submit" value="Create an account">
</form>
{#if $userAccountStore.error}
<div id="error">{$userAccountStore.error}</div>
{/if}
<style>
#signup-form input {
display: block;
margin: 10px auto;
}
</style>
TodoApp/src/TodoList.svelte
<script>
// Library imports
import { onMount } from 'svelte';
// Component imports
import TodoRow from './TodoRow.svelte';
// State
import { todosStore } from './Stores/Todos.js';
$: initialized = $todosStore.todos
$: error = $todosStore.error
$: todos = $todosStore.todos || []
let newTodoTitleValue;
// Events
onMount(() => {
todosStore.initialize()
})
function addNewTodo(e) {
e.preventDefault();
todosStore.createTodo({
title: newTodoTitleValue
})
newTodoTitleValue = ""
}
</script>
<!-- HTML -->
<div id="todoList">
{#each todos as todo, index (todo.itemId)}
<TodoRow todo={todo} />
{/each}
<form id="addTodo" on:submit={addNewTodo}>
<input id="addTodoInput" type="text" required placeholder="Add a new todo" bind:value={newTodoTitleValue}>
<input type="submit" value="Add">
</form>
</div>
{#if !initialized}
<div id="loading">Loading to-dos...</div>
{/if}
{#if error}
<div id="error">{error}</div>
{/if}
<style>
#todoList {
text-align: left;
font-size: 14px;
}
form#addTodo {
margin-top: 20px;
display: flex;
}
form#addTodo #addTodoInput {
flex-grow: 1;
margin-right: 5px;
}
</style>
TodoApp/src/TodoRow.svelte
<script>
// Props
export let todo;
// Library imports
import { afterUpdate } from 'svelte';
// Component imports
import Checkbox from './Checkbox.svelte';
// Dynamic state
import { todosStore } from './Stores/Todos.js';
$: title = todo.item.title
$: complete = todo.item.complete
// Events
function toggledCheckbox(e, todo) {
e.preventDefault()
todosStore.updateTodo(todo, { complete: !todo.item.complete })
}
</script>
<!-- HTML -->
<div class={complete ? "todo complete" : "todo"} on:click>
<Checkbox value={complete ? "checked" : "unchecked"} on:click={(e) => {toggledCheckbox(e, todo)}} />
<p>{title}</p>
</div>
<style>
.complete {
opacity: 0.5;
}
.todo p {
padding: 6px 0;
padding-left: 22px;
margin: 0;
font-size: 16px;
line-height: 20px;
}
</style>
TodoApp/src/Checkbox.svelte
<script>
// Props
export let value;
</script>
<!-- HTML -->
{#if value == "none"}
<div class="noCheckbox"></div>
{:else}
<div class="container" on:click>
{#if value == "checked"}<input type="checkbox" checked="checked">{:else}<input type="checkbox">{/if}
<span class="checkmark"></span>
</div>
{/if}
<style>
.noCheckbox {
min-width: 22px;
flex-grow: 0;
}
.container {
min-width: 22px;
flex-grow: 0;
display: block;
position: relative;
}
.container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
border-radius: 3px;
position: absolute;
top: 8px;
left: 0;
height: 14px;
width: 14px;
border: 1px solid rgba(142,161,174,0.5);
transition: all 0.2s ease;
}
.container:hover input ~ .checkmark {
border: 1px solid rgba(142,161,174,1);
}
.container input:checked ~ .checkmark {
border: 1px solid transparent;
background-color: #2196F3;
}
.checkmark:after {
content: "";
position: absolute;
display: none;
}
.container input:checked ~ .checkmark:after {
display: block;
}
.container .checkmark:after {
left: 5px;
top: 3px;
width: 2px;
height: 5px;
border: solid white;
border-width: 0 2px 2px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
</style>
A few notes on all of this code:
SignIn
and SignUp
repeat the same CSS. This is because Svelte keeps CSS from affecting any components outside of the one it is declared within. This helps you avoid all kinds of odd issues that can come up from the cascading part of CSS.
To avoid repeating CSS that you actually want to be global, put your global styles in /public/global.css
.
But better yet, since the SignIn
and SignUp
components are so similar, you may want to simply combine them into one component, depending on how you would want your app's user authentication interface to work.
You can see our use of props
which we pass to our custom components TodoRow
and Checkbox
. This is how state moves down into children components.
You can see our use of bindings
in TodoList.svelte
:
<input id="addTodoInput" type="text" required placeholder="Add a new todo" bind:value={newTodoTitleValue}>
By putting in bind:value={newTodoTitleValue}
, we are creating a two-way binding with the variable newTodoTitleValue
. This is how we get state into a parent component from a child component, and how we might update that child component with a new value. You can see both of these things happening in the addNewTodo
function, where we first use the value of newTodoTitleValue
, which is what the user has typed in, and then we set the value of newTodoTitleValue
to an empty string, which clears the input field.
Finally, Checkbox
dispatches its own event by forwarding the on:click
event to one of its divs. For how to make custom component events, see more here.
Other than that, everything else largely follows the conventions we've already discussed.
4.3 Run your app
With that, your app should compile and run! If the compiler doesn't automatically attempt to recompile, hit CTRL+C
in terminal, and npm run dev
again.
Once it has compiled, you can load up your app at http://localhost:5000/
, create an account, log out, sign back in, create tasks, and mark them as complete. If you refresh your browser window, you'll see that everything is persisted in your account, and if you open a second browser window and add or complete tasks, you'll see the first browser window automatically stay up to date.
This magic is all available thanks to Userbase's brilliant design and simple SDK, hooked into a lot of Svelte's magic which makes this starter app a fantastic foundation which can scale into a significantly more complex app without significantly more complex code.
Next steps
For a deeper understanding of Svelte, run through their fantastic tutorial at https://svelte.dev/tutorial/basics.
For a deeper understanding of Userbase, run through their Quickstart or read their docs at https://userbase.com/docs/.
With just these two resources, you can scale up to a very sophisticated production-ready app in very little time.
Happy creating!