DEV Community

Dan Bock
Dan Bock

Posted on

How to use Userbase in a React web app with TypeScript

Userbase is a new backend-as-a-service that promises a simple way to add user accounts and a general-purpose database to your web app. Here's how to set it up in a React web app with TypeScript.

This tutorial demonstrates the basic functionality of Userbase, including:

  • User registration
  • Login
  • Logout
  • Saving data

Step 1: Setup

First, make sure you have Node ≥ 8.10 and npm ≥ 5.6 on your machine, and enter this command:

npx create-react-app userbase-react-typescript-demo --template typescript

This uses create-react-app to create a template project using TypeScript. Now install Userbase in your new project:

cd userbase-react-typescript-demo
npm install userbase-js

Next, go to Userbase's web site and create an admin account. A free account is limited to one app and three users, which is good enough for our purposes. The web site will give you an app ID. Copy this, create a new file named .env in the root of your project, and paste your app ID like so:

REACT_APP_USERBASE_APP_ID=5e363c2a-7a29-47c8-9496-bd26cc5637ee

With the boring setup stuff out of the way, we can start coding.

Step 2: Add User Registration

(If you want to skip ahead to the completed app, check it out here.)

Open up App.tsx and delete the contents of the file. Replace it with the following. First, import some things we'll need from React and Userbase:

import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react'
import userbase, { UserResult } from 'userbase-js'

Next, the start of the component and a user state variable to keep track of whether the user is logged in. When user is undefined, that will mean the user is logged out. When it's defined, that will mean the user is logged in.

const App: React.FC = () => {
  const [user, setUser] = useState<UserResult>()

Next is the code we need to run to initialize Userbase when the user first opens the page. This picks up the app ID you saved in the .env file earlier.

useEffect(() => {
  userbase
  .init({ appId: process.env.REACT_APP_USERBASE_APP_ID as string })
  .then(session => session.user && setUser(session.user))
}, [])

Now a couple of things to manage the form that the user will type into:

const [regForm, setRegForm] = useState<{
  username?: string
  password?: string
}>({ username: '', password: '' })
const handleRegInputChange = (event: ChangeEvent<HTMLInputElement>) =>
  setRegForm({ ...regForm, [event.target.name]: event.target.value })

The last function is the one to send the user data to Userbase when the user registers:

const handleRegSubmit = (event: FormEvent<HTMLFormElement>) => {
  event.preventDefault()
  if (regForm.username && regForm.password)
    userbase
      .signUp({
        username: regForm.username,
        password: regForm.password,
        rememberMe: 'local'
      })
      .then((ur: UserResult) => setUser(ur))
      .catch(err => alert(err))
}

Finally, return the JSX containing the form, and export the component:

  return (
    <div>
      <h2>Register</h2>
      <form onSubmit={handleRegSubmit}>
        <label>
          Username:
          <input
            type="text"
            name="username"
            value={regForm?.username}
            onChange={handleRegInputChange}
          />
        </label>

        <label>
          Password:
          <input
            type="password"
            name="password"
            value={regForm?.password}
            onChange={handleRegInputChange}
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
    </div>
  )
}

export default App

Here's the full App.tsx after step 2.

Step 3: Add Login

The login code is going to look a lot like the registration code. The reason I'm not refactoring it here into a more DRY style is that in the real world you'd probably want to add different styling, input validation and other things to the login code than to the registration code.

The functions:

  const [loginForm, setLoginForm] = useState<{
    username?: string
    password?: string
  }>({ username: '', password: '' })

  const handleLoginInputChange = (event: ChangeEvent<HTMLInputElement>) =>
    setLoginForm({ ...loginForm, [event.target.name]: event.target.value })

  const handleLoginSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    if (loginForm.username && loginForm.password)
      userbase
        .signIn({
          username: loginForm.username,
          password: loginForm.password,
          rememberMe: 'local'
        })
        .then((ur: UserResult) => setUser(ur))
        .catch(err => alert(err))
  }

And the JSX:

          <h2>Log in</h2>
          <form onSubmit={handleLoginSubmit}>
            <label>
              Username:
              <input
                type="text"
                name="username"
                value={loginForm?.username}
                onChange={handleLoginInputChange}
              />
            </label>

            <label>
              Password:
              <input
                type="password"
                name="password"
                value={loginForm?.password}
                onChange={handleLoginInputChange}
              />
            </label>
            <input type="submit" value="Submit" />
          </form>

Here's the full App.tsx after step 3.

Step 4: Add Logout

The function to handle logout is pretty simple. It updates our app's state by calling setUser(undefined) when it gets the signal from Userbase that the user has been logged out.

  const handleLogout = () => {
    userbase
      .signOut()
      .then(() => setUser(undefined))
      .catch(err => alert(err))
  }

In the return statement, we need to add a new conditional. If the user state variable is defined, that means the user is signed in, and the app needs to display "Signed in as" and the user's name, plus a button the user can click on to log out. If user is undefined, that means the user is not signed in, and the registration and login options should be shown.

The return statement should now look like this:

  return (
    <div>
      {user ? (
        <div>
          <div>
            Signed in as {user.username}.{' '}
            <button onClick={handleLogout}>Log out</button>
          </div>
        </div>
      ) : (
        <div>
          <h2>Register</h2>
          <form onSubmit={handleRegSubmit}>
            <label>
              Username:
              <input
                type="text"
                name="username"
                value={regForm?.username}
                onChange={handleRegInputChange}
              />
            </label>

            <label>
              Password:
              <input
                type="password"
                name="password"
                value={regForm?.password}
                onChange={handleRegInputChange}
              />
            </label>
            <input type="submit" value="Submit" />
          </form>

          <h2>Log in</h2>
          <form onSubmit={handleLoginSubmit}>
            <label>
              Username:
              <input
                type="text"
                name="username"
                value={loginForm?.username}
                onChange={handleLoginInputChange}
              />
            </label>

            <label>
              Password:
              <input
                type="password"
                name="password"
                value={loginForm?.password}
                onChange={handleLoginInputChange}
              />
            </label>
            <input type="submit" value="Submit" />
          </form>
        </div>
      )}
    </div>
  )

Here's the full App.tsx after step 4.

Step 5: Save data

We need to add the Item import:

import userbase, { Item, UserResult } from 'userbase-js'

Inside the component, whenever user goes from undefined to defined, we need to call the openDatabase function. Our database will be called blueButton. Notice the changeHandler function that gets passed to Userbase here. That'll get defined below.

  const DATABASE_NAME = 'blueButton'

  useEffect(() => {
    if (user)
      userbase.openDatabase({ databaseName: DATABASE_NAME, changeHandler })
  }, [user])

We're going to have a button the user clicks, and the timestamp of each click will be saved to Userbase. The app will display the number of clicks on the screen. To do that, we need a state variable for number of clicks:

  const [numClicks, setNumCicks] = useState<number>()

And a function to insert into the database. This inserts a new record, with a JavaScript Date object for the current date and time, into the database. It will get called when the user clicks the button.

  const handleBlueButtonClick = () => {
    userbase.insertItem({ databaseName: DATABASE_NAME, item: new Date() })
  }

Here's one of the neatest parts of Userbase: the changeHandler function. We passed this function in as an argument to userbase.openDatabase above. As you can see, this function sets our numClicks state variable. But when does it get called? It gets called whenever the data in the database changes - even if it was changed from another device. There's a video of this neat trick below.

  const changeHandler = (items: Item[]) => {
    setNumCicks(items.length)
  }

Finally, here's the JSX that displays our big blue button and the number of times it's been clicked.

          <div>
            <h2>Click the blue button</h2>
            <button
              style={{
                fontSize: '25px',
                backgroundColor: 'blue',
                color: 'white'
              }}
              onClick={handleBlueButtonClick}
            >
              The Blue Button
            </button>
            <div style={{ marginTop: '25px' }}>
              You have clicked: {numClicks} times.
            </div>
          </div>

Here's the video of the final product in action. Userbase is using WebSockets behind the scenes, but you don't need to know anything about how those work.

Here's the final version of App.tsx.

Top comments (5)

Collapse
 
jen729w profile image
jen729w

Just wanted to say thanks Dan, this is a great tutorial, especially appreciate the TypeScript. I find it's a much easier language to learn by example and there were a few 'aaaahhhhhh' moments here for me. :-)

Collapse
 
netzwerg profile image
Rahel Lüthy

Excellent tutorial, thank you! Adding REACT_APP_USERBASE_APP_ID to .env is very unsafe and officially discouraged by the CRA docs (create-react-app.dev/docs/adding-c...). You might want to suggest storing it in .env.local instead so secret keys don't end up in public places – neither in the build, nor VCS.

Collapse
 
mateiadrielrafael profile image
Matei Adriel • Edited

What advantages does it have over firebase?

Collapse
 
danbockapps profile image
Dan Bock

There's a summary at their web site, but I think the big differences are the simplicity and the end-to-end encryption. There are certainly reasons you might want more complexity, or to be able to see your users' data, and in that case you wouldn't want Userbase.

Collapse
 
shelob9 profile image
Josh Pollock

I'm thinking of trying Userbase, and wanted to see what it looked like with Typescript and React. This was perfect for me. Thank you.