Create a Solana dApp from scratch

Scaffolding the frontend

Episode 7
5 months ago
14 min read

Whilst users can technically start sending and reading tweets using our program by interacting directly with the blockchain, no one really wants that sort of user experience.

We want to abstract all of that into a nice user interface (UI) that resembles what they are familiar with. For that reason, we will build a frontend client and we'll build it using VueJS.

We'll use VueJS because A. it's my favourite JavaScript framework and B. it is very much under-documented in the Solana ecosystem. If you're more familiar with other JavaScript frameworks such as React, you can still follow along as most concepts resonate between frameworks.

Now, frontend development is a world of its own and I could easily spend hours and hours detailing how to create the UI we will end up with at the end of this episode. However, the focus of this series is Solana and I wouldn't want to deviate too much from it. There are plenty of tutorials out there about frontend development — even on this blog.

At the same time, we need a UI to continue our journey and create our decentralised application. So here's the deal. In this episode, I'll explain how to get started with VueJS and install all the dependencies we’ll need so you can do it yourself. Then, when it comes to the actual design and components of the UI, I'll give you a bunch of files to copy/paste in various places and briefly explain what they do. The components will contain mock data at first so we can wire them with our Solana program in the next episodes.

Fasten your seatbelt because we're going to move quickly. Let's go! 🏎

Install Vue CLI

One of the easiest ways to create a new VueJS application is to use its CLI tools.

If you don’t have them already installed, you can do this by running the following.

npm install -g @vue/[email protected]

Note that we’re explicitly asking for version 5 — which is still a release candidate at the time of writing — because we want our VueJS app to be bundled with Webpack 5 instead of Webpack 4.

You can check the VueJS CLI tools are installed properly by running:

vue --version

Create a new Vue app

We can now create a new VueJS app by running vue create followed by the directory that should be created for it.

We want our frontend client to live under the app directory which is currently an empty folder. Therefore, we’ll also need to use the --force option to override it. Okay, let’s run this.

vue create app --force

You should now be asked to choose a preset for your app. We’ll be using Vue 3 in this series so let’s select the Default (Vue 3) preset.

? Please pick a preset:
  Default ([Vue 2] babel, eslint)
❯ Default (Vue 3) ([Vue 3] babel, eslint)        # <- This one.
  Manually select features

And just like that we’ve got ourselves a VueJS 3 application inside our project.

Let’s cd into it as we’ll be working inside that directory for the rest of this episode.

cd app

Install Solana and Anchor libraries

Next, let’s install the JavaScript libraries provided by Solana and Anchor. We mentioned in a previous episode that they were already included in our Anchor project for our tests but this is a different environment with its own dependencies so we need to install them explicitly.

Be sure to be inside the app directory and run the following.

npm install @solana/web3.js @project-serum/anchor

Configure node polyfills

The frontend world is full of quirks and gotchas and here’s one that I struggle with when creating this series.

Some of the JavaScript libraries we’ll be using in our app depend on Node.js polyfills.

Node.js is basically “JavaScript for servers” and the purpose of Node.js polyfills are to bring some of its core dependencies into the frontend world. That way, the same code can be used on both side.

For instance, remember how we converted a string into a buffer by using Buffer.from('some string')? We didn’t need to import that Buffer object because it’s a Node.js core dependency that was polyfilled for us.

Currently, the frontend world is moving away from bundling all these Node.js dependencies by default. And that’s exactly what Webpack did when they released version 5. Here’s a very good explanation from their documentation:

In the early days, webpack's aim was to allow running most Node.js modules in the browser, but the module landscape changed and many module uses are now written mainly for frontend purposes. Webpack <= 4 ships with polyfills for many of the Node.js core modules, which are automatically applied once a module uses any of the core modules (i.e. the crypto module).

Webpack 5 stops automatically polyfilling these core modules and focus on frontend-compatible modules. Our goal is to improve compatibility with the web platform, where Node.js core modules are not available.

So that’s a nice change but, as I said earlier, some of our dependencies rely on these polyfills to exist. If we don’t do anything we will end up with the following error when compiling our frontend.

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

Fortunately for us, there is a way to fix this issue by adding the polyfills we need back and/or telling Webpack we don’t need them so it can stop complaining.

In our case, we’ll only need the Buffer polyfill and we can disable the others that would have otherwise failed. We can do this inside our vue.config.js file which contains a configureWebpack property allowing us to provide additional Webpack configurations.

const webpack = require('webpack')
const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
    transpileDependencies: true,
    configureWebpack: {
        plugins: [
            new webpack.ProvidePlugin({
                Buffer: ['buffer', 'Buffer']
            })
        ],
        resolve: {
            fallback: {
                crypto: false,
                fs: false,
                assert: false,
                process: false,
                util: false,
                path: false,
                stream: false,
            }
        }
    }
})

Awesome! We should now be safe from confusing polyfill errors. 😌

Configure ESLint

Whilst we’re configuring things, let’s add a couple of things to our ESLint configurations. If you’re not familiar with ESLint, it’s a JavaScript linter that our code editor uses to warn us about errors or code that doesn’t comply with a given code style.

Since we’ll be using the super fancy <script setup> tag in our VueJS 3 components, we need to tell ESLint about it so our code editor doesn’t show lots of errors when the code is actually valid.

There’s no need to worry too much about the details here, simply open the package.json of your app directory and replace your eslintConfig object with the following.

"eslintConfig": {
  "root": true,
  "env": {
    "node": true,
    "vue/setup-compiler-macros": true
  },
  "extends": [
    "plugin:vue/vue3-essential",
    "eslint:recommended"
  ],
  "parserOptions": {
    "parser": "@babel/eslint-parser"
  },
  "rules": {
    "vue/script-setup-uses-vars": "error"
  }
},

Install TailwindCSS

I’ll be using my favourite CSS framework to design the user interface: TailwindCSS. If you’re not familiar with it, it’s a utility-based framework that is super powerful and an absolute delight to work with. Needless to say, I highly recommend it.

To install it, we need the following dependencies. As usual, make sure to run this in the app directory.

npm install [email protected] [email protected] [email protected]

Then we need to generate our Tailwind configuration file. For that simply run the following.

npx tailwindcss init -p

This generated a tailwind.config.js file in our app directory.

Note that we used the -p option to also generate a postcss.config.js file. This is necessary so that Webpack can recognise Tailwind as a PostCSS plugin and therefore compile our Tailwind configurations.

Let’s immediately make a little adjustment to our Tailwind config file. We’ll provide a purge array so that, when compiling for production, Tailwind can remove all of the utility classes that are not used within the provided paths.

Basically, we need to tell it where our HTML is located which, in our case, is inside any JavaScript file within the src folder or within the public index.html file.

So open up your tailwind.config.js file and replace the empty purge array with the following lines.

module.exports = {
  purge: [
    './public/index.html',
    './src/**/*.{vue,js,ts,jsx,tsx}',
  ],
  // ...
}

Next, create a new file in the src folder called main.css and add the following code.

@tailwind base;
@tailwind components;
@tailwind utilities;

When compiled, these three Tailwind statements will be replaced with lots of utility classes generated dynamically.

Finally, we need to import this new CSS file into our main.js file so it can be picked up by Webpack.

Let’s import it at the top of that file and add a few comments to separate the code into little sections.

// CSS.
import './main.css'

// Create the app.
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

We’re now fully ready to use TailwindCSS!

Install Vue Router

Next, we need some routing within our frontend. Clicking on a new page should be reflected in the URL and vice versa. Fortunately, we don’t need to implement that from scratch as we can use Vue Router for that purpose.

To install it, we need to run the following. Note that we need to explicitly install version 4 of Vue Router since this is the version compatible with Vue 3.

npm install [email protected]

Next, let’s define our routes — i.e. the mapping between URLs and VueJS components.

Create a new file in the src folder called routes.js and paste the following inside.

export default [
    {
        name: 'Home',
        path: '/',
        component: require('@/components/PageHome').default,
    },
    {
        name: 'Topics',
        path: '/topics/:topic?',
        component: require('@/components/PageTopics').default,
    },
    {
        name: 'Users',
        path: '/users/:author?',
        component: require('@/components/PageUsers').default,
    },
    {
        name: 'Profile',
        path: '/profile',
        component: require('@/components/PageProfile').default,
    },
    {
        name: 'Tweet',
        path: '/tweet/:tweet',
        component: require('@/components/PageTweet').default,
    },
    {
        name: 'NotFound',
        path: '/:pathMatch(.*)*',
        component: require('@/components/PageNotFound').default,
    },
]

These are all of the pages our application contains including a fallback for URLs that don’t exist.

If we try to compile our frontend application at this point using npm run serve it will fail because all of these components are missing but, don't worry, we'll add all of them in the next section.

Now that our routes are defined, we can import and plug the Vue Router plugin into our VueJS application.

Open your src/main.js file and update it as follow.

// CSS.
import './main.css'

// Routing.
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
const router = createRouter({
    history: createWebHashHistory(),
    routes,
})

// Create the app.
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).use(router).mount('#app')

As you can see, we first create a router instance by providing our routes and then make our VueJS app use it as a plug-in.

In case you’re wondering, the createWebHashHistory method prefixes all paths with a # so that we don’t need to configure any redirections in our server later.

At this point, our VueJS app is fully configured with Vue Router and TailwindCSS. All that’s left to do is implement the components that will make up the user interface of our frontend.

That means if you wanted to create your own design, you could pause here and implement the components listed in the routes.js file yourself.

However, I’ve prepared all of that for you so we can focus on how to integrate the frontend with our Solana program rather than spending ages designing a user interface.

So it’s time for some copy/pasting! 👀

Copy/paste some files

Okay, let’s do this! Download the ZIP file below and extract it to access all of the files that will compose our user interface.

Download the ZIP file

Now that you’ve got all the files, let’s move them to the right folders.

Inside the src folder:

  • Remove the existing App.vue component and replace it with the one provided in the ZIP file.
  • Remove the existing components directory and replace it with the one provided in the ZIP file.
  • Add the composables and api directories from the ZIP file.

Boom, frontend ready! 💥

At this point you should be able to run npm run serve and have a look at the user interface by accessing: http://localhost:8080/.

npm run serve

# Outputs:
#
#  App running at:
#  - Local:   http://localhost:8080/
#  - Network: http://192.168.68.118:8080/

Okay, let's have a little look around and explain the purpose of all of these files we've just added.

Explaining components

We’ll start with the components. Aside from the App.vue component which is located in the src folder, all other components should be inside the src/components folder.

Note that any tweet or any connected wallet is currently mocked with fake data so we can learn how to wire everything in the next episodes.

  • App.vue: This is the main component that loads when our application starts. It designs the overall layout of our app and delegates the rest to Vue Router by using the <router-view> component. Any page that matches the current URL will be rendered where <router-view> is.
  • PageHome.vue: The home page. It contains a form to send tweets and lists the latest tweets from everyone.
  • PageNotFound.vue: The 404 fallback page. It displays an error message and offers to go back to the home page.
  • PageProfile.vue: The profile page for the connected user/wallet. It displays the wallet’s public key before showing the tweet form and the list of tweets sent from that wallet.
  • PageTopics.vue: The topics page allows users to enter a topic and displays all tweets matching it. Once a topic is entered it also displays a form to send tweets with that topic pre-filled.
  • PageTweet.vue: The tweet page only shows one tweet. The tweet’s public key is provided in the URL allowing us to fetch the tweet account. This is useful for users to share tweets.
  • PageUsers.vue: Similarly to the topics page, the users page allows searching for other users by entering their public key. When a valid public key is entered, all tweets from that user will be fetched and displayed on this page.
  • TheSidebar.vue: This component is used in the main App.vue component and designs the sidebar on the left of the app. It uses the <router-link> component to easily generate Vue Router URLs. It also contains a button for users to connect their wallets but for now, that button doesn’t do anything.
  • TweetCard.vue: This component is responsible for the design of one tweet. It is used everywhere we need to display tweets.
  • TweetForm.vue: This component designs the form allowing users to send tweets. It contains a field for the content, a field for the topic and a little character count-down.
  • TweetList.vue: This component uses the TweetCard.vue component to display not just one but multiple tweets.
  • TweetSearch.vue: This component offers a reusable form to search for criteria. It is used on the topics page and the users page as we need to search for something on both of these pages.

Explaining API files

On top of components, the ZIP file also contains an api folder. This folder contains one file for each type of interaction we can have with our program. Technically, we don’t need to extract these interactions into their own files but it is a good way to make our components less complicated and easier to maintain.

For now, each of these files defines a function that returns mock data.

  • fetch-tweets.js: Provides a function that returns all tweets from our program. In a future episode, we will transform that function slightly so it can filter through topics and users.
  • get-tweet.js: Provides a function that returns a tweet account from a given public key.
  • send-tweet.js: Provides a function that sends a SendTweet instruction to our program with all the required information.

Explaining composables

There’s one last folder in that ZIP file to explain: composables.

In VueJS, we call “composables” functions that use the composition API to extend the behaviour of a component. If you’re familiar with React, this is comparable to React hooks for VueJS.

Since certain components needed some extra functionality, I took the liberty to create some composables to make components easier to read.

  • useAutoresizeTextarea.js: This composable is used in the TweetForm.vue component and makes the “content” field automatically resize itself based on its content. That way the field contains only one line of text to start with but extends as the user types.
  • useCountCharacterLimit.js: Also used by the TweetForm.vue component, this composable returns a reactive character count-down based on a given text and limit.
  • useFromRoute.js: This composable is used by many components. It’s a little refactoring that helps deal with Vue Router hooks. Normally, we’d need to add some code for when we enter a router and some other code when the route updates but the components stay the same — e.g. the topic changes in the topics page. That function enables us to write some logic once that will be fired on both events.
  • useSlug.js: This composable is used to transform any given text into a slug. For instance Solana is AWESOME will become solana-is-awesome. This is used anywhere we need to make sure the topic is provided as a slug. That way, we’ve got less risk of users tweeting on the same topic not finding each other’s tweets due to case sensitivity.

Conclusion

Well done, we’ve got ourselves a user interface! I truly hope you didn’t get any troubles along the way, the frontend world can be quite unforgiving at times. If you have any issues, feel free to comment below or, even better, create a new issue on the project’s repository.

Speaking of repositories, you can view the code of this episode on the episode-7 branch and compare the code with the previous episode as usual. This time, I’ve also added another link to compare after the commit that generated the frontend via vue create app so you can see what we’ve changed afterwards.

View Episode 7 on GitHub

Compare with Episode 6 / Compare after creating the VueJS app

In the next three episodes, we will wire our mock user interface with real data and with real interactions with our Solana program. We’ll start with integrating our frontend with Solana wallets such as Phantom so we can identify the connected user in our application. See you in the next episode!

Discussions

Author avatar
Wayne Culbreth
5 months ago

When I try to serve this, I get this error:

ERROR TypeError: defineConfig is not a function TypeError: defineConfig is not a function at Object.<anonymous> (/Users/culbrethw/Development/solana-twitter/app/vue.config.js:4:18)

I'm not familiar with Vue at all - but it doesn't like this line in vue.config.js:

module.exports = defineConfig({

I'm thinking maybe something funky with the @vue/cli install, but I'm really at a loss. I've got 5.0.0.rc1 installed. Any ideas?

💖 0

Discussion

Scaffolding the frontend
Author avatar
Wayne Culbreth
5 months ago

When I try to serve this, I get this error:

ERROR TypeError: defineConfig is not a function TypeError: defineConfig is not a function at Object.<anonymous> (/Users/culbrethw/Development/solana-twitter/app/vue.config.js:4:18)

I'm not familiar with Vue at all - but it doesn't like this line in vue.config.js:

module.exports = defineConfig({

I'm thinking maybe something funky with the @vue/cli install, but I'm really at a loss. I've got 5.0.0.rc1 installed. Any ideas?

💖 0
Author avatar
Wayne Culbreth
5 months ago

Disregard. 'npm update' and I'm in business.

💖 0

Would you like to chime in?

You must be a member to add a reply to a discussion.

Fortunately, it only takes two click to become one. See you on the other side! 🌸

Become a Member
Author avatar
Andrea
5 months ago

After going through this I had some issues with yarn complaining about engines. After running "yarn config set ignore-engines true" then I could run vue create app --force. Then I followed the instructions at which point there were still issues running "npm run serve" from app/src. I then ran "npm install eslint" which gave me a new error. Then, I ran "npm install [email protected]" in app/src (instead of in app) and got the code to compile. However, I then had run time issues. Notably:

Uncaught TypeError: Cannot read properties of undefined (reading 'name')

This seems to be a problem with "<div class="text-xl font-bold" v-text="route.name"></div>" in App.vue.

Any advice?

💖 1

Discussion

Scaffolding the frontend
Author avatar
Andrea
5 months ago

After going through this I had some issues with yarn complaining about engines. After running "yarn config set ignore-engines true" then I could run vue create app --force. Then I followed the instructions at which point there were still issues running "npm run serve" from app/src. I then ran "npm install eslint" which gave me a new error. Then, I ran "npm install [email protected]" in app/src (instead of in app) and got the code to compile. However, I then had run time issues. Notably:

Uncaught TypeError: Cannot read properties of undefined (reading 'name')

This seems to be a problem with "<div class="text-xl font-bold" v-text="route.name"></div>" in App.vue.

Any advice?

💖 1
Author avatar
Andrea
5 months ago

Never mind, solved! I over-wrote the routes.js file when trying to deal with engines issue. It works!

💖 0
Author avatar
Kevin Connors
5 months ago

I had the same issue with Uncaught TypeError: Cannot read properties of undefined (reading 'name')

Since the tutorial has you copy paste a lot and then move files around manually, I assume I also screwed something up along the lines. What I did was just go to the github, download the entire app folder, and move that into my project. I recommend other people do the same as it's not very easy to follow this tutorial the way it is written.

💖 0
Author avatar
Rixiao Zhang
2 months ago

how to overwrite routes.js? please help !

💖 0

Would you like to chime in?

You must be a member to add a reply to a discussion.

Fortunately, it only takes two click to become one. See you on the other side! 🌸

Become a Member
Author avatar
Rixiao Zhang
2 months ago

when I run command npm run server, I do not see anything on http://localhost:8080/. Is this expected?

💖 0

Discussion

Scaffolding the frontend
Author avatar
Rixiao Zhang
2 months ago

when I run command npm run server, I do not see anything on http://localhost:8080/. Is this expected?

💖 0

Would you like to chime in?

You must be a member to add a reply to a discussion.

Fortunately, it only takes two click to become one. See you on the other side! 🌸

Become a Member
Author avatar
Rixiao Zhang
2 months ago

when I inspect the page, I found the following error. Any help would be appreciated.

Uncaught TypeError: $setup.route is undefined render App.vue:17 renderComponentRoot runtime-core.esm-bundler.js:893 componentUpdateFn runtime-core.esm-bundler.js:5030 run reactivity.esm-bundler.js:167 setupRenderEffect runtime-core.esm-bundler.js:5156 mountComponent runtime-core.esm-bundler.js:4939 processComponent runtime-core.esm-bundler.js:4897 patch runtime-core.esm-bundler.js:4489 render runtime-core.esm-bundler.js:5641 mount runtime-core.esm-bundler.js:3877 mount runtime-dom.esm-bundler.js:1590 <anonymous> main.js:15 js app.js:360 webpack_require app.js:895 webpack_exports app.js:2017 O app.js:949 <anonymous> app.js:2018 <anonymous> app.js:2020

💖 0

Discussion

Scaffolding the frontend
Author avatar
Rixiao Zhang
2 months ago

when I inspect the page, I found the following error. Any help would be appreciated.

Uncaught TypeError: $setup.route is undefined render App.vue:17 renderComponentRoot runtime-core.esm-bundler.js:893 componentUpdateFn runtime-core.esm-bundler.js:5030 run reactivity.esm-bundler.js:167 setupRenderEffect runtime-core.esm-bundler.js:5156 mountComponent runtime-core.esm-bundler.js:4939 processComponent runtime-core.esm-bundler.js:4897 patch runtime-core.esm-bundler.js:4489 render runtime-core.esm-bundler.js:5641 mount runtime-core.esm-bundler.js:3877 mount runtime-dom.esm-bundler.js:1590 <anonymous> main.js:15 js app.js:360 webpack_require app.js:895 webpack_exports app.js:2017 O app.js:949 <anonymous> app.js:2018 <anonymous> app.js:2020

💖 0

Would you like to chime in?

You must be a member to add a reply to a discussion.

Fortunately, it only takes two click to become one. See you on the other side! 🌸

Become a Member
Author avatar
Rixiao Zhang
2 months ago

Uncaught TypeError: $setup.route is undefined render App.vue:17

renderComponentRoot runtime-core.esm-bundler.js:893

componentUpdateFn runtime-core.esm-bundler.js:5030

run reactivity.esm-bundler.js:167

setupRenderEffect runtime-core.esm-bundler.js:5156

mountComponent runtime-core.esm-bundler.js:4939

processComponent runtime-core.esm-bundler.js:4897

patch runtime-core.esm-bundler.js:4489

render runtime-core.esm-bundler.js:5641

mount runtime-core.esm-bundler.js:3877

mount runtime-dom.esm-bundler.js:1590

<anonymous> main.js:15

js app.js:360

__webpack_require__ app.js:895

__webpack_exports__ app.js:2017

O app.js:949

<anonymous> app.js:2018

<anonymous> app.js:2020
💖 0

Discussion

Scaffolding the frontend
Author avatar
Rixiao Zhang
2 months ago

Uncaught TypeError: $setup.route is undefined render App.vue:17

renderComponentRoot runtime-core.esm-bundler.js:893

componentUpdateFn runtime-core.esm-bundler.js:5030

run reactivity.esm-bundler.js:167

setupRenderEffect runtime-core.esm-bundler.js:5156

mountComponent runtime-core.esm-bundler.js:4939

processComponent runtime-core.esm-bundler.js:4897

patch runtime-core.esm-bundler.js:4489

render runtime-core.esm-bundler.js:5641

mount runtime-core.esm-bundler.js:3877

mount runtime-dom.esm-bundler.js:1590

<anonymous> main.js:15

js app.js:360

__webpack_require__ app.js:895

__webpack_exports__ app.js:2017

O app.js:949

<anonymous> app.js:2018

<anonymous> app.js:2020
💖 0

Would you like to chime in?

You must be a member to add a reply to a discussion.

Fortunately, it only takes two click to become one. See you on the other side! 🌸

Become a Member

Would you like to chime in?

You must be a member to start a new discussion.

Fortunately, it only takes two click to become one. See you on the other side! 🌸

Become a Member