If you like fast JavaScript apps, you’ll love Svelte. It works a bit differently than some of the other frameworks that you may be familiar with. Instead of shipping a large runtime, Svelte is compiled. This means the code you end up with is optimized and dramatically reduced in size.
Before we look at the code, I’ll mention that I'm not a Svelte expert. I've been reading and writing about it quite a bit, and I decided it was high time to build something. So, this is that something. It's possible there are better ways to do things than what you'll see here, but I think anyone wanting to get started with Svelte will have a leg up after reading this article.
The bulk of what follows is a walkthrough of the code in this GitHub repository that I made to accompany the article. We’ll go file by file and talk about what’s going on with the application and how it’s wired together.
The app displays quotes from the Quotes by Design public API, and is built using four tools — Svelte, Express, webpack and Babel. So you have an idea of the end product, here's what the app looks like when it's up and running. You can click to see a larger image.
If you haven’t used Svelte before, I think you’re going to be pretty damn happy with it. I found the experience to be great.
The Code
Let’s start with a quick review of the package.json
file. This is where the app's dependencies are listed, as well as the scripts used in the build process.
package.json
{
"name": "svelte-quotes",
"version": "1.0.0",
"description": "A demo app of Svelte that displays quotes",
"main": "src/index.js",
"scripts": {
"build": "webpack",
"server": "node server.js",
"start": "npm run build & npm run server",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"svelte",
"sveltejs",
"javascript"
],
"author": "John Hannah",
"license": "MIT",
"dependencies": {
"express": "^4.16.3",
"svelte": "^1.59.0"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
"html-webpack-plugin": "^3.1.0",
"svelte-loader": "^2.5.1",
"webpack": "^4.4.1",
"webpack-cli": "^2.0.13"
}
}
If you’ve used other JavaScript frameworks, this list of dependencies will look familiar. Most of them are used to transpile and bundle up the code. On lines 7-9 we have the scripts that build the app and fire up the web server.
Now let’s check out the webpack config.
webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: ['babel-polyfill', './src/index.js'],
output: {
path: path.resolve(__dirname, 'public'),
filename: 'app.js',
publicPath: '/'
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: './src/index.html'
})
],
module: {
rules: [
{
test: /\.js?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
babelrc: false,
presets: [['env', { targets: { browsers: ['last 2 versions'] } }]]
},
},
},
{
test: /\.svelte$/,
exclude: /node_modules/,
use: 'svelte-loader'
}
]
}
};
Notice the inclusion of babel-polyfill
on line 7. The app needs to get data from the API and to do that I’ve chosen to use the Fetch API and the new async/await syntax of JavaScript. Using babel-polyfill
helps with the latter. It's a nice new feature of the language when working with async operations like data fetching.
The second thing to note is on line 34, the use of svelte-loader
. This is what webpack uses to compile the Svelte code to JavaScript so it will work in browsers. Notice that it’s for files with a .svelte extension. Many of the examples in the Svelte documentation use files with a .html extension, but as we’ll see, that’s not ideal for this app.
Now’s let’s take a look at the code for Express:
server.js
const express = require('express');
const path = require('path');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.listen(8080, () => {
console.log('App started and available at http://localhost:8080');
});
Express is used to provide a web server for the application. You’ll see on line 6 that it uses express.static
. This means it's serving a static HTML file to users (which we’ll review in a moment).
This is the reason all the Svelte files use the .svelte file extension — I don’t want the HTML file to be processed by webpack and having a different file extension helps webpack tell them apart. Aside from this practical consideration, it’s simply a clearer distinction for me while I’m working to use a separate file extension for Svelte code.
Associating .svelte Files with HTML Syntax
A quick aside here about happily using the .svelte extension with your editor of choice. Most editors don’t yet recognize the .svelte extension, so you’ll need to associate it with HTML syntax to get all the nice code completion and highlighting you expect. Luckily, this is quick work.
You can see how to make the change in VS Code here, for Atom here and for Sublime here. If you use another editor, just Google it. It’s a fast, straightforward task that is definitely worth doing.
Next, let's take a look at that HTML file I was just talking about.
index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Svelte Quotes</title>
</head>
<body>
<main></main>
</body>
</html>
There isn’t a lot to see, but it’s an important file. When we build our app, webpack takes this file and moves it to a directory named public. Webpack also adds our JavaScript file for us. The config for this process is on line 14 of webpack.config.js and uses the HtmlWebpackPlugin.
To see a bit more about how this works, you can follow the instructions for installing and starting the app that are found on the repo. Once it’s done building, go to the public folder and notice that the app.js file has been added near the botttom of index.html.
Alright, we’re ready to dig into the main code for our Svelte app. The starting point is index.js:
index.js
import App from './components/App.svelte';
const app = new App({
target: document.querySelector('main'),
data: {
quotes: []
},
});
This is the file that webpack is configured to use as the entry point of the application (line 6 of webpack config). On line 1, there is the import of the root component, App
. It's initialized starting on line 3, and is passed an object with a couple of properties.
The first, target
, tells Svelte which element in our index.html it should work with, in this case, main
.
The second property is data
. This is where the default state for the App
component is set.
Now let’s look at the next file, the heart 💖 of the application, App.svelte
. There's a lot going on, but we'll break it down into bit-sized chunks after we see the code.
App.svelte
<div id="app">
<h1>Great Quotes</h1>
<p style="margin: 0 0 30px">Curated by Chris Coyier</p>
{{#if quotes.length === 0}}
<p>Quotes loading...</p>
{{else}}
<Quotes quotes={{quotes}} />
{{/if}}
<button on:click='loadMoreQuotes()'>Load More</button>
</div>
<script type="text/javascript">
import Quotes from './Quotes.svelte';
// async data fetching function
const fetchQuotes = async (data, component) => {
const response = await fetch(`http://quotesondesign.com/wp-json/posts?filter[order]=rand&filter[posts_per_page]=3`);
const json = await response.json();
const quotes = data.quotes.concat(json);
component.set({ quotes });
}
// export the default object
export default {
oncreate() {
let data = this.get();
fetchQuotes(data, this);
},
methods: {
loadMoreQuotes() {
let data = this.get();
fetchQuotes(data, this);
}
},
components: {
Quotes
}
};
</script>
<style>
<!-- Plain old CSS here -->
</style>
This is a single file component, and it’s one of the things I like best about Svelte. If you're thinking it reminds you a bit of Vue, I agree. Notice there are three sections to this file: the markup, the script and the styles. We’ll go through them one by one.
Markup
The markup is at the top of the file. For reference, here it is broken out from the rest of the code:
<div id="app">
<h1>Great Quotes</h1>
<p style="margin: 0 0 30px">Curated by Chris Coyier</p>
{{#if quotes.length === 0}}
<p>Quotes loading...</p>
{{else}}
<Quotes quotes={{quotes}} />
{{/if}}
<button on:click='loadMoreQuotes()'>Load More</button>
</div>
This is just regular HTML, with some additional support for tags, triples, and logic statements. Those are the bits in curly braces and they allow for adding and working with data inside the markup. Although the braces may remind you of the Moustache syntax Vue uses, they are actually JavaScript expressions.
Notice on line 4 we have this:
{{#if quotes.length === 0}}
When the app first loads, the quotes are not yet available, so we have some logic to display a loading message. Once the quotes have arrived, we pass them to the Quotes
component, as you see on line 7.
<Quotes quotes={{quotes}} />
And finally we have a button that allows us to add more quotes to the list:
<button on:click='loadMoreQuotes()'>Load More</button>
Notice the on:click
attribute. This is a directive. Svelte uses directives for event handlers and refs, among other things. In this case, it handles the click event on the button. When a user clicks the button, a function called loadMoreQuotes
is called, which, as you may guess, loads more quotes!
Script
There is a lot going on here. First, notice that this is JavaScript code wrapped in a standard <script>
tag. Not reinventing the wheel is a big part of the Svelte philosophy and you’ll find that a lot of things stay close to familiar conventions.
<script type="text/javascript">
import Quotes from './Quotes.svelte';
// async data fetching function
const fetchQuotes = async (data, component) => {
const response = await fetch(`http://quotesondesign.com/wp-json/posts?filter[order]=rand&filter[posts_per_page]=3`);
const json = await response.json();
const quotes = data.quotes.concat(json);
component.set({ quotes });
}
// export the default object
export default {
oncreate() {
let data = this.get();
fetchQuotes(data, this);
},
methods: {
loadMoreQuotes() {
let data = this.get();
fetchQuotes(data, this);
}
},
components: {
Quotes
}
};
</script>
On line 2, the Quotes
component is imported so that it can be used in the current component, which is App
.
Next, we have a data fetching function called fetchQuotes
. It’s an async fuction and if the syntax looks unfamiliar, check out this nice discussion of async/await in JavaScript.
We then see the default export on line 13 which is a JavaScript object with some properties and a method on it. When App
was imported in index.js, this is what was received.
First we see the oncreate
method:
oncreate() {
let data = this.get();
fetchQuotes(data, this);
}
This is a built-in Svelte lifecycle method or hook. I've understood it to be roughly analogous to React’s componentDidMount
. By the way, Svelte has two lifecycle hooks — oncreate
and ondestroy
.
Inside oncreate
, a variable called data
is set to the value of this.get()
. The get
method is part of the Component API and it's used to retrieve component state. In this case, the value is the empty array that was added when the component was initialized.
On line 3, is the call to fetchQuotes
, and it's passed two parameters — data
, which was just set, and this
, which is a reference to the current component, App
.
Let’s take a closer look at the code for fetchQuotes
:
const fetchQuotes = async (data, component) => {
const response = await fetch(`http://quotesondesign.com/wp-json/posts?filter[order]=rand&filter;[posts_per_page]=3`);
const json = await response.json();
const quotes = data.quotes.concat(json);
component.set({ quotes });
}
Lines 2-4 fetch and process the data. On line 5 is the call, component.set()
, which is also a part of the Component API and it sets values in state.
The quotes that are received from the API are passed in and Svelte updates the state of quotes so that it now holds an array of three quotes. Technically, I guess the array holds three objects that contain our quote data.
Quick aside here on state in Svelte. Component state works great for a lot of cases. However, if the app grew and needed something more, Svelte comes with a global store that is analogous to Redux or MobX in React. One feature I really like is that the API for managing component and global state is the same. It's comprised of just three methods — set
, get
and observe
(which isn't used in this app).
Next up, we have the methods
property:
methods: {
loadMoreQuotes() {
let data = this.get();
fetchQuotes(data, this);
}
},
The methods
property is provided by Svelte for declaring your own methods. In the case of this app, there needs to be a function that will go out and fetch some more quotes when someone clicks the “Load More” button, and that’s what’s going on here.
It’s very similar to what was happening in oncreate
except that the value of data
will be an array with three quotes the first time a user clicks the button (because they were fetched in oncreate
which runs right after the component is created). The rest is the same!
Finally, we come to the components
property:
components: {
Quotes
}
The Quotes
component loops over the array of quotes, so it needs to be declared here. If the app grew and more components were imported into App
, they'd need to be listed here as well.
Styles
Finally, the styles! I didnt include the code because it’s just plain old CSS. However, Svelte does some interesting things with it. First of all, the CSS is scoped to the component. So, if you add some styling for a <p>
tag, for example, it will not be global, but instead scoped to the component.
Another interesting point is that the CSS isn’t bundled up and put into a separate CSS file. Instead, it’s injected directly into the document <head>
. This is great for a couple of reasons — no render blocking CSS that slows the app down, plus one less HTTP request.
I should mention that Svelte does support adding global styles. You can read about that in the Svelte docs on scoped styles.
Now, if you’re familiar with React, this may sound a bit like CSS-in-JS and it does have some similarities. However, working with Svelte single file components is much nicer in my experience. It’s just plain CSS. There’s no config or unfamiliar conventions, it just works.
OK, now on to the last file, Quotes.svelte:
{{#each quotes as quote}}
<div class="quote">
{{{quote.content}}}
<p class="author">- {{quote.title}}</p>
</div>
{{/each}}
I'd like to point out the triple curly braces that surround quotes.content
. There are three because the API I used returns the quotes wrapped in <p>
tags, and I wanted them evaluated as HTML. If I had used double curly braces, the <p>
tags would have printed as part of the quote. This functionality is similar to dangerouslySetInnerHTML
in React.
And that's the code! 🙂
You Should Try Svelte!
So, Svelte is pretty awesome, right? I love it for a number of reasons. First of all, it’s fast as hell, and that is arguably the most important quality an app can have.
Second, single file components are a pleasure to work with. I didn't appreciate them fully until I started using them.
Lastly, there are a lot of little things that make it a joy to work with. The "triples" syntax above is one example. It's so simple. The API for state management is simple. These little things add up to make for a really good experience.
And despite being a new framework, Svelte has a nice community building up around it. As I mentioned, Rich Harris created Svelte. He has other very successful projects and to me, that experience shows in the way he interacts in the issue queues. It’s a project that has a good feel to it.
Well, that's it for this week. I’ll no doubt be writing more on Svelte in the future. I encourage you to give it a try. You'll enjoy the experience and your users will appreciate the lightning fast app you've built for them!
If you liked this post, sign up for my weekly newsletter. I curate the best JavaScript writing from around the web and deliver it to readers every Thursday. The sign up form is right below this article.
Until next time, happy coding…