Creating a workout tracking app with React Native and Fauna
Tags: fauna, reactnative, react, javascript
Published: 2022-01-06 14:08:58

There are lots of options when it comes to databases in React Native, either cloud or offline. But I don't see a lot of tutorials showing how to use Fauna within a React Native. That's why in this tutorial, I'll be showing you how it can be done.

For those who are new to Fauna, it's a cloud database that supports relations, documents and graphs. It also supports real-time streaming and GraphQL. If you're in need of a serverless solution for your database needs then consider using Fauna.

As a practical use case, I'll be walking you through how to create a workout tracking app with React Native and Fauna.


## App overview

The app that we will be creating allows for recording the exercises performed by the user on a given day. 

First, we have the exercises screen. This is where the user can add new exercises that they want to keep track of. Each exercise is classified by category and muscle being worked by the exercise. In this case, there's only one category: weight and reps. This allows the user to track the weight they're lifting and the number of repetitions they have lifted it: 



![exercises screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xxn0azu6lkbthpemihmu.png)

Here's what shows up when the user clicks on the **ADD** button:


![add exercise modal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m2wu2t26u1sgq8wwkqzv.png)

Next, we have the current workout screen. This is where the user can add a new workout. To get to this screen, they first have to go to the exercises screen and select an existing exercise. From there, they can enter the weight and reps they performed. This is considered "one set" of the workout. A workout can have as many sets as the user wants:

![current workout](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uuxka3zjy53wwu626wjx.png)

Lastly, we have the workout history screen where the user can keep track of the workouts they performed for a specific exercise. This is grouped into days, sorted from latest to oldest:

![workout history](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7bct13l4oyglr58whh7v.png)

You can view the source code of this app on its [GitHub repo](https://github.com/anchetaWern/RNWorkoutFauna).


## Prerequisites

To follow this tutorial, you need to have the following installed on your machine:


- React Native CLI 2.0.1
- Node 14.18.0
- npm 6.14.15
- Xcode 13.0

Experience in JavaScript, React and React Native is also required. This isn't a "building from scratch" tutorial where we build the app from scratch, showing all of the code. We will only focus on the crucial parts such as navigation and working with Fauna. We won't be going through the code for the different components or their styling, so I expect you’ll download the source code from GitHub for this tutorial.

Lastly, a Fauna account is required. Go ahead and [sign up for an account](https://dashboard.fauna.com/accounts/register) if you don't have one already.


## Setting up Fauna

Once you're inside the Fauna dashboard, the first thing to do is create a new database:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kyc0gb1x09pt9io5lvcx.png)

You can also set it to whichever you like. Be sure to use the [correct connection domain](https://docs.fauna.com/fauna/current/learn/understanding/region_groups.html?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_WorkoutTrackerReactNative_WAncheta) once you connect the app.

### Create Collections

The next step is adding the collections. If you're coming from a relational database background, these are pretty much the same as tables in a database. It's basically a way of storing related data together. We only need two collections for this app:


- **exercises** - for storing exercises
- **workouts** - for storing workout history


![create new collection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yk714glrhdtgsbodrk2t.png)

Create one for the exercises. Leave the defaults as they are then click **SAVE**:


![create exercises collection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n95dlswxvl31g75ab0oe.png)

And another for the workouts:


![create workouts collection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i24sv3hku03c4b7egyvt.png)

### Generating an access key

We won't be able to talk to the database from the app without creating an access key. Click on the **Security** menu and click **NEW KEY**:


![update security settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h2grkks741vrfdi183ku.png)

Select the database, and set the role to Server then click **SAVE**:


![create new key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lia8cvv8f9x9gotqyesw.png)

Once created, copy the secret key somewhere safe because it won't be displayed again. We'll be adding this key to the app later:


![fauna key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rz2arinognniwj2887nq.png)

### Creating a custom role

The final step in setting up Fauna is creating a custom role. Still on the **Security** page, click on the **Roles** tab and click on **NEW CUSTOM ROLE** button:


![create a role](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gblv8qbpvqrf9o5z5j0p.png)

Set the name as `MyCustomRole` and from the collections dropdown, select both the exercises and workouts collection then check all the privileges. Note that this is just for testing purposes so we won't have any issues when it comes to permissions. In a production app, you [have to check only the privileges your app is using](https://docs.fauna.com/fauna/current/security/roles?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_WorkoutTrackerReactNative_WAncheta#pco):


![create custom role](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vghohgdjkl5faw0hggez.png)

Once that's done, don't forget to click on **SAVE**.

That's it for now. Later on, you'll need to update this role again to give it permission for the indexes that we'll be creating.


## Bootstrapping the app

Start by generating a new React Native project:

```bash
npx react-native init RNFaunaWorkout
```

Once that's done, you should have a new `RNFaunaWorkout` folder in your current working directory. Navigate inside that folder. That will be the root directory for all the commands and file paths that I'll be referring to in this tutorial.


## Installing the dependencies

Our project depends on the following packages:


- [FaunaDB](https://www.npmjs.com/package/faunadb) - the JavaScript driver for Fauna.
- [React Navigation](https://reactnavigation.org/) - for implementing navigation in the app.
- [React Native Paper](https://callstack.github.io/react-native-paper/) - for implementing cross-platform Material Design in React Native.

  
Here are the commands for installing the dependencies. First is Fauna:

```bash
npm install faunadb
```

Next, install React Navigation and its dependencies:

```bash
npm install @react-navigation/native
npm install react-native-screens react-native-safe-area-context
npm install @react-navigation/material-top-tabs react-native-tab-view
npm install react-native-pager-view
```

Next, install React Native Paper:

```bash
npm install react-native-paper
npm install react-native-vector-icons
```

Once that's done, link the native dependencies for React Native Vector Icons:

```bash
react-native link react-native-vector-icons
```

If you're on Mac, also execute the following to install the iOS dependencies:

```bash
npx pod-install
```

Be sure to follow any additional installation instructions mentioned in the documentation for each package we installed.

At this point, you should be able to run the app:

```bash
npx react-native run-android
```

For iOS, you need to open the `.xcworkspace` file in the `ios` directory. This will launch the iOS project in Xcode. Simply run the app from there.



## Building the app

As mentioned earlier, we're not actually building the app from scratch. We'll only go through the most relevant parts in this tutorial. I'll leave it to you to check out the code that we won't be going through in the GitHub repo.

First, we have the `index.js` file. It's where we set up the React Native Paper theme:

```javascript
// index.js
import * as React from "react";
import { AppRegistry } from "react-native";
import { DefaultTheme } from "@react-navigation/native";
import { Provider as PaperProvider } from "react-native-paper";
import App from "./App";
import { name as appName } from "./app.json";

const theme = {
  ...DefaultTheme,
  dark: true,
  roundness: 10,
  colors: {
    ...DefaultTheme.colors,
    text: "#333",
    background: "#ccc",
    gray: "#858585",
    white: "#fff",
    default: "#f2f2f2",
  },
  fonts: {
    ...DefaultTheme.fonts,
    small: 15,
    regular: 16,
    big: 20,
    icon: 30,
  },
};

export default function Main() {
  return (
    <PaperProvider theme={theme}>
      <App />
    </PaperProvider>
  );
}

AppRegistry.registerComponent(appName, () => Main);
```

Then in the `App.js` we're wrapping the app's Root component with `AppContextProvider`. As you'll see later, this will provide global state that will be used throughout the app:

```javascript
// App.js
import React from "react";
import type { Node } from "react";
import {
  SafeAreaView,
  StatusBar,
  useColorScheme,
  StyleSheet,
} from "react-native";
import Root from "./Root";

import { AppContextProvider } from "./src/context/AppContext";

const App: () => Node = () => {

  const isDarkMode = useColorScheme() === "dark";

  return (
    <SafeAreaView style={styles.root}>
      <StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
      <AppContextProvider>
        <Root />
      </AppContextProvider>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  root: {
    flex: 1,
    justifyContent: "center",
  },
});

export default App;
```

### Root Component

The Root component is where we set up the screens that will be used throughout the app. We're using a [bottom tab navigator](https://reactnavigation.org/docs/bottom-tab-navigator/) as the root navigator. Inside it are the exercises screen and the workout screen. Both of these screens has an "add" button on the right header position. The one for the exercises screen toggles the value for the `isAddingExercise` in the global app state. As you'll see later, this state value is used to determine whether to show the modal for creating a new exercise or not. On the other hand, the "add" button for the workout screen is used to navigate to the exercises screen. Because to start recording a new workout session, the user has to select an exercise first. The main purpose of having a separate tab for the workout screen is for easy access when the user has already selected an exercise:

```javascript
// Root.js
import React, { useContext } from "react";
import { NavigationContainer } from "@react-navigation/native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { Button, withTheme } from "react-native-paper";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import ExercisesScreen from "./src/screens/ExercisesScreen";
import WorkoutTabScreen from "./src/screens/WorkoutTabScreen";
import { AppContext } from "./src/context/AppContext";

const Tab = createBottomTabNavigator();

function getHeaderTitle(route) {
  // ..
}

function Root({ theme }) {
  const { colors, fonts } = theme;
  const { setIsAddingExercise, setIsAddingWorkout } = useContext(AppContext);
  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen
          name="Exercises"
          component={ExercisesScreen}
          options={{
            tabBarLabel: "Exercises",
            tabBarIcon: ({ color, size }) => (
              <MaterialCommunityIcons
                name="dumbbell"
                color={colors.gray}
                size={fonts.icon}
              />
            ),
            headerRight: () => (
              <Button
                icon="plus"
                color={colors.text}
                onPress={() => setIsAddingExercise(true)}
              >
                Add
              </Button>
            ),
          }}
        />
        <Tab.Screen
          name="Workout"
          options={({ route, navigation }) => ({
            tabBarLabel: "Workout",
            headerTitle: getHeaderTitle(route),
            tabBarIcon: ({ color, size }) => (
              <MaterialCommunityIcons
                name="weight-lifter"
                color={colors.gray}
                size={fonts.icon}
              />
            ),
            headerRight: () => (
              <Button
                icon="plus"
                color={colors.text}
                onPress={() => navigation.navigate("Exercises")}
              >
                Add
              </Button>
            ),
          })}
          component={WorkoutTabScreen}
        />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

export default withTheme(Root);
```

The `getHeaderTitle()` function is used for showing a different title for the workout screen based on the exercise selected by the user. You might be wondering why it's `route.params.params` instead of just `route.params`. That's because the data is being passed to the nested screen as you'll see later:

```javascript
function getHeaderTitle(route) {
  if (route.params) {
    const exercise_name = route.params.params.exercise.name;
    return exercise_name.length > 25
      ? exercise_name.substr(0, 25) + ".."
      : exercise_name;
  }
  return "Workout";
}
```

### AppContext

The AppContext is what provides the global app state. This allows us to use the data that was set from the current screen in another screen:

```javascript
// src/context/AppContext.js
import React, { useState } from "react";
const AppContext = React.createContext();

const AppContextProvider = (props) => {
  const [isAddingExercise, setIsAddingExercise] = useState(false); // whether to show the add exercise modal or not
  const [workoutHistory, setWorkoutHistory] = useState([]); 

  const value = {
    isAddingExercise,
    setIsAddingExercise,
    workoutHistory,
    setWorkoutHistory,
  };

  return (
    <AppContext.Provider value={value}>{props.children}</AppContext.Provider>
  );
};

export { AppContext, AppContextProvider };
```

### Connecting to Fauna

Before we proceed with the code for each of the screens, let's first take a look at the code for connecting to Fauna and manipulating the database:

```javascript
// src/config/db.js
import faunadb from "faunadb";
const client = new faunadb.Client({
  secret: "YOUR FAUNA SECRET",
  domain: "YOUR FAUNA DOMAIN",
});

const q = faunadb.query;
export { client, q };
```

That's really all there is to it when it comes to connecting to Fauna. Just supply the `secret` and the `domain` where your database instance is hosted. If you selected "United States" earlier, the connection domain should be `db.us.fauna.com`. If you selected anything else, check out the [docs on region groups](https://docs.fauna.com/fauna/current/learn/understanding/region_groups.html?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_WorkoutTrackerReactNative_WAncheta). If you scroll down near the bottom, you'll find a table showing the region group and their corresponding connection domain.

### Get Exercises

Now we're ready to make some queries to the database. First, let's take a look at the query for getting the exercises:

```javascript
// src/data/index.js
import {client, q} from '../config/db';
export const getExercises = () => {
  return client
    .query(q.Paginate(q.Match(q.Ref('indexes/exercises_index'))))
    .then(response => {
      const exercises_ref = response.data;
      const getAllDataQuery = exercises_ref.map(ref => {
        return q.Get(ref);
      });
      return client.query(getAllDataQuery).then(data => data);
    })
    .catch(error => console.error('Error: ', error.message));
};
```

Fauna uses [FQL](https://docs.fauna.com/fauna/current/api/fql/?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_WorkoutTrackerReactNative_WAncheta) (Fauna Query Language) as its query language. We won't be diving into it in this tutorial since there are already existing guides for that. If you're up for some reading, I recommend the following:

- [Getting started with FQL, Fauna’s native query language - part 1: a look at FQL and fundamental Fauna concepts](https://fauna.com/blog/getting-started-with-fql-faunadbs-native-query-language-part-1)
- [Getting started with FQL, Fauna’s native query language - part 2: a deep dive into indexes with Fauna](https://fauna.com/blog/getting-started-with-fql-faunadbs-native-query-language-part-2)

It's not required for you to read the articles above to follow this tutorial, but its definitely recommended.

The `faunadb` package we installed earlier provides the JavaScript API for FQL. This means that the function calls we made above basically looks similar to FQL in its raw form (eg. when you execute it via the Fauna console). If you check out the [FQL API cheat sheet](https://docs.fauna.com/fauna/current/api/fql/cheat_sheet), you'll see the same methods we used above:

- [query](https://docs.fauna.com/fauna/current/api/fql/functions/query?lang=javascript)
- [paginate](https://docs.fauna.com/fauna/current/api/fql/functions/paginate?lang=javascript)
- [match](https://docs.fauna.com/fauna/current/api/fql/functions/match?lang=javascript)
- [ref](https://docs.fauna.com/fauna/current/api/fql/functions/ref?lang=javascript)

In most cases, you'll have to use `client.query(q.Paginate(q.Match(q.Ref('YOUR INDEX'))))` to fetch data from the database.

In the following code, we're getting the data from the `exercises` index:

We haven't really created it yet, so go to the Fauna dashboard and click on the **Indexes** menu then click on **NEW INDEX**:


![create new index](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4cerudioat93bzdclrpd.png)

Select the exercises collection and set the name of the index to `exercises_index`. Leave the defaults as it is then click **SAVE**:


![create exercises index](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ztjl05jj9u9bgieym06p.png)

Next, you need to give permission to the role you created earlier to access this index. To do that, go to the **Security** page and click on the **Roles** tab then click on the gear icon on the role. This should redirect you to the same page as below. In the **Indexes** section, select the exercises index and check all the privileges:


![add indexes permission](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ido2e10p9jd27yr1x510.png)

Once that's done, the code for getting data from the exercises index should work. Note that `response.data` doesn't actually contain the data. All it returns is the reference to the data. That's why we have to use JavaScript's `map()` function to go through the results and call `q.Get()` on each to construct the query for getting the data for each row. The call to `client.query(getAllDataQuery)` is what returns the actual data:

```javascript
return client
    .query(q.Paginate(q.Match(q.Ref('indexes/exercises_index'))))
    .then(response => {
      const exercises_ref = response.data;
      const getAllDataQuery = exercises_ref.map(ref => {
        return q.Get(ref);
      });
      return client.query(getAllDataQuery).then(data => data);
    })
    .catch(error => console.error('Error: ', error.message));
```

### Save Exercise

Creating a new exercise record is a lot simpler. We just need to use the `q.create()` method and pass in the collection as the first argument, and an object containing a `data` object which contains the data you want to save:

```javascript
// src/data/index.js
export const saveExercise = (name, category, primary_muscle) => {
  return client
    .query(
      q.Create(q.Collection('exercises'), {
        data: {
          name,
          category,
          primary_muscle,
        },
      }),
    )
    .then(ret => ret)
    .catch(error => console.error('Error: ', error.message));
};
```

### Get Workouts by Exercise

Getting workouts for a specific exercise is also required as we can't be fetching all the workouts that had been saved ever since the user started recording. It's bad for performance reasons.

To achieve this, we need to create another index. This time, let's use the shell to create the index. Go ahead and click on the **Shell** menu in the Fauna dashboard. That should show you the following screen. The larger box on the top is where the output of whatever commands you execute in the shell is shown. While the one below it is where you type in the commands. Clear it out for now:


![Fauna Shell](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3fpl2of21qf3jf3bf7xt.png)

Enter the following command in the shell:

```javascript
CreateIndex({
  name: "all_workouts_by_exercise_id",
  source: Collection("workouts"),
  terms: [
    { field: ["data", "exercise_id"]}
  ]
})
```

Make sure to run it as the role you created earlier. You can change that by clicking on the **RUN AS** button and selecting the role. The above code is the [standard code for creating an index in Fauna](https://docs.fauna.com/fauna/current/api/fql/functions/createindex?lang=javascript). The `CreateIndex()` function accepts an object containing the following properties:


- `name` - the machine-friendly name for the index. 
- `source` - the source collection.
- `terms` - an [array of term objects](https://docs.fauna.com/fauna/current/api/fql/indexes?lang=javascript#term) describing the fields that should be searchable.

Here's the code. It looks pretty much the same as the code for getting the exercises, the only difference is that we're passing a second argument to `q.Match()`. This value will be used as the value for the term you added:

```javascript
// src/data/index.js
export const getWorkoutsByExercise = exercise_id => {
  return client
    .query(
      q.Paginate(
        q.Match(q.Ref('indexes/workouts_by_exercise_id_index'), exercise_id),
      ),
    )
    .then(response => {
      const workouts_ref = response.data;
      const getAllDataQuery = workouts_ref.map(ref => {
        return q.Get(ref);
      });
      return client.query(getAllDataQuery).then(data => data);
    })
    .catch(error => console.error('Error: ', error.message));
};
```

We can actually implement the same thing without using an index, but since we already know we will need this exact query in the app, it's better to create an index for it instead. That way we don't have to construct the query for it everytime we need it.

### Create Workout

Next is the code for creating a new workout. It looks pretty much the same as the code for creating a new exercise, only this time we're saving the data to the `workouts` collection. We also need to save the timestamp. Fauna actually saves a timestamp for each document already. But that one is attached to the database itself and is used for the temporal stuff. It also [provides date and time functions](https://fauna.com/blog/core-fql-concepts-part-1-working-with-dates-and-times) but we also won't be using that. To keep things simple, we're gonna use good old `new Date()` to get the unix timestamp and storing it along with the other data we need to store for each workout: 

```javascript
// src/data/index.js
export const saveWorkout = (exercise_id, weight, reps) => {
  const time_created = Math.round(new Date().getTime() / 1000);
  return client
    .query(
      q.Create(q.Collection('workouts'), {
        data: {
          exercise_id,
          weight,
          reps,
          time_created,
        },
      }),
    )
    .then(ret => console.log('created workout: ', ret))
    .catch(error => console.error('Error: ', error.message));
};
```

### Update Workout

Updating a workout also looks the same as creating a workout. The only difference is that we need to pass in the document's ref ID as a second argument to the `q.Update()` function. Note that the data you pass in doesn't have to contain all the fields (with their updated values) that were present when you created the document. That's why we're only specifying the `weight` and `reps` here:

```javascript
// src/data/index.js
export const updateWorkout = (id, weight, reps) => {
  return client
    .query(
      q.Update(q.Ref(q.Collection('workouts'), id), {
        data: {
          weight,
          reps,
        },
      }),
    )
    .then(ret => console.log('updated workout: ', ret))
    .catch(error => console.error('Error: ', error.message));
};
```

### Delete Workout

Lastly, here's the code for deleting a workout. All we need is to pass in the ref ID as a second argument to `q.Ref()`. That should delete the corresponding document in the collection you specified as the first argument:

```javascript
// src/data/index.js
export const deleteWorkout = id => {
  return client
    .query(q.Delete(q.Ref(q.Collection('workouts'), id)))
    .then(ret => console.log('deleted workout'))
    .catch(err => console.error('Error: %s', err));
};
```

### Data Formatter Helper

The data formatter is our nifty little helper which allows us to manipulate the raw data that we get from Fauna to a format which best suits our needs. It's also responsible for filtering the workout history to only the one's performed by the user in the current day:

```javascript
// src/helpers/DataFormatter.js
import groupBy from 'lodash.groupby';
import {fromUnixTime, format} from 'date-fns';

function getGroupedWorkouts(res) {
  const formatted_workouts = res.map(item => {
    const {exercise_id, weight, reps, time_created} = item.data;
    const date = format(fromUnixTime(time_created), 'yyyy-MM-dd');

    return {
      id: item.ref.id,
      exercise_id,
      weight,
      reps,
      date,
      time_created,
    };
  });

  return groupBy(formatted_workouts, 'date');
}

export const groupWorkouts = res => {
  return getGroupedWorkouts(res);
};

export const filterTodaysWorkout = grouped => {
  const today = format(new Date(), 'yyyy-MM-dd');
  return grouped[today] ? grouped[today] : [];
};
```

### Exercises Screen

Now, let's proceed with the exercises screen. First, import the modules we need. `getExercises`, `getWorkoutsByExercise`, and `saveExercise` allows us to interact with the Fauna database. While `groupWorkouts` is for formatting the data so that it can easily be presented in the UI:

```javascript
// src/screens/ExercisesScreen.js
import React, { useState, useEffect, useContext } from "react";
import { View, TextInput, StyleSheet } from "react-native";
import { List, withTheme } from "react-native-paper";
import AddExerciseModal from "../components/AddExerciseModal";
import { getExercises, getWorkoutsByExercise, saveExercise } from "../data";
import { AppContext } from "../context/AppContext";
import { groupWorkouts } from "../helpers/DataFormatter";
```

Inside the `ExercisesScreen` component, we have some state for storing the exercises, filtered exercises, and the exercise being searched by the user. Filtered exercises are simply the exercises that has been filtered based on the value of `searchExercise`. The filtered exercises is what's going to be displayed in the UI:

```javascript
function ExercisesScreen({ navigation, theme }) {
  const { fonts, colors } = theme;
  
  const [exercises, setExercises] = useState([]);
  const [filteredExercises, setFilteredExercises] = useState([]);
  const [searchExercise, setSearchExercise] = useState("");
}
```

Next, we extract the methods we need from the AppContext:

```javascript
const {
  // for toggling the create exercise modal visibility
  isAddingExercise, 
  setIsAddingExercise,
  setWorkoutHistory, // for updating the state with the current workout history being viewed
} = useContext(AppContext);
```

Once the component has loaded, that's the time we fetch the exercise data from the database then update the state:

```javascript
useEffect(() => {
  getExercises().then((res) => {
    setExercises(res);
    setFilteredExercises(res);
  });
}, []);
```

We also have another useEffect, this time for listening for changes on searchExercise. Here, we're filtering the exercises based on its value:

```javascript
useEffect(() => {
  const filtered = exercises.filter((item) => {
          return item.data.name.startsWith(searchExercise);
  });
        setFilteredExercises(filtered);
}, [searchExercise]);
```

Next, the `createExercise` function is executed when the user clicks on the "create" button on the add exercise modal. All it does is call the `saveExercise()` function for interacting with the FaunaDB database, then calls `getExercises()` function to update the UI with the updated data:

```javascript
const createExercise = (name, category, primary_muscle) => {
    saveExercise(name, category, primary_muscle).then(() => {
      getExercises().then((res) => {
        setExercises(res);
        setFilteredExercises(res);
      });
    });
    setIsAddingExercise(false);
};
```

The `goToWorkout` function is executed when the user clicks on any exercise on the list. This makes a request to Fauna to get the workout history for a particular exercise then updates the global state with it. Navigation works a bit differently because the workout screen is actually a tab navigator. This means that it has other screens under it. That's why aside from the name of the workout screen, we also need to pass in the name of the screen under it. In this case, it's `CurrentWorkout`. Then we pass in the parameters we want to pass via the `params` property. That's the reason why this specific data had to be accessed under `route.params.params` as you've seen earlier in the `getHeaderTitle()` function:

```javascript
const gotoWorkoutScreen = (item_id, item_data) => {
    getWorkoutsByExercise(item_id).then((res) => {
      const grouped_workouts = groupWorkouts(res);
      setWorkoutHistory(grouped_workouts);
    });
    navigation.navigate("Workout", {
      screen: "CurrentWorkout",
      params: {
        exercise_id: item_id,
        exercise: item_data,
      },
    });
};
```

Lastly, here's the the UI for the exercises screen. All it contains is the search field for searching an exercise, the list of exercises, and the add exercise modal:

```javascript
return (
    <View style={styles.container}>
      <View style={[styles.box, styles.searchContainer]}>
        <TextInput
          value={searchExercise}
          placeholder="Search Exercise"
          onChangeText={(text) => setSearchExercise(text)}
          style={[styles.input, { backgroundColor: colors.white }]}
        />
      </View>
      <View style={styles.box}>
        {filteredExercises.map((item) => {
          return (
            <List.Item
              title={item.data.name}
              description={item.data.muscle}
              key={item.data.name}
              onPress={() => gotoWorkoutScreen(item.ref.id, item.data)}
            />
          );
        })}
      </View>
      <AddExerciseModal
        isAddingExercise={isAddingExercise}
        setIsAddingExercise={setIsAddingExercise}
        createExercise={createExercise}
      />
    </View>
);
```

I'll leave it to you to check out the complete code in the GitHub repo for the styles.

### Workout Tab Screen

The workout tab screen is just a tab navigator with two screens on it: the current workout screen and the workout history screen. We need to pass in the `initialParams` prop on each screen so that they inherit whatever navigation params is passed to their parent:

```javascript
// src/screens/WorkoutTabScreen.js
import React, { useState } from "react";
import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";
import WorkoutScreen from "./WorkoutScreen";
import WorkoutHistoryScreen from "./WorkoutHistoryScreen";

const Tab = createMaterialTopTabNavigator();

function WorkoutTabScreen({ route }) {
  return (
    <Tab.Navigator>
      <Tab.Screen
        initialParams={route.params}
        name="CurrentWorkout"
        options={{
          title: "Today",
        }}
        component={WorkoutScreen}
      />
      <Tab.Screen
        initialParams={route.params}
        name="WorkoutHistory"
        options={{
          title: "History",
        }}
        component={WorkoutHistoryScreen}
      />
    </Tab.Navigator>
  );
}
export default WorkoutTabScreen;
```

### Workout Screen

The workout screen is where all the workout recording happens. This is where the user can create new workouts, and update or delete existing ones:

```javascript
// src/screens/WorkoutScreen.js
import React, { useState, useContext, useEffect } from "react";
import { View, Text, TextInput, ScrollView, StyleSheet } from "react-native";
import { Button, IconButton, withTheme } from "react-native-paper";
import { useRoute } from "@react-navigation/native";
import SetItem from "../components/SetItem";
import {
  saveWorkout,
  updateWorkout,
  deleteWorkout,
  getWorkoutsByExercise,
} from "../data";
import { groupWorkouts, filterTodaysWorkout } from "../helpers/DataFormatter";
import { AppContext } from "../context/AppContext";
```

Inside the component, we extract the `workoutHistory` and `setWorkoutHistory` from the global state. Because as you've seen in the exercises screen earlier, we're actually calling the `setWorkoutHistory()` function to update the global state with the workout history of the exercise clicked on by the user. So we're basically just reading that here. Below that, we have a few state variables for keeping track of the currently selected set (`selectedSet`), the index of the selected set (`selectedIndex`), the weight (`weight`), repetitions (`reps`), and an array containing the workout for the current day for that specific exercise:

```javascript
function WorkoutScreen({ navigation, theme }) {
  const route = useRoute();
  const { colors, fonts } = theme;

  const {
    workoutHistory,
    setWorkoutHistory,
  } = useContext(AppContext);

  const [selectedSet, setSelectedSet] = useState(null);
  const [selectedIndex, setSelectedIndex] = useState(null);
  const [weight, setWeight] = useState(0);
  const [reps, setReps] = useState(0);
  const [todaysWorkout, setTodaysWorkout] = useState([]);
  const currentAction = selectedIndex !== null ? "Update" : "Add";
  const disableDelete = selectedIndex !== null ? false : true;
}
```

Next, we have the functions for incrementing or decrementing the weight and reps:

```javascript
const increment = (type, value) => {
  if (type === "weight") {
    setWeight(weight + 1);
  } else if (type === "reps") {
    setReps(reps + 1);
  }
};

const decrement = (type, value) => {
  if (value >= 1) {
    if (type === "weight") {
      setWeight(value - 1);
    } else if (type === "reps") {
      setReps(value - 1);
    }
  }
};
```

Next, we have the function that gets called when the user clicks on a specific set. In this case, we need to update the `selectedSet` and `selectedIndex` to match. The `weight` and `reps` field also need to be updated based on the weight and reps for that set. This will then allow us to update the details for that set:

```javascript
const selectSet = (item, index) => {
  setSelectedSet(item);
  setSelectedIndex(index);
  setWeight(parseInt(item.weight));
  setReps(parseInt(item.reps));
};
```

Next is the function that gets called everytime the user creates a new workout entry or updates an existing entry. We can determine which one needs to be executed based on the presence of the `selectedIndex` in the state. If it's present then we're updating a workout entry. Otherwise, we're creating a new entry:

```javascript
const saveAction = () => {
  if (selectedIndex !== null) {
    updateWorkout(selectedSet.id, weight, reps).then(() =>
      syncWorkoutHistory()
    );
  } else {
    if (route.params) {
      saveWorkout(route.params.params.exercise_id, weight, reps).then(() =>
        syncWorkoutHistory()
      );
    }
  }
};
```

Here's the code for fetching the fresh workout history from the database. We need to call this function everytime we make a modification to the workout history so that the UI can be updated:

```javascript
const syncWorkoutHistory = () => {
  getWorkoutsByExercise(route.params.params.exercise_id).then((res) => {
    const grouped_workouts = groupWorkouts(res);
    setWorkoutHistory(grouped_workouts);
  });
};
```

The `deleteSet()` function gets called when the user clicks on the "delete" button after selecting a set:

```javascript
const deleteSet = () => {
  deleteWorkout(selectedSet.id).then(() => syncWorkoutHistory());
};
```

When the route params changes, we call the `syncWorkoutHistory()` function to update the UI with the workouts for the specific exercise:

```javascript
useEffect(() => {
  if (route.params) {
    syncWorkoutHistory();
                      
    // reset the inputs
    setSelectedSet(null);
    setSelectedIndex(null);
    setWeight(0);
    setReps(0);
  }
}, [route.params]);
```

Since this is the current workout screen, we only need to show today's workout to the user. So we watch for changes in the `workoutHistory` and update `todaysWorkout` based on that:

```javascript
useEffect(() => {
  if (workoutHistory) {
    const todays_workout = filterTodaysWorkout(workoutHistory);
    setTodaysWorkout(todays_workout);
  }
}, [workoutHistory]);
```

Here's the UI code for the current workout screen:

```javascript
return (
    <ScrollView style={styles.container}>
      <View style={styles.top}>
        <View style={styles.field}>
          <Text>WEIGHT (LB)</Text>
          <View style={styles.inputContainer}>
            <IconButton
              icon="minus"
              size={fonts.icon}
              style={{ backgroundColor: colors.background }}
              onPress={() => decrement("weight", weight)}
            />
            <TextInput
              keyboardType="number-pad"
              style={[styles.input, { fontSize: fonts.big }]}
              onChangeText={(text) => setWeight(text)}
              value={weight.toString()}
            />
            <IconButton
              icon="plus"
              size={fonts.icon}
              style={{ backgroundColor: colors.background }}
              onPress={() => increment("weight", weight)}
            />
          </View>
        </View>
        <View style={styles.field}>
          <Text>REPS</Text>
          <View style={styles.inputContainer}>
            <IconButton
              icon="minus"
              size={fonts.icon}
              style={{ backgroundColor: colors.background }}
              onPress={() => decrement("reps", reps)}
            />
            <TextInput
              keyboardType="number-pad"
              style={[styles.input, { fontSize: fonts.big }]}
              onChangeText={(text) => setReps(text)}
              value={reps.toString()}
            />
            <IconButton
              icon="plus"
              size={fonts.icon}
              style={{ backgroundColor: colors.background }}
              onPress={() => increment("reps", reps)}
            />
          </View>
        </View>
      </View>
      <View style={styles.buttonContainer}>
        <Button color={colors.text} onPress={() => saveAction()}>
          {currentAction}
        </Button>
        <Button
          labelStyle={{ color: colors.text }}
          disabled={disableDelete}
          onPress={() => deleteSet()}
        >
          Delete
        </Button>
      </View>
      <View style={styles.setContainer}>
        {todaysWorkout.map((item, index) => {
          const isSelected = index === selectedIndex;
          return (
            <SetItem
              item={item}
              index={index}
              key={index}
              onPress={() => {
                selectSet(item, index);
              }}
              isSelected={isSelected}
            />
          );
        })}
      </View>
    </ScrollView>
);
```

### Workout History Screen

We now proceed to the workout history screen. This is where the user can see all the workouts they performed for a specific exercise since they started recording:

```javascript
// src/screens/WorkoutHistory.js
import React, { useState, useContext } from "react";
import { ScrollView, View, Text, StyleSheet } from "react-native";
import { withTheme } from "react-native-paper";
import { fromUnixTime, format } from "date-fns";
import { AppContext } from "../context/AppContext";
import SetItem from "../components/SetItem";
import { getWorkoutsByExercise } from "../data";
```

Since all the data we require for this screen has already been set in the current workout screen, all we have to do is extract the `workoutHistory` from the app context. The workout history has to be sorted from latest to oldest so we need to use `Object.keys()` to get an array of the `workoutHistory`'s properties. In this case, the properties are the workout dates. Calling `reverse()` on this resulting array will sort the workout history from latest to oldest. From there, we simply extract and format the data accordingly:

```javascript
function WorkoutHistoryScreen({ theme }) {
  const { fonts } = theme;
  const { workoutHistory } = useContext(AppContext);

  return (
    <ScrollView style={styles.container}>
      {Object.keys(workoutHistory)
        .reverse()
        .map((key, date) => {
          const day_workouts = workoutHistory[key];
          const formatted_date = format(
            fromUnixTime(day_workouts[0]["time_created"]),
            "yyyy, MMMM dd"
          );
          return (
            <View style={styles.card} key={date}>
              <View style={styles.sectionHeader}>
                <Text style={{ fontSize: fonts.regular }}>
                  {formatted_date}
                </Text>
              </View>
              <View>
                {day_workouts.map((item, index) => {
                  return <SetItem item={item} index={index} key={index} />;
                })}
              </View>
            </View>
          );
        })}
    </ScrollView>
  );
}
```


## Conclusion and next steps

That's it! In this tutorial, you learned how to use Fauna within a React Native app. Specifically, you learned how to fetch, create, update, and delete documents in Fauna. Along the way, you also learned how to use tab navigation and nested navigation in React Navigation and also how to use React Native Paper.

There's a lot of opportunity to improve this app further. Here are a few ideas:


- Limit database transactions on the currently logged in user - we haven't implemented authentication yet, so the first thing in the todo list is to add authentication. That will then allow you to get a unique ID for each user which can be added when saving data in the database. From there, you can create a new index that will filter out the data based on the user ID.
- Different workout categories - we've only used "weight and reps". You can also add "time", "time and weight", "distance and time", "distance and weight", and "distance, time, and weight".
- Show the personal record. For example, with "weight and reps", you mark the personal record calculated by `weight x reps` to encourage the user to surpass it.
- [Live UI updates for database changes](https://fauna.com/blog/live-ui-updates-with-faunas-real-time-document-streaming) - as you have seen, we performed a new query everytime the workout data changed just so we can update the UI. This Fauna feature will allow us to skip that part so we can listen directly for data changes and update the UI accordingly.
- Graphs to visualize the workout data over time.
- Calendar UI to view the workouts the user performed on a specific day.
- Add an offline database like SQLite for offline capability. Then sync the offline data to Fauna when the user goes online.

I could go on, but let's leave it there. 

You can view the source code of the workout tracking app in its [GitHub repo](https://github.com/anchetaWern/RNWorkoutFauna).