Get started with React and StaticBackend

dstpierre

Dominic St-Pierre

Posted on June 25, 2023

Get started with React and StaticBackend

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.

  1. Self-hosted version via Docker or binary
  2. CLI - command-line interface
  3. Sign-up for a 60-day free trial - (require credit card)

Let's install the CLI for this example:

$ npm install -g @staticbackend/cli 
Enter fullscreen mode Exit fullscreen mode

This install a backend command you may use.

Start the dev server

$ backend server
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

  1. The public key tells StaticBackend which app to target.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
      })
    })();
  }
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.
💖 💪 🙅 🚩
dstpierre
Dominic St-Pierre

Posted on June 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related