Get started with React and StaticBackend
Dominic St-Pierre
Posted on June 25, 2023
StaticBackend is a simple backend server API you may use to build complex web applications. Fully open source and easy to self-host. It handles lot of backend building blocks.
GitHub repository: https://github.com/staticbackendhq/react-wiki
Let's see how we can use StaticBackend from a React application. In this tutorial, we will build a simple wiki web application.
Setup your development environment
You'll need to have a working development environment for StaticBackend and React to follow along.
StaticBackend
You have three options to get started with StaticBackend. It depends on your preferences.
- Self-hosted version via Docker or binary
- CLI - command-line interface
- Sign-up for a 60-day free trial - (require credit card)
Let's install the CLI for this example:
$ npm install -g @staticbackend/cli
This install a backend
command you may use.
Start the dev server
$ backend server
This will create a backend application, an admin user and prints the keys you'll need.
You'll have an instance of StaticBackend running locally at http://localhost:8099.
Create the React app
We will use Create React App to create a new React application named react-wiki
.
$ npx create-react-app react-wiki --template typescript
... lots of npm output ...
$ cd react-wiki
Test your app
Let's test that everything works so far before we jump into adding StaticBackend related functionalities. After that, you may start your application via this command:
$ npm start
You should see the default page created by create-react-app
.
Add StaticBackend
We'll now add StaticBackend's JavaScript library to the React application.
$ npm install @staticbackend/js
We will create a reusable module that will expose the StaticBackend client to the rest of the React app.
Creating an instance of the client requires two essential arguments.
- The public key tells StaticBackend which app to target.
- The region indicates to the client which API URL to use.
Let's add those two configuration values inside the .env
file.
create-react-app
handles this file and make its variables available to your client-side application.
If you're working with multiple developers, they can all have their public key on their local development computer. So you'd only need to change those two values to go to production.
Let's create the .env
file:
REACT_APP_SB_PUBLUC_KEY=dev_memory_pk
REACT_APP_SB_REGION=dev
REACT_APP_SB_ROOT_TOKEN=todo we don't need this
The variables must starts with REACT_APP_ to be available to your application via the process.env.REACT_APP_XYZ
.
Make sure to change the values from the email printed to the terminal when you created your app.
We can now create our reusable module that initializes the StaticBackend JavaScript client.
src/sb.ts:
import {Backend} from "@staticbackend/js"
const pubKey = process.env.REACT_APP_SB_PUBLUC_KEY;
const region = process.env.REACT_APP_SB_REGION;
const bkn = new Backend(pubKey || "public key required", region || "dev");
export const backend = bkn;
Our JavaScript library exposes only one item, the Backend
function. We use it to pass our public key and the region. From here, we will be able to import the client like this:
import {backend} from "./sb";
All available functions supported by StaticBackend are accessible via the backend
instance.
User authentication
Our first step is to handle user authentication. We will use one page containing two forms, one for creating an account and one for letting the user log in.
Before we jump in, it might be helpful to understand how StaticBackend handles authentication and requests.
Users authenticate themselves in exchange for a session token. You need to supply this token for each request you send to StaticBackend.
It might be good to save this session token to the session storage. However, it expires after 12 hours.
src/auth.tsx:
import React, {Component} from "react";
import {backend} from "./sb";
export interface IProps {
onToken: (token: string) => void;
}
interface IState {
email: string;
password: string;
}
export class Auth extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
email: "",
password: ""
}
}
onChanged(field: "email" | "password", e: React.ChangeEvent<HTMLInputElement>) {
const val = e.currentTarget?.value;
if (field == "email") {
this.setState({
...this.state,
email: e.target?.value,
});
} else {
this.setState({
...this.state,
password: e.target?.value
});
}
}
signup = async () => {
const {email, password} = this.state;
const res = await backend.register(email, password);
if (!res.ok) {
alert(res.content);
return;
}
this.props.onToken(res.content);
}
signin = async () => {
const {email, password} = this.state;
const res = await backend.login(email, password);
if (!res.ok) {
alert(res.content);
return;
}
this.props.onToken(res.content);
}
render() {
return (
<div>
<h1>User authentication</h1>
<div>
<label>Email</label><br />
<input type="email" required value={this.state.email} onChange={this.onChanged.bind(this, "email")} />
</div>
<div>
<label>Password</label><br />
<input type="password" required value={this.state.password} onChange={this.onChanged.bind(this, "password")} />
</div>
<div>
<button onClick={this.signup.bind(this)}>Create account</button>
<button onClick={this.signin.bind(this)}>Login</button>
</div>
</div>
);
}
}
The main code that's not React boilerplate is the two functions related to authentication signup
and signin
. We pass the email and password to create an account or authenticate the user.
Notice how we're using an async/await function to call StaticBackend endpoints.
We're using a callback function from the props
to send the user's session token back to the parent.
The following changes were made to the app.tsx
file.
src/app.tsx:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import {Auth} from "./auth";
interface IState {
token: string | null;
}
export class App extends React.Component<any, IState> {
constructor(props: any) {
super(props);
this.state = {
token: null
}
}
onToken(token: string) {
this.setState({token: token});
}
render() {
if (!this.state.token) {
return <Auth onToken={this.onToken.bind(this)} />;
}
return (
<div>
<h1>Todo</h1>
</div>
)
}
}
If we have no session token, we display our Auth component. Otherwise, we show a simple to-do text for now.
Wiki article CRUD
The core functionality of StaticBackend is its database. Let's see how teams manage a wiki by creating, retrieving, updating, and deleting rticles.
Create and save article
Let's create a simple component to create and edit articles. When we create an article, we'll ask for the author's name, the title, and the body content.
When we are in edit mode, only the content will be editable.
We cannot build it with the default security model By default, users of the same account have read permission but cannot modify a record created by another user of the same account. Only the document owner has the write permission using the default security model.
We want users of the same account to have write permission for an article created across their team members for our wiki.
Custom permissions are set using a suffix of the table name. In our case, we will use articles_770_
instead of articles
to let users of the same account update documents created for their account.
We will use articles_770_
which means:
- 7: The owner has read/write permissions.
- 7: The account users have read/write permissions.
- 0: Other logged-in users has no permissions.
Note that the default permission are::
- 7: The owner has read/write.
- 4: Users of same account has read permission.
- 0: Other logged-in users has no permissions.
src/article_edit.tsx:
import React, { FormEventHandler } from "react";
import { backend } from "./sb";
export interface IProps {
token: string;
editId: string | null;
onSave: (article: Article) => void;
}
interface IState {
article: Article;
isNew: boolean;
}
export class ArticleEdit extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
article: {
id: "",
accountId: "",
title: "",
body: "",
authorName: "",
created: new Date()
},
isNew: props.editId == null
}
}
componentDidMount = async () => {
if (this.props.editId) {
const res = await backend.getById(
this.props.token,
"articles_770_",
this.props.editId
);
if (!res.ok) {
alert(res.content);
return;
}
this.setState({
article: res.content,
isNew: false
})
}
}
onChanged(field: "title" | "body" | "authorName", e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
const val = e.currentTarget?.value;
let { article } = this.state;
article[field] = val;
this.setState({
...this.state,
article: article,
});
}
save = async (e: any) => {
e.preventDefault();
const {token, editId, onSave} = this.props;
const {isNew, article} = this.state;
let res = null;
if (isNew) {
article.created = new Date();
res = await backend.create(
token,
"articles_770_",
article
)
} else {
res = await backend.update(
token,
"articles_770_",
editId || "",
article
)
}
if (!res.ok) {
alert(res.content);
return;
}
this.props.onSave(res.content);
}
renderAuthor() {
if (this.state.isNew) {
return (
<p>
<input
type="text"
value={this.state.article.authorName}
onChange={this.onChanged.bind(this, "authorName")}
placeholder="Author name"
required
/>
</p>
)
} else {
<p>{this.state.article.authorName}</p>
}
}
render() {
const { article, isNew } = this.state;
return (
<div>
<h1>{isNew ? "Create new article" : `Editing ${article.title}`}</h1>
<form onSubmit={this.save.bind(this)}>
<p>
<input
type="text"
value={article.title}
onChange={this.onChanged.bind(this, "title")}
placeholder="Article title"
/>
</p>
<p>
<textarea
value={article.body}
onChange={this.onChanged.bind(this, "body")}>
</textarea>
</p>
{this.renderAuthor()}
<p>
<button type="submit">
{isNew ? "Create article" : "Save changes"}
</button>
</p>
</form>
</div>
)
}
}
The things that you should focus on are:
This component receives its token and an indicator if we're editing or creating a new article.
export interface IProps {
token: string;
editId: string | null;
onSave: (article: Article) => void;
}
The state and constructor are straightforward.
Let's look at the componentDidMount
where we use the getById
function to grab the article via the id
received in the props
.
const res = await backend.getById(
this.props.token,
"articles_770_",
this.props.editId
);
The save
function make sure to call the create
or update
function depending if we're creating or updating the article.
Once saved, we call the onSave
function received in the props
and return the control to the parent component.
List and delete article
There have been some changes to the src/app.tsx
file to list, react to the creation and update, and delete articles.
Here's the part that have changed:
The state:
interface IState {
token: string | null;
articles: Array<Article>;
isEditing: boolean;
editId: string | null;
}
The callback after a successful account creation or login now fetches the articles for that account.
onToken(token: string) {
this.setState({
...this.state,
token: token
});
(async () => {
const res = await backend.list(
token,
"articles_770_"
);
if (!res.ok) {
console.error(res.content);
return;
}
let articles = res.content.results;
if (!articles) {
articles = [];
}
this.setState({
...this.state,
articles: articles
})
})();
}
When an article is created or updated we handle the changes in a callback we pass via props
to the ArticleEdit
component.
onArticleSaved(article: Article) {
let { articles, editId } = this.state;
if (editId == null) {
articles.push(article);
} else {
let idx = this.findArticle(articles, editId);
if (idx > -1) {
articles[idx] = article;
}
}
this.setState({
...this.state,
articles: articles,
isEditing: false,
editId: null
})
}
Thie is the deletion of an article:
del = async (id: string) => {
const res = await backend.delete(
this.state.token || "",
"articles_770_",
id || ""
);
if (!res.ok) {
alert(res.content);
return;
}
let { articles } = this.state;
let idx = this.findArticle(articles, id);
if (idx > -1) {
articles.splice(idx, 1);
}
this.setState({
...this.state,
articles: articles
})
}
The render
function:
render() {
if (!this.state.token) {
return <Auth onToken={this.onToken.bind(this)} />;
} else if (this.state.isEditing) {
return <ArticleEdit
token={this.state.token}
editId={this.state.editId}
onSave={this.onArticleSaved.bind(this)}
/>;
}
return (
<div>
<h1>List articles</h1>
<button onClick={this.newArticle.bind(this)}>
Create a new article
</button>
<p>
<strong>Todo list articles</strong>
</p>
<ul>
{this.state.articles.map((a) => this.renderArticle(a))}
</ul>
</div>
)
}
Notice how we're passing the callback to the ArticleEdit
's onSave
props.
Finally, the renderArticle
which render a simple <li>
with the Edit and Delete buttons.
renderArticle(a: Article) {
return (
<li key={a.id}>
<h4>{a.title}</h4>
<p>{a.body}</p>
<p>By {a.authorName} on {a.created}</p>
<p>
<button onClick={this.edit.bind(this, a.id)}>Edit</button>
<button onClick={this.del.bind(this, a.id)}>Delete</button>
</p>
</li>
)
}
Conclusion
The concept should look familiar if you have built a React application in the past. You may use all the functionalities of StaticBackend via the
backend
client.
In this tutorial, we've only covered the authentication and the
database, but there are many more building blocks at your disposal.
- WebSocket - real-time database and topic-based communication. It's not limited to database events.
- Storage - upload files and serve them via a CDN close to your users location.
- Server-side functions - create secure function when you need to run things server-side.
- Send emails - all web/mobile apps need to send emails to their users.
- Cache - cache key data for performance.
Posted on June 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.