🤯 Building your first Neuro App with React
Tags: javascript, react, tutorial, webdev
Published: 2020-01-31 13:52:06

Most apps today change _state_ based on user intent. To be more specific, hand movements that translate into clicks, taps, presses, etc. Yet, every single intent starts in our brains. 

Today, we are going to build a different type of app. We'll build an app that changes _state_ based on your **cognitive state**.

Hear me out.

What if our app changed the motion of **a WebGL ocean** based on your _calm_ level? A "visual meditation" experience driven by the way you are feeling.

  - [View App](https://ocean.neurosity.co/)

The first step would be to measure and access such data.  And for that, we'll use a Notion headset.


![Tweet Introducing Notion](https://dev-to-uploads.s3.amazonaws.com/i/r8n43pl9ktvax4t583g7.png)
<figcaption>Tweet Introducing Notion</figcaption>

***


## Getting Started

Let's start by bootstrapping our app with Create React App (CRA). We open the project in VS Code and run the app locally.

  - `npx create-react-app mind-controlled-ocean`
  - `code mind-controlled-ocean`
  - `npm start`

If all goes well, you should see something like this:

![Create React App Default View](https://dev-to-uploads.s3.amazonaws.com/i/k1knwodmvrqogao5wvlf.png)
<figcaption>Create React App Default View<figcaption>


## 🔑 Authentication


We believe in privacy. That's why Notion is the first brain computer to feature authentication. Adding auth to the app is pretty straightforward. For this, we'll need a login form and 3 side effects to sync the authentication state. 

All you need to connect to your Notion brain computer is a [Neurosity account](https://console.neurosity.co/) and a Device ID. So, let's start by creating a new component for the login form that will collect this information.


```jsx
// src/components/LoginForm.js
import React, { useState } from "react";

export function LoginForm({ onLogin, loading, error }) {
  const [deviceId, setDeviceId] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  function onSubmit(event) {
    event.preventDefault();
    onLogin({ deviceId, email, password });
  }

  return (
    <form className="card login-form" onSubmit={onSubmit}>
      <h3 className="card-heading">Login</h3>
      {!!error ? <h4 className="card-error">{error}</h4> : null}
      <div className="row">
        <label>Notion Device ID</label>
        <input
          type="text"
          value={deviceId}
          disabled={loading}
          onChange={e => setDeviceId(e.target.value)}
        />
      </div>
      <div className="row">
        <label>Email</label>
        <input
          type="email"
          value={email}
          disabled={loading}
          onChange={e => setEmail(e.target.value)}
        />
      </div>
      <div className="row">
        <label>Password</label>
        <input
          type="password"
          value={password}
          disabled={loading}
          onChange={e => setPassword(e.target.value)}
        />
      </div>
      <div className="row">
        <button type="submit" className="card-btn" disabled={loading}>
          {loading ? "Logging in..." : "Login"}
        </button>
      </div>
    </form>
  );
}

```

> 🖍 Add styles the form - [here's the CSS I used](https://github.com/neurosity/notion-ocean/blob/master/src/global.css#L18-L99).

This component will hold the state of the `deviceId`, `email` and `password`. Additionally, our form component will accept an `onLogin` prop that will execute when the user clicks on the "Login" button. We'll also accept a `loading` prop for when the form submission is in progress, and an `error` message prop to be displayed when an error occurs.

Now that we've created our login component, let's add a login page that will make use of our new component.


```jsx
// src/pages/Login.js
import React, { useState, useEffect } from "react";
import { LoginForm } from "../components/LoginForm";

export function Login({ notion, user, setUser, setDeviceId }) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [isLoggingIn, setIsLoggingIn] = useState(false);

  function onLogin({ email, password, deviceId }) {
    if (email && password && deviceId) {
      setError("");
      setEmail(email);
      setPassword(password);
      setDeviceId(deviceId);
    } else {
      setError("Please fill the form");
    }
  }

  return (
    <LoginForm
      onLogin={onLogin}
      loading={isLoggingIn}
      error={error}
    />
  );
}
```

![Login form](https://dev-to-uploads.s3.amazonaws.com/i/aur340eejmmzjuf5fsgo.png)
<figcaption>Login Form Component<figcaption>


The goal of this page is to display the login form, add basic form validation via the `setError` function, and execute a login function. For the latter, let's add a side effect that will sync with `email`, `password` and the props received the page.


```jsx
useEffect(() => {
  if (!user && notion && email && password) {
    login();
  }

  async function login() {
    setIsLoggingIn(true);
    const auth = await notion
      .login({ email, password })
      .catch(error => {
        setError(error.message);
      });

    if (auth) {
      setUser(auth.user);
    }

    setIsLoggingIn(false);
  }
}, [email, password, notion, user, setUser, setError]);
```

You can think of `user` as the object that holds the auth user session set by the Notion API. So we are only calling our `login()` function if there is no auth session, we have a Notion instance in the state, and the user has submitted an email and password.


Very soon you'll find out how we'll receive the props: `notion, user, setUser, setDeviceId`. But before we do that, let's go back to our `App.js` and start putting it all together.


## ⚙️ App State

To keep this app simple, we'll just be using React's `useState` hook, the Reach Router, and a local storage hook brought to you by [react-use 👍](https://github.com/streamich/react-use). This means our general application state strategy will consist of keeping the global state at the App component level and passing down the necessary props to its child components.
  - `npm install @reach/router react-use`


We'll start with a single route, but we'll add 2 more routes as we continue to build the app.


```jsx
// src/App.js
import React, { useState, useEffect } from "react";
import { Router, navigate } from "@reach/router";
import useLocalStorage from "react-use/lib/useLocalStorage";
import { Login } from "./pages/Login";

export function App() {
  const [notion, setNotion] = useState(null);
  const [user, setUser] = useState(null);
  const [deviceId, setDeviceId] = useLocalStorage("deviceId");
  const [loading, setLoading] = useState(true);

  return (
    <Router>
      <Login
        path="/"
        notion={notion}
        user={user}
        setUser={setUser}
        setDeviceId={setDeviceId}
      />
    </Router>
  );
}
```

> 🏆 I found the [Reach Router](https://reach.tech/router) by Ryan Florence (and friends) to be the perfect balance between simplicity and predictability. Their documentation is extremely clear and allowed me to implement basic routing within minutes.

If you were wondering why have we decided to keep the `deviceId` in the local storage, it is because we'll need to access it before and after the user has logged in. It also makes a nicer user experience not to have to enter it multiple times.


## 🧠 Notion


Now that we have basic state management in place, let's integrate our app with _Notion_ by installing the API and importing it in `App.js`.
  - `npm install @neurosity/notion`


> 🤯 The Notion API enables full communication between the device and the app. We'll use it to get real-time feedback based on the user's cognitive state. For this app, we'll work specifically with the _calm_ API.


```js
import { Notion } from "@neurosity/notion";
```

Connecting to a Notion device is simple. We instantiate a new _Notion_ and pass the Device ID. We can add a side effect that sets the instance to the App component state by syncing with `deviceId`. 

📖 You can find the full Notion documentation at [docs.neurosity.co](http://docs.neurosity.co).

```jsx
useEffect(() => {
  if (deviceId) {
    const notion = new Notion({ deviceId }); // 😲
    setNotion(notion);
  } else {
    setLoading(false);
  }
}, [deviceId]);
```

Another state we want to sync is the `user` state. 

In the following example, we'll add a side effect that syncs with the value of the `notion` instance. If `notion` hasn't been set yet, then we'll skip subscribing to _calm_ events until the `notion` instance is created.


```jsx
useEffect(() => {
  if (!notion) {
    return;
  }

  const subscription = notion.onAuthStateChanged().subscribe(user => {
    if (user) {
      setUser(user);
    } else {
      navigate("/");
    }
    setLoading(false);
  });

  return () => {
    subscription.unsubscribe();
  };
}, [notion]);
```


If the app has an active user session persisted by the Notion authentication, we'll want to get the current logged in user, and set it to the state in our App component.

The `onAuthStateChanged` method returns an observable of user auth events. It is important to note that when using the Notion API in the browser, the session will persist via local storage. So, if you close the app, or reload the page, the session will persist and `onAuthStateChanged` will return the user session instead of `null`. This is exactly what we want.


If no session is detected we can navigate to the login page. Otherwise, set `user` in the component's state.


We can complete full authentication by adding a Logout page.


```jsx
// src/pages/Logout.js
import { useEffect } from "react";
import { navigate } from "@reach/router";

export function Logout({ notion, resetState }) {
  useEffect(() => {
    if (notion) {
      notion.logout().then(() => {
        resetState();
        navigate("/");
      });
    }
  }, [notion, resetState]);

  return null;
}
```

The logout page is simply a React component with no DOM elements. The only logic we need is a side effect that will call the `notion.logout()` method if the `notion` instance is present. Lastly, it redirects the user to the initial route after logging out.

This component can now be added as a route in `App.js`.

```jsx
// src/App.js
// ...
import { Logout } from "./pages/Logout";
// ...

return (
  <Router>
    {/* ... */}
    <Logout path="/logout" notion={notion} resetState={() => {
      setNotion(null);
      setUser(null);
      setDeviceId("");
    }} />
  </Router>
);
```

Now that authentication is completed, let's add App logic based on our *cognitive state*!


## 🌊 WebGL Ocean

The moment I saw [David](http://david.li/)'s WebGL ocean, I fell in love with it. So using Notion to influence the weather driving the ocean waves felt like a fun experiment.


> 💡 Fun fact: This ocean wave simulation was created in 2013 by David Li with JavaScript and WebGL. It has been featured as part of Google Experiments. I was happy to see it was open-sourced under the MIT license.

For this next part, the idea is to create a new component that will use the WebGL ocean. So let's create a directory called Ocean (`./src/components/Ocean`) and add the following files to it.
  - [simulation.js](https://github.com/neurosity/notion-ocean/blob/master/src/components/Ocean/simulation.js)
  - [weather.js](https://github.com/neurosity/notion-ocean/blob/master/src/components/Ocean/weather.js)
  - Ocean.js:


```js
// src/components/Ocean/Ocean.js
import React, { useState, useEffect, useRef } from "react";
import useRafState from "react-use/lib/useRafState";

import { Simulator, Camera } from "./simulation.js"; // by David Li
import { mapCalmToWeather } from "./weather.js";

const camera = new Camera();

export function Ocean({ calm }) {
  const ref = useRef();
  const [simulator, setSimulator] = useState();
  const [lastTime, setLastTime] = useRafState(Date.now());

  useEffect(() => {
    const { innerWidth, innerHeight } = window;
    const simulator = new Simulator(ref.current, innerWidth, innerHeight);
    setSimulator(simulator);
  }, [ref, setSimulator]);

  useEffect(() => {
    if (simulator) {
      const currentTime = Date.now();
      const deltaTime = (currentTime - lastTime) / 1000 || 0.0;
      setLastTime(currentTime);
      simulator.render(deltaTime, camera);
    }
  }, [simulator, lastTime, setLastTime]);

  return <canvas className="simulation" ref={ref}></canvas>;
}
```

And if all goes well, we should see this.

![Ocean Wave Simulation](https://dev-to-uploads.s3.amazonaws.com/i/xkjalcz4a87rfy0o63xt.gif)
<figcaption>Ocean Wave Simulation<figcaption>


Let me break down what's happening here.
  - 1️⃣ The React component returns a canvas element for the WebGL 3D scene
  - 2️⃣ We use React's `useRef` to access the canvas HTML element
  - 3️⃣ We instantiate a new `Simulator` when the reference changes. The `Simulator` class is responsible for controlling rendering, and the weather properties such as _wind_, _choppiness_, and _size_.
  - 4️⃣ We use the `useRaf` (requestAnimationFrame) hook to create a loop where the callback executes on every animation frame.

At this point, our ocean waves move based on static weather values: _choppiness_, _wind_, and _size_. So, how do we map these weather settings based on the `calm` score?


For that, I've created a utility function in `weather.js` for mapping the _calm_ score to its corresponding weather settings: _choppiness_, _wind_, and _size_. And then, we can create a side effect that syncs every time the `calm` score changes.

```js
useEffect(() => {
  if (simulator) {
    setWeatherBasedOnCalm(animatedCalm, 0, 0);
  }

  function setWeatherBasedOnCalm(calm) {
    const { choppiness, wind, size } = mapCalmToWeather(calm);
    simulator.setChoppiness(choppiness);
    simulator.setWind(wind, wind);
    simulator.setSize(size);
  }
}, [calm, simulator]);
```

## Cognitive State

This is the fun part. This is where we get to access brain data and map it to the app state.


By subscribing to `notion.calm()`, we get a new `calm` score approximately every second. So, let's add the `<Ocean calm={calm} />` component, add `calm` as a prop and create a side effect that syncs with the instance of `notion` and with `user`. If these two states are present, then we can safely subscribe to _calm_.

> 🧘🏿‍♀️ The calm score is derived from your passive cognitive state. This metric based on the [alpha wave](https://en.wikipedia.org/wiki/Alpha_wave). The calm score ranges from `0.0` to `1.0`. The higher the score, the higher the probability a _calm_ feeling is detected. Getting a calm score over `0.3` is significant. Things that can help increase the calm score include closing your eyes, keeping still, breathing deeply, or meditating.


```jsx
// src/pages/Calm.js
import React, { useState, useEffect } from "react";
import { Ocean } from "../components/Ocean/Ocean";

export function Calm({ user, notion }) {
  const [calm, setCalm] = useState(0);

  useEffect(() => {
    if (!user || !notion) {
      return;
    }

    const subscription = notion.calm().subscribe(calm => {
      const calmScore = Number(calm.probability.toFixed(2));
      setCalm(calmScore);
    });

    return () => {
      subscription.unsubscribe();
    };
  }, [user, notion]);

  return (
    <Ocean calm={calm} />
  );
}
```

> 💡 All notion metrics, including `notion.calm()` return an RxJS subscription that we can use to safely unsubscribe when the component unmounts.

And finally, we add our Calm page to `App.js`.

```jsx
// src/App.js
// ...
import { Calm } from "./pages/Calm";
// ...

// If already authenticated, redirect user to the Calm page
useEffect(() => {
  if (user) {
    navigate("/calm");
  }
}, [user]);

return (
  <Router>
    {/* ... */}
    <Calm path="/calm" notion={notion} user={user} />
  </Router>
);
```

And with that, our Neuro React App is now complete.

* [View full code](https://github.com/neurosity/notion-ocean)

{% github neurosity/notion-ocean no-readme %}


***

I'm excited about app experiences that are influenced by who we are as a person. Every brain is different, yet we keep building apps that present the same experience to every user. What if apps were tailored to _you_?.

What if apps could help you relax when you are stressed?

What if you could authenticate an app with your brainwaves?

What if video games could change their narrative based on your feelings? 

What if...