Create a React app with hat-js

Build a secure note app with React and @dataswift/hat-js

Using our Javascript SDK hat-js you can create data rich applications without writing a single line of backend server code. In this tutorial, you'll learn the fundamentals of using a Dataswift PDA, build on the HAT Microserver project, by making a fully functional app to store your private thoughts as Notes. When new notes get added to the app, the data will be stored in a HAT, in the cloud.

By the end of this tutorial you'll learn about:

  • Authenticating users

  • Register new users

  • Saving data to the HAT

Just the code (for the impatient)

Jump ahead by grabbing the code from our Github repo: git clone, yarn install and yarn start if you want to get hacking right away.

Set up your environment

Make sure your development environment includes Node.js and an npm package manager.

To check if you have Node.js installed, run this command in your terminal:

node -v

To confirm that you have npm installed you can run this command in your terminal:

npm -v

Install Create React App

You'll need to have Node v8.16.0 or Node v10.16.0 or later version on your machine.

npx create-react-app my-first-hat-app

If you use npm 5.1 or earlier you can't use npx. Instead, install create-react-app globally:

npm install -g create-react-app
create-react-app my-first-hat-app

And you can start by typing:

cd my-first-hat-app
npm start

Then open http://localhost:3000/ to see your app.

Testing HATs are configured to support requests from https://localhost:3000 by default. If you run the project locally on a different port the requests will not come through by the CORS policy.

Install HAT-JS

Now you can install the HAT-JS:

npm install @dataswift/hat-js

You'll need to restart the server every time you install a package using npm. You can use Ctrl + C to stop the server and npm start to start the server.

Create the main structure

Let's structure the project as following:

  • assets/: to keep all the styles and images.

  • app/: for the App component and Routing.

  • components/: for reusable React components.

  • features/: to group React components by feature.

  • utils/: to keep the helper functions.

Let's create the assets/, app/, components/, utils/ and features/ folders in the src/ folder. And move the App.js, App.css and App.test.js in the src/app/ folder.

The src/ folder structure should now look like this:

├───src/
│ ├───app/
│ │ ├───App.css
│ │ ├───App.js
│ │ └───App.test.js
│ ├───assets/
│ ├───components/
│ ├───features/
│ ├───utils/
│ ├───index.css
│ ├───index.js
│ ├───logo.svg
│ ├───serviceWorker.js
│ └───setupTests.js

Assets

You can download the compressed folder with basic assets from here. This includes the background image and the basic CSS styles to use across the project.

Unzip folder and paste the files into the assets/ folder.

The images/ and the styles/ folder should look like this:

│ ├───assets/
│ │ ├───images/
│ │ │ └───background-image.jpg
│ │ └───styles/
│ │ ├───main.scss
│ │ ├───_base.scss
│ │ ├───_branding.scss
│ │ ├───_buttons.scss
│ │ ├───_flex.scss
│ │ ├───_inputs.scss
│ │ └───_texts.scss

Install the node-sass to use Sass and SCSS:

npm install node-sass

And add this line in the index.js:

// /index.js
import './assets/styles/main.scss';

Also, include the material icons in the of the index.html file which is located in the public/ folder:

// public/index.html
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">

Main pages and Routing

For this application we'll need 4 routes:

  • "/" for the Home page.

  • "/login" for the Login page.

  • "/signup" for the Signup page.

  • "/authentication" to use it for the authentication callback URL.

First, we'll need to install the react-router and react-router-dom dependencies:

npm install react-router react-router-dom

Create a new file Routing.js in the app/ folder and we can include the first Route to the Home page:

// src/app/Routing.js
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import HomePage from "../features/Home/HomePage";
function Routing() {
return (
<Router>
<Switch>
<Route exact path="/" component={HomePage} />
</Switch>
</Router>
);
}
export default Routing;

Basic components

Home Page

In the feature folder, create a Home/ subfolder in the src/features/ folder with the HomePage.js and HomePage.scss files:

// src/features/Home/HomePage.js
import React from 'react';
import './HomePage.scss';
function HomePage() {
return (
<div className="home-wrapper"></div>
);
}
export default HomePage;
// src/features/Home/HomePage.scss
.home-wrapper {
min-height: 100%;
background-size: cover;
background-image: linear-gradient(to bottom, rgba(74, 85, 107, 0.2), rgba(0, 0, 0, 0.66)), url('../../assets/images/background-image.jpg');
-webkit-background-size: cover;
}

Header

To add a basic navigation for the app, you'll need to create the Header component in the src/components/ folder. Let's create a subfolder header to include the JavaScript and CSS file.

// src/components/header/Header.js
import React from 'react';
import { NavLink } from 'react-router-dom';
import './Header.scss';
function Header() {
return (
<>
<header className="header">
<NavLink exact activeClassName="active" to="/">
Home
</NavLink>
<ul>
<li>
<NavLink activeClassName="active" to="/login">
Log in
</NavLink>
</li>
<li>
<NavLink activeClassName="active" to="/signup">
Create account
</NavLink>
</li>
</ul>
</header>
</>
);
}
export default Header;
// src/components/header/Header.scss
header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 6.4rem;
border-bottom: 1px solid #657690;
padding-left: 10%;
padding-right: 10%;
margin: 0 auto;
background: #efefef;
a {
font-size: 1.6rem;
text-decoration: none;
color: #4a556b;
outline: 0;
&:hover {
color: #252736;
}
}
ul {
list-style: none;
align-self: end;
height: 100%;
padding: 2rem;
li {
display: inline-block;
margin-left: 2rem;
}
}
}

Now, we can import the Header component in the already created Routing.js:

// src/app/Routing.js
...
// Use the following syntax to import the Header
import Header from "../components/header/Header";
function Routing() {
return (
<Router>
{/* Add the Header component as following */}
<Header />
<Switch>
<Route exact path="/" component={HomePage} />
</Switch>
</Router>
);
}

The home page should look like this now:

Login Page

With the same way let's create the Login page in the features/login/ folder:

// src/features/Login/LoginPage.js
import React from 'react';
function LoginPage() {
return (
<form className={'flex-column-wrapper flex-content-center flex-align-items-center'}>
<div className={'flex-spacer-small'} />
<h3>Log in</h3>
<div className={'flex-spacer-small'} />
<input
name={'username'}
type={'text'}
placeholder="Username"
autoComplete={'username'}
/>
<div className={'flex-spacer-large'} />
<button className={'btn btn-accent'} type={'submit'}>
Next
</button>
<div className={'flex-spacer-small'} />
</form>
);
}
export default LoginPage;

And include the "/login" Route into the Routing.js:

// src/app/Routing.js
import LoginPage from "../features/Login/LoginPage";
function Routing() {
return (
<Router>
<Header />
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/login" component={LoginPage} />
</Switch>
</Router>
);
}

We can access the Login page on "/login" route or by clicking on the "Login" button.

The HTML form elements work a bit differently from other DOM elements in React.As in HTML the form element keep its own internally state, in React we have the option to keep the state in the Component level. For convenience we can have JavaScript function to handle the input changes and the submission of the form.

Since React 16.8, we can easily set the state in function components as following:

// Declare a new state variable, which we'll call "username"
const [username, setUsername] = useState('');

You can learn more about forms and hooks

// src/features/Login/LoginPage.js
import React, { useState } from 'react';
function LoginPage() {
const [username, setUsername] = useState('');
const handleChange = event => {
setUsername(event.target.value);
};
const handleSubmit = event => {
event.preventDefault();
};
return (
<form className={'flex-column-wrapper flex-content-center flex-align-items-center'}
onSubmit={e => handleSubmit(e)}
>
.
.
<input
.
.
value={username}
onChange={e => handleChange(e)}
/
.
.
</form>
);
}

Next step is to add the validation for the username. Let's create a validation.js file in the utils/ folder.

The username must follow these rules:

  • start with a letter

  • contain alphanumeric characters only

  • contain lowercase letters only

  • be between 4 to 21 characters

// src/utils/validations.js
const HAT_NAME_REGEX = /^[a-z][a-z0-9]{2,19}[a-z0-9]$/;
export const isHatName = hatName => {
return HAT_NAME_REGEX.test(hatName);
};

And you'll use this function to validate the user's input and display an error message when the username is not valid to submit. In the LoginPage.js we can validate the user's input and display an error message when the username is not valid to submit. In the onChange method we can reset the error message.

// src/features/Login/LoginPage.js
import React, { useState } from 'react';
import { isHatName } from "../../utils/validations";
function LoginPage() {
const [username, setUsername] = useState('');
const [errorMsg, setErrorMsg] = useState('');
const errorMessages = {
usernameNotValid: 'Sorry, this username is not valid'
};
const handleChange = event => {
setUsername(event.target.value);
// Reset the error message when the user is start typing.
setErrorMsg('');
};
const handleSubmit = event => {
event.preventDefault();
validateLoginDetails();
};
const validateLoginDetails = () => {
// Validate the username and display an error message if it's not valid.
if (isHatName(username)) {
// TODO redirect the user to their PDA.
} else {
setErrorMsg(errorMessages.usernameNotValid);
}
};
return (
<form
className={'flex-column-wrapper flex-content-center flex-align-items-center'}
onSubmit={e => handleSubmit(e)}
>
.
.
<input
className={` ${errorMsg ? 'input-error-field' : null}`}
.
.
/>
{errorMsg && <div className={'input-error-label'}>{errorMsg}</div>}
.
.
</form>
);
}

Redirect the user to the Login page to get the Access token

First, we need to redirect the user to their PDA where they can enter their password to complete the authorization.

We will need to build the URL using our app ID, our callback URI and a URI in case of failure.

The URL will look similar to this:

https://<hat-url>/#/hatlogin?
name=<application-id>
&redirect=<redirect>
&fallback=<fallback>
  • hat-url — The PDA’s unique URL. eg. testing.hubat.net

  • application-id — Our application ID. eg. testhatapp

  • redirect — The callback URI for our application. eg. http://localhost:3000/authentication

  • fallback — The URI will be called on authorization failure.

Lets create a configuration file app.config.js in the src/ folder:

// src/app.config.js
export const appConfig = {
applicationId: 'testhatapp',
namespace: 'testhatapp',
hatCluster: '.hubat.net',
hatApiVersion: 'v2.6',
secure: true,
};
  • applicationId — The application ID.

  • namespace — The application's namespace.

  • hatCluster — The HAT's cluster for the login. In this case we are using '.hubat.net' for testing purposes only. For production you'd use a real non-dev-sandbox PDA.

  • hatApiVersion — The API version of the HAT.

  • secure — If you want to run the HAT locally, you have to modify this field to 'false'.

For better user experience, it's good practice to validate the user's input and navigate them to their PDA to Login only if their username is valid and exists. Otherwise we'll display an error message to inform the user about the issue. You can use the HAT-JS to check if a PDA with this username exists.

Let's create the redirectValidUser method to redirect an existing user. We can use the hat-js to check if the user exists:

// src/features/Login/LoginPage.js
import React, { useState } from 'react';
import { HatClient } from '@dataswift/hat-js';
import { isHatName } from "../../utils/validations";
import { appConfig } from "../../app.config";
function LoginPage() {
const [username, setUsername] = useState('');
const [errorMsg, setErrorMsg] = useState('');
const errorMessages = {
usernameNotValid: 'Sorry, this username is not valid',
usernameNotRecognised: 'Sorry, this username is not recognised'
};
const redirectValidUser = async (username) => {
try {
const hat = new HatClient({});
const hatDomain = username + appConfig.hatCluster;
// Use the hat-js's helper method to check if the user exists
const res = await hat.auth().isDomainRegistered(hatDomain);
// if the res is true, navigate the user to their PDA
if (res) {
const hatUrl = `https://${hatDomain}`;
const redirectUrl = `http://${window.location.host}/authentication`;
const fallback = `http://${window.location.host}/authentication`;
const applicationId = appConfig.applicationId;
window.location.href = hat.auth().generateHatLoginUrl(hatUrl, applicationId, redirectUrl, fallback);
} else {
// Display an error message when the username is not recognised
setErrorMsg(errorMessages.usernameNotRecognised);
}
} catch (e) {
console.log(e);
}
};
const validateLoginDetails = () => {
if (isHatName(username)) {
// Use the new method when the username is valid
redirectValidUser(username);
} else {
setErrorMsg(errorMessages.usernameNotValid);
}
};
return (
<form>
.
.
</form>
);
}

Upon successful login, the HAT will verify if the application has been granted all the permissions it needs. If not, the HAT user will need to do so for the app to access their HAT.

Lastly, the user will be redirected back to our application with the authentication token attached. This should be stored for subsequent API calls:

http://localhost:3000/authentication?token=eyJ0eXAiOiJKV1Qi….

Authenticate user

To collect the token parameter, we will need to create a new component in the Login feature

Create the authentication handler

In this example we will store the token value in the Session Storage. Upon successful login we will navigate the user back to the HomePage where we will need to add a different screen to display to the user when they are authenticated. In case of error, we will redirect the user back to the Login page.

// src/features/Login/AuthenticationHandler.js
import React, { useEffect } from 'react';
import { useHistory } from 'react-router';
import { getParameterByName } from "../../utils/windowHelper";
function AuthenticationHandler() {
const history = useHistory();
useEffect(() => {
const token = getParameterByName('token');
const error = getParameterByName('error');
if (error) {
history.push('/login');
}
if (token) {
sessionStorage.setItem('token', token);
history.push('/');
}
}, [history]);
return <div>Loading...</div>;
}
export default AuthenticationHandler;

Get parameter by name

In order to get the query parameter value, let's create a new function getParameterByName in the utils/ folder:

// src/utils/windowHelper.js
export const getParameterByName = variable => {
const query = window.location.search.substring(1);
const vars = query.split('&');
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split('=');
if (pair[0] === variable) {
return decodeURIComponent(pair[1]);
}
}
return null;
};

Import the function in the AuthenticationHandler.js:

// src/features/Login/AuthenticationHandler.js
import { getParameterByName } from "../../utils/windowHelper";

Include the AuthenticationHandler in the Routing:

// src/app/Routing.js
import AuthenticationHandler from "../features/Login/AuthenticationHandler";
function Routing() {
return (
<Router>
<Header />
<Switch>
.
.
<Route path="/authentication" component={AuthenticationHandler} />
</Switch>
</Router>
);
}

Create Signup Page

In the same way with the Login page, we can create a page for the registration.

For the registration, we need to collect the username and the email, and redirect to the Hatters to complete the registration.

Lets create the SignupPage.js in the Signup feature folder:

// src/features/Signup/SignupPage.js
import React, { useState } from 'react';
import { appConfig } from '../../app.config';
import { isEmail, isHatName } from '../../utils/validations';
function SignupPage() {
const initUser = {
username: '',
email: '',
};
const [user, setUser] = useState(initUser);
const [errorMsg, setErrorMsg] = useState('');
const errorMessages = {
usernameNotValid: 'Sorry, this username is not valid',
emailNotRecognised: 'Sorry, email is not valid',
};
const navigateToHatters = () => {
const redirectUrl = `http://${window.location.host}/authentication`;
const applicationId = appConfig.applicationId;
window.location.href = `https://hatters.dataswift.io/services/baas/signup?email=${user.email}&hat_name=${user.username}&application_id=${applicationId}&redirect_uri=${redirectUrl}&lang=en`;
};
const handleChange = event => {
const name = event.target.name;
const value = event.target.value;
setUser({ ...user, [name]: value });
setErrorMsg('');
};
const handleSubmit = event => {
event.preventDefault();
validateSignupDetails();
};
const validateSignupDetails = () => {
let errorMsg = '';
if (!isHatName(user.username)) {
errorMsg = errorMessages.usernameNotValid;
} else if (!isEmail(user.email)) {
errorMsg = errorMessages.emailNotRecognised;
} else {
navigateToHatters();
}
if (errorMsg) {
setErrorMsg(errorMsg);
}
};
return (
<form onSubmit={e => handleSubmit(e)} className={'flex-column-wrapper flex-content-center flex-align-items-center'}>
<div className={'flex-spacer-small'} />
<h3>Create Account</h3>
<div className={'flex-spacer-small'} />
<input
className={` ${errorMsg ? 'input-error-field' : null}`}
name={'username'}
type={'text'}
placeholder="Username"
autoComplete={'username'}
value={user.username}
onChange={e => handleChange(e)}
/>
<input
className={` ${errorMsg ? 'input-error-field' : null}`}
name={'email'}
type={'text'}
placeholder="Email"
autoComplete={'email'}
value={user.email}
onChange={e => handleChange(e)}
/>
{errorMsg && <div className={'input-error-label'}>{errorMsg}</div>}
<div className={'flex-spacer-large'} />
<button className={'btn btn-accent'} type={'submit'}>
Next
</button>
<div className={'flex-spacer-small'} />
</form>
);
}
export default SignupPage;

In the validations.js file include a function to check if the Email is valid:

// src/utils/validations.js
const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export const isEmail = email => {
return EMAIL_REGEX.test(email);
};

Include the "/signup" route in the Routing.js:

// src/app/Routing.js
function Routing() {
return (
<Router>
<Header />
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/login" component={LoginPage} />
<Route path="/signup" component={SignupPage} />
<Route path="/authentication" component={AuthenticationHandler} />
</Switch>
</Router>
);
}
export default Routing;

Create a private Route for the authenticated username

In this case, we need to create a "global" state to keep information for the current authenticated user. With the Context we can create a simple state share across all the components.

We'll need to keep information such as a flag to check if the user is authenticated, the JWT token, and the username.

state = {
user: {
isAuthenticated: false,
token: '',
hatName: '',
},
}

Firstly, we have to create the Context object. Create the AuthContext.js file in the context folder under the components/ folder:

// src/components/context/AuthContext.js
import React from 'react';
const AuthContext = React.createContext();
export default AuthContext;

Secondly, create the Context Provider with state and helper function to log in/out. Create the AuthProvider.js:

// src/components/context/AuthProvider.js
import React, { Component } from 'react';
import AuthContext from './AuthContext';
import { HatTokenValidation } from '@dataswift/hat-js/lib/utils/HatTokenValidation';
export class AuthProvider extends Component {
state = {
user: {
isAuthenticated: false,
token: '',
hatName: '',
},
};
render() {
return (
<AuthContext.Provider
value={{
user: this.state.user,
logout: () => {
sessionStorage.removeItem('token');
this.setState({
user: {
isAuthenticated: false,
token: '',
hatName: '',
},
});
},
login: (token, hatName) => {
if (token && !HatTokenValidation.isEncodedTokenExpired(token)) {
this.setState({
user: {
isAuthenticated: true,
token: token,
hatName: hatName,
},
});
}
},
}}
>
{this.props.children}
</AuthContext.Provider>
);
}
}

Include AuthProvider in the App component

// src/app/App.js
import { AuthProvider } from "../components/context/AuthProvider";
function App() {
return (
<AuthProvider>
<div className="app">
<Routing/>
</div>
</AuthProvider>
);
}

Check if the user is authenticated

Now, in the Home page, we can check if the user is authenticated and display a protected page. In order to see this private page, they must login first.

HAT-JS offer the option to import the HatTokenValidation class to help decoding the token, accessing the JWT's properties and check the token expiration time:

// src/features/Home/HomePage.js
import React, { useEffect, useContext } from 'react';
import './HomePage.scss';
import { HatTokenValidation } from '@dataswift/hat-js/lib/utils/HatTokenValidation';
import AuthContext from "../../components/context/AuthContext";
function HomePage() {
const authContext = useContext(AuthContext);
useEffect(() => {
const token = sessionStorage.getItem('token');
if (token) {
const decodedToken = HatTokenValidation.decodeToken(token);
// If the token has not expired, log in the user.
if (!HatTokenValidation.isExpired(decodedToken)) {
authContext.login(token, decodedToken.iss);
}
}
}, []);
return (
<AuthContext.Consumer>
{context => (
<>
<div className="home-wrapper">{context.user.isAuthenticated && <div>Authenticated!</div>}</div>
</>
)}
</AuthContext.Consumer>
);
}
export default HomePage;

Add a log out button

In the Header.js we can use the AuthContext and display the "Logout" button when the user is authenticated:

// src/components/header/Header.js
import React from 'react';
import { Link, NavLink } from 'react-router-dom';
import './Header.scss';
import AuthContext from '../context/AuthContext';
function Header() {
return (
<AuthContext.Consumer>
{context => (
<>
<header className="header">
<NavLink exact activeClassName="active" to="/">
Home
</NavLink>
{context.user.isAuthenticated && (
<Link to="/login" onClick={() => context.logout()}>
Log out
</Link>
)}
{!context.user.isAuthenticated && (
<ul>
<li>
<NavLink activeClassName="active" to="/login">
Log in
</NavLink>
</li>
<li>
<NavLink activeClassName="active" to="/signup">
Create account
</NavLink>
</li>
</ul>
)}
</header>
</>
)}
</AuthContext.Consumer>
);
}
export default Header;

Create a protected Home component

The next step is to create the protected home component. Lets create the HomePrivate.js file into the Home feature:

// src/features/Home/HomePrivate.js
import React from 'react';
function HomePrivate() {
return (
<div className="home-private">
<div className="container">
<div className="content">
Private content
</div>
</div>
</div>
);
}
export default HomePrivate;

And include this CSS in the HomePage.scss:

// src/features/Home/HomePage.scss
.
.
.home-private {
height: 100%;
width: 100%;
.container {
border-radius: 6px;
padding: 2.4rem;
}
.content {
background: #e9e9e9;
padding: 2.4rem;
border-radius: 6px;
}
}

And then, replace the \Authenticated!\</div> with the component:

// src/features/Home/HomePage.js
.
.
import HomePrivate from "./HomePrivate";
function HomePage() {
.
.
return (
<AuthContext.Consumer>
{context => (
<>
<div className="home-wrapper">{context.user.isAuthenticated && <HomePrivate />}</div>
</>
)}
</AuthContext.Consumer>
);
}
export default HomePage;

Create Notes component

Create a new folder Notes in the features/ with the Notes.js file:

// src/features/Notes/Notes.js
import React from 'react';
import './Notes.scss';
function Notes() {
return (
<form
className={'notes-wrapper flex-column-wrapper flex-content-center flex-align-items-center'}
>
<div className={'flex-spacer-small'} />
<h3>Save a note on your HAT</h3>
<input
name={'note'}
type={'text'}
placeholder="Remember to..."
autoComplete={'text'}
/>
<div className={'flex-spacer-small'} />
<button className={'btn btn-accent'} type={'submit'}>
Save
</button>
<div className={'flex-spacer-small'} />
</form>
);
}
export default Notes;

And the Notes.scss:

// src/features/Notes/Notes.scss
.notes-wrapper {
input {
margin-top: 2.5rem;
margin-bottom: 2.5rem;
}
}
.note-row-wrapper {
display: flex;
flex-direction: row;
margin-top: 1rem;
.note-content {
text-align: left;
flex: 1 1;
color: #4a556b;
font-size: 1.6rem;
}
button {
margin-left: 1rem;
}
}
.notes-list {
margin-top: 2.5rem;
width: 100%;
max-width: 32rem;
list-style-type: none;
}

Include the Notes component in the HomePrivate.js:

// src/features/Home/HomePrivate.js
import React from 'react';
import Notes from "../Notes/Notes";
function HomePrivate() {
return (
<div className="home-private">
<div className="container">
<div className="content">
<Notes />
</div>
</div>
</div>
);
}
export default HomePrivate;

And now we can see the simple form for the notes:

Create, read, update, delete data records

One of the core design principles of the HAT has been its ability to store any structure data. Data is grouped under a combination of namespace and endpoint.

The namespace is similar to a "database" and you can define the unique namespace name in your application. The applications can by default have read and write data permission in this namespace and they cannot read data from other namespaces.

The endpoint is similar to a "table" in a "database". We have to option to create multiple endpoints for our app. One common pattern is to create endpoints to keep same structured data together. For example, if we want to store the user's profile and their location in our app, we can create two separate endpoints "profile" and "locations" and store the data there. Those endpoints we don't have to define them in our application, once we write the first data the endpoints will get created.

You can learn more about the API and how you can structure the data here.

Create data into the HAT

For the Notes we can use the following structure:

{
value: 'This is a note!',
dateCreated: "2020-05-20T10:17:10.398Z",
}

Lets start by storing our first note into the HAT:

// src/features/Notes/Notes.js
import React, { useContext, useState } from 'react';
import { HatClient } from '@dataswift/hat-js';
import './Notes.scss';
import { appConfig } from '../../app.config';
import AuthContext from "../../components/context/AuthContext";
function Notes() {
const [newNote, setNewNote] = useState('');
const authContext = useContext(AuthContext);
// The endpoint to store the Notes data
const notesEndpoint = 'my-notes';
// Create the configuration object to pass the auth token, api version and
// the ssl flag to the HAT-JS
const config = {
token: authContext.user.token,
apiVersion: appConfig.hatApiVersion,
secure: appConfig.secure,
};
// Initialize the HatClient with the configuration file
const hat = new HatClient(config);
const handleChange = event => {
setNewNote(event.target.value);
};
const handleSubmit = event => {
event.preventDefault();
createData();
};
const createData = async () => {
try {
if (!newNote) return;
const dateCreated = new Date().toISOString();
// Create the structure for the Note
const body = {
value: newNote,
dateCreated: dateCreated,
};
// Use the HAT-JS library to create our first data record
const res = await hat.hatData().create(appConfig.namespace, notesEndpoint, body);
if (res.parsedBody) {
// Reset the Note value
setNewNote('');
}
} catch (e) {
console.log(e.cause + e.state)
}
};
return (
<form
onSubmit={e => handleSubmit(e)}
>
<input
.
.
value={newNote}
onChange={e => handleChange(e)}
/>
.
.
</form>
);
}
export default Notes;

Read data from the HAT

And now, we can create a list to fetch the available data:

// src/features/Notes/Notes.js
function Notes() {
const [notes, setNotes] = useState([]);
.
.
.
const fetchNotes = async () => {
try {
// Use the HAT-JS library to get all the data records for the Note endpoint
const res = await hat.hatData().getAllDefault(appConfig.namespace, notesEndpoint);
if (res.parsedBody) {
setNotes(res.parsedBody);
}
} catch (e) {
console.log(e.cause + e.status);
}
};
useEffect(() => {
fetchNotes();
}, []);
const ListNotes = () =>
notes
.sort((a, b) => new Date(b.data.dateCreated) - new Date(a.data.dateCreated))
.map((item, index) => {
return (
<li key={index}>
<div className={'note-row-wrapper'}>
<div className={'note-content'}>{item.data.value}</div>
<button type={'button'}>
<i className={'material-icons'}>delete</i>
</button>
<button type={'button'}>
<i className={'material-icons'}>edit</i>
</button>
</div>
</li>
);
});
return (
<form ..>
.
.
<ul className={'notes-list'}>
<ListNotes />
</ul>
</form>
);
}
export default Notes;

We can now see our first note!

The response from the HAT is:

[
{
"endpoint":"testhatapp/my-notes",
"recordId":"552551c8-6b46-451d-b91c-c2022561d0c6",
"data":{
"value":"this is my first note!",
"dateCreated":"2020-05-13T10:44:59.651Z"
}
}
]

The "endpoint" is combined with the application namespace. The unique "recordId" for every data record that can be used for the data record deletion, and the "data" field where we can find the stored data.

Update a data record

In order to update the HAT record, we have to send the same structure as the response from the HAT. To keep the example simple, we can add an exclamation mark at the end of the value every time the user clicks the Update button. In order to update a data records, we have to send the data as structured in the HAT's response.

// src/features/Notes/Notes.js
function Notes() {
.
.
const updateData = async hatRecord => {
const noteIndex = notes.indexOf(hatRecord);
// Update the value
hatRecord.data.value += '!';
try {
// Update the HAT record. We have the option to pass multiple records with the array
const res = await hat.hatData().update([hatRecord]);
if (res.parsedBody) {
setNotes(prevNotes => {
const draft = [...prevNotes];
draft[noteIndex] = res.parsedBody[0];
return draft;
});
}
} catch (e) {
console.log(e.cause + e.status);
}
};
.
.
const ListNotes = () =>
notes
.sort((a, b) => new Date(b.data.dateCreated) - new Date(a.data.dateCreated))
.map((item, index) => {
return (
<li key={index}>
<div className={'note-row-wrapper'}>
.
.
<button type={'button'} onClick={() => updateData(item)}>
<i className={'material-icons'}>edit</i>
</button>
</div>
</li>
);
});
return (
<form ..>
..
</form>
);
}
export default Notes;

Delete data from the HAT

Using the unique record ID from the Note, we can delete it easily from the HAT:

// src/features/Notes/Notes.js
function Notes() {
.
.
const deleteData = async recordId => {
try {
// Delete a note with record ID. We can pass an array of multiple records IDs
const res = await hat.hatData().delete([recordId]);
if (res.parsedBody) {
setNotes(prevNotes => {
// Find index to remove from the Note array
const index = prevNotes.findIndex(note => note.recordId === recordId);
if (index !== -1) {
const draft = [...prevNotes];
draft.splice(index, 1);
return draft;
} else {
return prevNotes;
}
});
}
} catch (e) {
console.log(e.cause + e.status);
}
};
const ListNotes = () =>
notes
.sort((a, b) => new Date(b.data.dateCreated) - new Date(a.data.dateCreated))
.map((item, index) => {
return (
<li key={index}>
<div className={'note-row-wrapper'}>
<div className={'note-content'}>{item.data.value}</div>
<button type={'button'} onClick={() => deleteData(item.recordId)}>
<i className={'material-icons'}>delete</i>
</button>
.
.
</div>
</li>
);
});
return (
<form ..>
..
</form>
);
}
export default Notes;

To learn more detail about HAT Login, HAT-JS and storing data into the HAT please see the Javascript Developers Guide.