Strapi is a headless CMS framework that allows you to integrate CMS functionalities into any of your technical stacks. This gives you more flexibility when choosing the different components that make up your project.

One of the many frameworks you can integrate Strapi to is React Native. React Native is a cross-platform framework that allows you to write your code in JavaScript, then deploy it on Android and iOS.

In this tutorial, you'll learn how to create a Notes app with Strapi v4 and React Native. You'll be able to add notes on the Strapi backend. Then, you'll be able to view, edit, create, and delete notes in the React Native app.

You can find the code for this tutorial on this GitHub repository.

Setup Strapi

In your terminal, run the following command to install and setup Strapi:

npx create-strapi-app@latest strapi --quickstart

This will create a new directory called strapi, and, once the installation is done, a new page will open in your default browser at the Strapi backend. It's usually at localhost:1337.

You'll need to create an admin user. Once you're done, you'll be redirected to the admin dashboard.

Create Content-Types

Click on Content-Type Builder on the sidebar. Then, click on Create new collection type under Collection Types.

In the pop-up, enter Note for display name. Then click continue.

You'll create 3 fields:

  1. title: of type Text. Make sure to set it required in the Advanced Settings tab.
  2. content: of type Rich Text. Make sure to set it required in the Advanced Settings tab.
  3. date: of type Date. Make sure to select datetime in the Type dropdown, and set it required in the Advanced Settings tab.

You should have a Note collection type with the following fields:

Once you're done click Save.

Change Permissions

The next step is to change permissions so that you can access the notes from React Native.

Click on Settings in the sidebar, then go to Roles under Users & Permissions Plugin. You'll see two entries in the table. Click on the edit icon for the Public row.

Then, scroll down. Under Permissions, click on Note to expand it, then select all permissions. Once you're done, click Save at the top right.

Setup React Native

Next, you'll set up a React Native project.

First, you need to install the Expo CLI if you don't have it installed:

npm i -g expo-cli

Next, run the following command to create a new React Native project:

expo init notes-app

Choose Blank when asked about the type of project to create.

Once that is done, change to the newly created directory notes-app and install dependencies with NPM:

cd notes-app
npm i

Now, you'll need to install the dependencies you'll need for this tutorial. First, start by installing some dependencies with Expo's CLI:

expo install react-native-screens react-native-safe-area-context

These dependencies are necessary to add React Navigation, which is a library that adds navigation capabilities between screens in your app.

Suggested Read: React Native Navigation Tutorial.

Next, install the necessary dependencies with NPM:

npm i react-native-paper @react-navigation/native @react-navigation/native-stack react-native-pell-rich-editor react-native-webview

Here's what each dependency is for:

  1. react-native-paper: React Native Paper library to easily add styled-components in your app.
  2. @react-navigation/native @react-navigation/native-stack: More libraries for React Navigation.
  3. react-native-pell-rich-editor: a Rich Editor element for React Native.
  4. react-native-webview: required by react-native-pell-rich-editor.

Create Home Screen

The home screen displays a list of notes with just the title and the date. It will also have a + button at the top right to add notes.

Create the file screens/HomeScreen.js with the following content:

import axios from "axios";
import { useEffect, useState } from "react";
import { FlatList, View } from "react-native";
import { Caption, List, Snackbar } from "react-native-paper";

export default function HomeScreen ({ navigation }) {
  const [notes, setNotes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    loadNotes()

    const subscribe = navigation.addListener('focus', () => {
      loadNotes();
    });

    return subscribe;
  }, [])

  function loadNotes () {
    axios.get('http://<IP>:1337/api/notes')
      .then(({ data }) => {
        setNotes(data.data);
        setLoading(false);
      })
      .catch((e) => {
        console.error(e);
        setError('An error occurred, please try again later.');
        setLoading(false);
      });
  }

  return (
    <View>
      {!loading && !notes.length && <Caption style={{textAlign: 'center', marginTop: 10}}>You have no notes</Caption>}
      <FlatList
        data={notes}
        renderItem={({ item }) => (
          <List.Item 
            key={item.id}
            title={item.attributes.title}
            description={item.attributes.date}
            onPress={() => navigation.navigate('Editor', {
                note: item
            })}
            />
        )}      
        refreshing={loading}
        onRefresh={loadNotes}
        style={{width: '100%', height: '100%'}}
      />
      <Snackbar visible={error.length > 0} onDismiss={() => setError('')}>{error}</Snackbar>
    </View>
  )
}

You first create the state variable notes which will hold the notes when received from the Strapi backend. You use a FlatList component to display the notes. This will render each note using the List.Item component from React Native Paper. The title of the item will be the title of the note, and the description will be the date of the note.

When the item in the list is clicked, the user will be taken to the Editor screen (which you'll create in the next section).

The fetching of the notes will happen in the loadNotes function. This function is called when the screen first opens, when the screen gains focus, and when the flat list is refreshed.

In the loadNotes function, you send a request to http://<IP>:1337/api/notes. Notice that to run the app on your phone, you need to use your machine's network IP. So, replace <IP> with your machine's IP.

This endpoint is Strapi's Endpoint for fetching the entries of a content type. You then set the notes state variable to the data received from Strapi.

Next, you need to make changes to the App.js file to show different screens.

Open App.js and replace the content with the following:

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { IconButton } from 'react-native-paper';
import EditorScreen from './screens/EditorScreen';
import HomeScreen from './screens/HomeScreen';

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} options={({navigation}) => ({
          headerRight: () => (
            <IconButton icon='plus' onPress={() => navigation.navigate('Editor')} />
          )
        })} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Notice that the Home screen has a button at the top right that will take you to the Editor screen.

Now, let's run the app. In your terminal, run the following:

npm start

This will allow you to open the app on iOS or Android. You'll need the Expo Go app on your phone. Then, on Android, open the app and scan the QR code in the terminal or the developer tool page to open the app. Alternatively, on iOS, you need to scan the QR code in your Camera app which will let you open the app in Expo Go.

When you open the app, you'll see an empty home screen.

Create Editor Screen

Now, you'll create the editor screen which will show the user the editor with either the content filled (if editing an existing note) or an empty rich text editor.

Create screens/EditorScreen.js with the following content:

import { useLayoutEffect, useRef, useState } from 'react';
import { RichEditor, RichToolbar} from "react-native-pell-rich-editor";
import { Keyboard, KeyboardAvoidingView, ScrollView, View } from 'react-native';
import { Button, Colors, Snackbar, Subheading, TextInput } from 'react-native-paper';
import axios from 'axios';

export default function EditorScreen ({ route, navigation }) {
  const editor = useRef();
  const [title, setTitle] = useState(route.params && route.params.note ? route.params.note.attributes.title : '');
  const [content, setContent] = useState(route.params && route.params.note ? route.params.note.attributes.content : '');
  const [error, setError] = useState('')

  function saveNote () {
    editor.current.blurContentEditor(); //lose focus for editor and close keyboard
    Keyboard.dismiss();
    const trimmedTitle = title.trim(),
      trimmedContent = content.trim();
    if (!trimmedTitle.length || !trimmedContent.length) {
      setError('Please fill both title and content');
      return;
    }
    axios({
      method: route.params && route.params.note ? 'PUT' : 'POST',
      url: 'http://<IP>:1337/api/notes' + (route.params && route.params.note ? '/' + route.params.note.id : ''),
      data: {
        data: {
          title,
          content,
          date: Date.now()
        }
      }
    }).then(() => {
      //redirect back to home screen
      navigation.goBack();
    })
    .catch((e) => {
      console.error(e);
      setError('An error occurred, please try again later')
    })
  }

  function deleteNote () {
    axios.delete('http://<IP>:1337/api/notes/' + route.params.note.id)
      .then(() => {
        //redirect back to home screen
      navigation.goBack();
      })
      .catch((e) => {
        console.error(e);
        setError('An error occurred, please try again later.');
      })
  }

  useLayoutEffect(() => {
    navigation.setOptions({
      headerTitle: content.length === 0 ? 'New Note' : 'Edit Note',
      headerRight: route.params && route.params.note ? () => (
        <Button color={Colors.redA100} onPress={deleteNote}>Delete</Button>
      ) : () => (<></>)
    });
  }, []);

  return (
    <View style={{margin: 10, flex: 1, justifyContent: 'space-between'}}>
      <TextInput label="Title" value={title} onChangeText={setTitle} mode="outlined" />
      <Subheading>Content</Subheading>
      <RichToolbar
        editor={editor}
      />
      <ScrollView keyboardDismissMode='onDrag'>
        <KeyboardAvoidingView behavior={"position"}	style={{ flex: 1 }} keyboardVerticalOffset={250}>
          <RichEditor 
            style={{ flex: 1}}
            ref={editor} 
            onChange={setContent} 
            initialContentHTML={content} 
            placeholder='Start typing...'
            useContainer />
          <Button onPress={saveNote} mode="contained" style={{marginTop: 20}}>
            Save
          </Button>
        </KeyboardAvoidingView>
      </ScrollView>
      <Snackbar visible={error.length > 0} onDismiss={() => setError('')}>{error}</Snackbar>
    </View>
  )
}

In this code snippet, you create a editor ref variable for the Rich text editor. This is necessary for the library you're using. You also create a title and content state variables. These will be used to store the input values and will have as initial values the note's title and content if it exists.

On the screen, you show a rich text editor with a toolbar to add advanced text editing. You also add a Save button and a Delete button for existing notes.

Save a Note

When the Save button is clicked, you check if the note exists or is new. If the note already exists, then, a PUT request is sent to http://<IP>:1337/api/notes/<note_id>, where <IP> is your machine's IP and <note_id> is the current note's ID. This Strapi endpoint is used to update an entry in a collection.

Alternatively, if the note is new, a POST request is sent to http://<IP>:1337/api/notes, where <IP> is your machine's IP. This Strapi endpoint is used to create an entry.

Both requests accept in the body of the request a data parameter with the entry's data. You pass the title, content, and current date.

Delete a Note

When the Delete Button is clicked, a DELETE request is sent to http://<IP>:1337/api/notes/<note_id>, where <IP> is your machine's IP and <note_id> is the ID of the note to delete. Remember, this is only available if the note exists.

After the note is saved or deleted, you take the user back to the home screen.

Next, you need to add the new screen to App.js. Add it after the Home screen:

<Stack.Screen name="Editor" component={EditorScreen} />

That's all, now run the app if it's not running. Try first clicking on the + button at the top right of the home screen. You'll see a rich text editor with a toolbar and a Save button.

Add a Note

Try entering any content in both title and content fields. Once you're done, click Save. You'll be taken back to the home screen where you can see the new item you added.

Edit a Note

Now, click on a note and edit its content, then click Save. If you click on that note again, you'll see that the content has been edited successfully.

Delete a Note

If you click on a note from the home screen, you'll notice a delete button at the top right of the screen.

Click on the delete button and you'll be taken back to the home screen where you can see your note does not exist anymore.

Conclusion

This simple note app showcases how you can connect a React Native app to Strapi. Strapi makes it simple to add CMS capabilities to apps using React Native. Using a CMS like Strapi to easily manage the notes on your app allows you to also manage the notes on different platforms like on the web.