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.
Top comments (5)
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. :-)
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.What advantages does it have over firebase?
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.
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.