Strapi is a headless CMS that allows you to easily build customizable backend services. You can integrate Strapi applications with any type of frontend and can deploy the application to the cloud.
This tutorial guides you through the process of creating a simple React-native to-do app with Strapi as your backend. You will be able to create, edit, and delete to-dos on a per-user basis by interacting with the Strapi's REST API.
Note: This tutorial assumes that you already have an instance of Strapi up and running and created a test user. If you don’t, read the Getting Started guide.
The sample app is available on Github.
Firstly, we’ll need to create a Strapi project with the command below:
npx create-strapi-app@latest strapi-backend
After creating the project, Strapi will automatically start a development server. We can always run the development server ourselves by opening the root folder in the terminal and running the yarn develop
command:
yarn develop
We’ll need to create a new “todo” content type. For that, we need to access our Content-Type Builder and click on + Create new collection type.
Now that we have successfully created our new Content-Type, we need to add some fields to it:
title
(Text, Short, required): The title of the TODO.description
(Text, Long, required): A quick summary of the TODO.finished
(Boolean, default: false): If the TODO was finished.owner
(Relation, required): The User (User-Permissions) that the TODO belongs to.When you successfully have added the fields to your Content-Type, it should look like this:
After saving the changes, we need to register the endpoints for the ToDo collection. The endpoints allows our react-native app to interact with the collection. To register the endpoints we need to:
Now that we have our API set up, we can concentrate on our mobile app. I suggest checking out React native’s Getting Started guide if you aren’t familiar with React-native. We’ll have to initialize a new React-native project by running the React-native CLI.
react-native init TodoApp
Running this command will create a new directory with the specified app name that will be the root of your project. In the base directory of your project, you’ll have to create the following folder structure:
1src
2 |-- app
3 |-- controllers
4 |-- models
5 |-- views
6 |-- components
7 |-- redux
8 |-- actions
9 |-- reducers
10 |-- screens
Now that we have our project initialized, our structure optimized, and our backend running, we can add some packages to our app. We’ll use a predefined list of packages I found. There may be better alternatives, but you are free to decide this for yourself!
react-native-paper
: A set of components following Google’s Material Design guidelinesreact-native-vector-icons
: Used by react-native-paper to display a gigantic set of icons that fit your needs.react-navigation
: A library for creating in-app navigation and handling navigation intents. It also provides integration with react-native-paper by providing a material themed bottom tab bar.react-native-gesture-handler
: Required by react-navigation to work properly.redux
: Redux is a library for handling global state and modification.react-redux
: Provides the components needed for redux to work with react-native.redux-persist
: Enables you to save and persist your state locally on the device. It is especially handy for authentication flows.async-storage
: Asynchronous on-device storageTo add the packages, install them with yarn:
yarn add react-native-paper react-native-vector-icons react-navigation redux react-redux redux-persist @react-native-community/async-storage react-native-gesture-handler
Before getting to the interface of the app, we’ll create a model for our TODO. To do so, create a new file in ./src/app/models/TodoModel.js
. Since this file contains the model for the Content-Type we have created earlier, the fields need to be exactly the same.
./src/app/models/TodoModel.js
file containing the following code:1 // TodoModel.js
2
3 /**
4 * TODO Model as defined in Strapi
5 */
6
7 import {edit, save, dismiss} from '../controllers/TodoController';
8
9 class TodoModel {
10 constructor(user, title, description, finished = false, id = undefined) {
11 this.user = user;
12 this.title = title;
13 this.description = description;
14 this.finished = finished;
15 this.id = id;
16 // save function adds id property later
17 }
18
19 async save() {
20 // save the todo to Strapi
21 const id = await save(this);
22
23 // should no id be returned throw an error
24 if (!id) {
25 throw new Error('Todo could not be saved');
26 }
27
28 // set id and return true
29 this.id = id;
30 return true;
31 }
32
33 async edit() {
34 if (!this.id) {
35 throw new Error('Cannot edit TODO before it was saved.');
36 }
37
38 const edited = await edit(this);
39
40 // check if the edit returned false
41 if (!edited) {
42 throw new Error('Todo could not be edited.');
43 }
44
45 return true;
46 }
47
48 async dismiss() {
49 if (!this.id) {
50 throw new Error('Cannot delete TODO before it was saved.');
51 }
52
53 const dismissed = await dismiss(this);
54
55 // check if the dismiss returned false
56 if (!dismissed) {
57 throw new Error('Todo could not be deleted.');
58 }
59
60 return true;
61 }
62 }
63
64 export default TodoModel;
User
Content-Type from the User-Permissions plugin in Strapi. Create a file in ./src/app/models/UserModel.js
containing the following code:1 // UserModel.js
2
3 /**
4 * User model as defined in Strapi
5 */
6
7 import {login, logout} from '../controllers/UserController';
8
9 class UserModel {
10 constructor(identifier, password) {
11 this.identifier = identifier;
12 this.password = password;
13 }
14
15 async login() {
16 const result = await login(this);
17
18 if (!result) {
19 throw new Error('Unable to login user.');
20 }
21
22 return true;
23 }
24
25 async logout() {
26 const result = await logout(this);
27
28 if (!result) {
29 throw new Error('Unable to logout user.');
30 }
31
32 return true;
33 }
34 }
35
36 export default UserModel;
Now that we have coded our models, you may notice that we imported a file we haven’t created yet, so let’s create the two needed files:
./src/app/controllers/UserController.js
./src/app/controllers/TodoController.js
These file are our controllers where we hold our app logic that will be executed when we call our model functions.
1 // TodoController.js
2
3 import {store} from '../../redux/Store';
4
5 /**
6 * if you have an instance of Strapi running on your local
7 * machine:
8 *
9 * 1. Run `adb reverse tcp:8163 tcp:8163` (only on android)
10 *
11 * 2. You have to change the access IP from localhost
12 * to the IP of the machine Strapi is running on.
13 */
14 const url = 'http://localhost:1337/todos';
15
16 /**
17 * add a todo to Strapi
18 */
19 export const save = async todo => {
20 const requestBody = JSON.stringify({
21 title: todo.title,
22 description: todo.description,
23 finished: todo.finished,
24 user: todo.user.id,
25 });
26
27 const requestConfig = {
28 method: 'POST',
29 headers: {
30 Authorization: `Bearer ${store.getState().jwt}`,
31 'Content-Type': 'application/json',
32 },
33 body: requestBody,
34 };
35
36 const response = await fetch(url, requestConfig);
37
38 const json = await response.json();
39
40 if (json.error) {
41 return null;
42 }
43
44 return json._id;
45 };
46
47 /**
48 * add a todo to Strapi
49 */
50 export const edit = async todo => {
51 const requestBody = JSON.stringify({
52 title: todo.title,
53 description: todo.description,
54 due: todo.due,
55 finished: todo.finished ? 1 : 0,
56 user: todo.user.id,
57 });
58
59 const requestConfig = {
60 method: 'PUT',
61 headers: {Authorization: `Bearer ${store.getState().jwt}`},
62 body: requestBody,
63 };
64
65 const response = await fetch(`${url}/${todo.id}`, requestConfig);
66 const json = await response.json();
67
68 if (json.error) {
69 return false;
70 }
71
72 return true;
73 };
74
75 /**
76 * delete a todo from Strapi
77 */
78 export const dismiss = async todo => {
79 const response = await fetch(`${url}/${todo.id}`, {
80 headers: {Authorization: `Bearer ${store.getState().jwt}`},
81 });
82
83 const json = response.json();
84
85 if (json.error) {
86 return false;
87 }
88
89 return true;
90 };
Now, the second controller:
1 // UserController.js
2
3 import {saveUser, deleteUser} from '../../redux/actions/UserActions';
4
5 /**
6 * if you have an instance of Strapi running on your local
7 * machine:
8 *
9 * 1. Run `adb reverse tcp:8163 tcp:8163` (only on android)
10 *
11 * 2. You have to change the access IP from localhost
12 * to the IP of the machine Strapi is running on.
13 */
14 const url = 'http://192.168.0.57:1337';
15
16 /**
17 * @param {UserModel} user
18 */
19 export const login = async user => {
20 const requestConfig = {
21 method: 'POST',
22 headers: {
23 'Content-Type': 'application/json',
24 },
25 body: JSON.stringify({
26 identifier: user.identifier,
27 password: user.password,
28 }),
29 };
30
31 try {
32 const response = await fetch(`${url}/auth/local`, requestConfig);
33 const json = await response.json();
34
35 if (json.error) {
36 return false;
37 }
38
39 saveUser(json.jwt, json.user);
40
41 return true;
42 } catch (err) {
43 alert(err);
44 return false;
45 }
46 };
47
48 /**
49 * @param {UserModel} user
50 */
51 export const logout = async user => {
52 deleteUser();
53 };
As seen, we call our redux store at the end of the UserController.login()
and UserController.logout()
. It will make more sense in a few moments.
To be able to update our UI, we need to create a Redux store. This store will hold our user data and be persisted if modified. Amazing, right?
Create the following files:
./src/redux/Store.js
./src/redux/reducers/UserReducer.js
./src/redux/actions/UserActions.js
Now that we have created the files, we can start creating our store logic. The logic for each store identity is held in their so-called reducer.
The reducer can receive an action; this action has a type and an optional payload that you can define on a per-request basis. We’ll need two types of actions that’ll be USER_SAVE
and USER_DELETE
that symbolize respective user log in -/out’s. We will not implement USER_DELETE
though.
1 // UserReducer.js
2
3 const defaultState = {
4 jwt: null,
5 user: null,
6 };
7
8 /**
9 * This is a reducer, a pure function with (state, action) => state signature.
10 * It describes how an action transforms the state into the next state.
11 *
12 * The shape of the state is up to you: it can be a primitive, an array, an object,
13 * or even an Immutable.js data structure. The only important part is that you should
14 * not mutate the state object, but return a new object if the state changes.
15 *
16 * In this example, we use a `switch` statement and strings, but you can use a helper that
17 * follows a different convention (such as function maps) if it makes sense for your
18 * project.
19 */
20 const UserReducer = (state = defaultState, action) => {
21 switch (action.type) {
22 case 'USER_SAVE': {
23 return {
24 ...state,
25 ...{jwt: action.payload.jwt, user: action.payload.user},
26 };
27 }
28
29 case 'USER_DELETE': {
30 return defaultState;
31 }
32
33 default:
34 return defaultState;
35 }
36 };
37
38 export default UserReducer;
To call this reducer, we will access the previously created UserActions.js
file. That holds two actions: saveUser()
and deleteUser()
.
1 // UserActions.js
2
3 import {store} from '../Store';
4
5 // The only way to mutate the internal state is to dispatch an action.
6 // The actions can be serialized, logged or stored and later replayed.
7 export const saveUser = (jwt, user) => {
8 store.dispatch({
9 type: 'USER_SAVE',
10 payload: {
11 jwt,
12 user,
13 },
14 });
15 };
16
17 export const deleteUser = () => {
18 store.dispatch({type: 'USER_DELETE'});
19 };
And lastly, we have to code our Store.js
file. This file not only includes the reducer but also provides the persistence via the previously installed redux-persist
library.
1 // Store.js
2
3 import {createStore} from 'redux';
4 import {persistStore, persistReducer} from 'redux-persist';
5 import AsyncStorage from '@react-native-community/async-storage';
6
7 import rootReducer from '../redux/reducers/UserReducer';
8
9 const persistConfig = {
10 key: 'root',
11 storage: AsyncStorage,
12 };
13
14 const persistedReducer = persistReducer(persistConfig, rootReducer);
15
16 // Create a Redux store holding the state of your app.
17 // Its API is { subscribe, dispatch, getState }.
18 const createdStore = createStore(persistedReducer);
19 const createdPersistor = persistStore(createdStore);
20
21 export const store = createdStore;
22 export const persistor = createdPersistor;
Just one more step and your app is redux-ready! Add the PersistorGate
and Provider
components to your App.js
file.
1 // App.js
2
3 import React from 'react';
4 import {PersistGate} from 'redux-persist/integration/react';
5 import {Provider} from 'react-redux';
6 import {store, persistor} from './src/redux/Store';
7
8 const App = () => {
9 return (
10 <Provider store={store}>
11 <PersistGate loading={null} persistor={persistor} />
12 </Provider>
13 );
14 };
15
16 export default App;
To build our screens, we'll use the previously-installed react-navigation package. We’ll have to create a bunch of files; hope you are ready to get your hands dirty!
./src/screens/Overview.js
./src/screens/Login.js
./src/components/navigation/Authentication.js
Once created, fill all Screen files with mock up content so you can distinguish what screen you are currently on.
1 // Overview.js & Login.js
2
3 import React from 'react';
4 import {View, StyleSheet} from 'react-native';
5 import {Text} from 'react-native-paper';
6
7 const SCREEN_NAME = props => {
8 return (
9 <View style={styles.base}>
10 <Text style={styles.text}>SCREEN_NAME</Text>
11 </View>
12 );
13 };
14
15 const styles = StyleSheet.create({
16 base: {
17 flex: 1,
18 alignContent: 'center',
19 justifyContent: 'center',
20 },
21 text: {
22 textAlign: 'center',
23 },
24 });
25
26 export default SCREEN_NAME;
Note: Replace
SCREEN_NAME
with the name of the screen.
Open up the file Authentication.js
created in the last step and create a new SwitchNavigator via the createStackNavigator()
method. We use the SwitchNavigator in combination with redux to redirect the user to the login page or the overview page depending on his authentication state.
1 // Authentication.js
2
3 import React from 'react';
4
5 // navigation components
6 import {createSwitchNavigator, createAppContainer} from 'react-navigation';
7
8 import Login from '../../screens/Login';
9 import Overview from '../../screens/Overview';
10 import {store} from '../../redux/Store';
11
12 const Authentication = () => {
13 const [route, setRoute] = React.useState(
14 store.getState().jwt ? 'Overview' : 'Login',
15 );
16
17 const Navigator = createAppContainer(
18 createSwitchNavigator(
19 {
20 Login: {
21 screen: Login,
22 },
23 Overview: {
24 screen: Overview,
25 },
26 },
27 {
28 initialRouteName: route,
29 },
30 ),
31 );
32
33 // on mount subscribe to store event
34 React.useEffect(() => {
35 store.subscribe(() => {
36 setRoute(store.getState().jwt ? 'Overview' : 'Login');
37 });
38 }, []);
39
40 return <Navigator />;
41 };
42
43 export default Authentication;
Import the navigation file into your App.js
file and render it as a component. Also, add the Provider component of react-native-paper
1 // App.js
2
3 import React from 'react';
4 import {Provider as PaperProvider} from 'react-native-paper';
5 import {PersistGate} from 'redux-persist/integration/react';
6 import {Provider} from 'react-redux';
7 import {store, persistor} from './src/redux/Store';
8 import Authentication from './src/components/navigation/Authentication';
9
10 const App = () => {
11 return (
12 <Provider store={store}>
13 <PersistGate loading={null} persistor={persistor}>
14 <PaperProvider>
15 <Authentication />
16 </PaperProvider>
17 </PersistGate>
18 </Provider>
19 );
20 };
21
22 export default App;
Now run your project and take a look at your device/emulator and you should see the following screen:
Our mockup screen is amazing but we need to add some functionality to it.
1 // Login.js
2
3 import React from 'react';
4 import {View, StyleSheet, StatusBar} from 'react-native';
5 import {
6 Headline,
7 Paragraph,
8 TextInput,
9 Button,
10 Snackbar,
11 Portal,
12 } from 'react-native-paper';
13 import UserModel from '../app/models/UserModel';
14
15 const Login = props => {
16 const [identifier, setIdentifier] = React.useState('');
17 const [password, setPassword] = React.useState('');
18 const [visible, setVisible] = React.useState(false);
19 const [loading, setLoading] = React.useState(false);
20 const [error, setError] = React.useState(false);
21
22 const validateInput = () => {
23 let errors = false;
24
25 if (!identifier || identifier.length === 0) {
26 errors = true;
27 }
28
29 if (!password || password.length === 0) {
30 errors = true;
31 }
32
33 return !errors;
34 };
35
36 const authenticateUser = async () => {
37 if (validateInput()) {
38 setLoading(true);
39 const user = new UserModel(identifier, password);
40
41 try {
42 await user.login();
43 } catch (err) {
44 setError(err.message);
45 setVisible(true);
46 setLoading(false);
47 }
48 } else {
49 setError('Please fill out all *required fields');
50 setVisible(true);
51 setLoading(false);
52 }
53 };
54
55 return (
56 <View style={styles.base}>
57 <>
58 <StatusBar backgroundColor="#ffffff" barStyle="dark-content" />
59 </>
60
61 <View style={styles.header}>
62 <Headline style={styles.appTitle}>TodoApp</Headline>
63 <Paragraph style={styles.appDesc}>
64 Authenticate with Strapi to access the TodoApp.
65 </Paragraph>
66 </View>
67
68 <>
69 <View style={styles.divider} />
70 <TextInput
71 onChangeText={text => setIdentifier(text)}
72 label="*Username or email"
73 placeholder="*Username or email">
74 {identifier}
75 </TextInput>
76 </>
77
78 <>
79 <View style={styles.divider} />
80 <TextInput
81 onChangeText={text => setPassword(text)}
82 label="*Password"
83 placeholder="*Password"
84 secureTextEntry>
85 {password}
86 </TextInput>
87 </>
88
89 <>
90 <View style={styles.divider} />
91 <Button
92 loading={loading}
93 disabled={loading}
94 style={styles.btn}
95 onPress={() => authenticateUser()}
96 mode="contained">
97 Login
98 </Button>
99 <View style={styles.divider} />
100 <View style={styles.divider} />
101 </>
102
103 <>
104 {/**
105 * We use a portal component to render
106 * the snackbar on top of everything else
107 * */}
108 <Portal>
109 <Snackbar visible={visible} onDismiss={() => setVisible(false)}>
110 {error}
111 </Snackbar>
112 </Portal>
113 </>
114 </View>
115 );
116 };
117
118 const styles = StyleSheet.create({
119 base: {
120 flex: 1,
121 paddingLeft: 16,
122 paddingRight: 16,
123 alignContent: 'center',
124 justifyContent: 'center',
125 backgroundColor: '#ffffff',
126 },
127 divider: {
128 height: 16,
129 },
130 headline: {
131 fontSize: 30,
132 },
133 appDesc: {
134 textAlign: 'center',
135 },
136 header: {
137 padding: 32,
138 },
139 appTitle: {
140 textAlign: 'center',
141 fontSize: 35,
142 lineHeight: 35,
143 fontWeight: '700',
144 },
145 btn: {
146 height: 50,
147 paddingTop: 6,
148 },
149 });
150
151 export default Login;
Try to login with a Strapi user and you’ll land directly on the overview page. Close the app, open it again, and you’ll see that you are directly accessing the overview screen. This is thanks to redux-persist loading your saved state and passing it to our SwitchNavigator in Authentication.js
.
Do you know what’s one of the greatest features of mobile development? Endless lists! We’re going to create a list that is created for our application. Since the length of such a list is undefined, the number of possible layouts is too.
Let's get started with our list component for which we’ll create a new ./src/components/TodoList.js
file and paste the following:
1 // TodoList.js
2
3 import React from 'react';
4 import {View, StyleSheet} from 'react-native';
5 import {
6 Text,
7 IconButton,
8 ActivityIndicator,
9 Button,
10 Portal,
11 Dialog,
12 Paragraph,
13 TextInput,
14 HelperText,
15 Divider,
16 } from 'react-native-paper';
17 import {FlatList} from 'react-native-gesture-handler';
18 import {store} from '../../redux/Store';
19 import TodoView from '../../app/views/TodoView';
20 import TodoModel from '../../app/models/TodoModel';
21
22 /**
23 * the footer also acts as the load more
24 * indicator.
25 */
26 export const TodoFooter = props => {
27 return (
28 <>
29 {props.shouldLoadMore ? (
30 <View style={styles.loaderView}>
31 <ActivityIndicator animating />
32 </View>
33 ) : null}
34 </>
35 );
36 };
37
38 /**
39 * This is our header for the list that also
40 * includes the todo.add action.
41 */
42 export const TodoHeader = props => {
43 const [error, setError] = React.useState('');
44 const [title, setTitle] = React.useState('');
45 const [visible, setVisible] = React.useState(false);
46 const [description, setDescription] = React.useState('');
47
48 const createTodoFromDialog = async () => {
49 if (title.length === 0 || description.length === 0) {
50 setError('Title and description are required.');
51 return;
52 }
53
54 const user = store.getState().user;
55 const todo = new TodoModel(user, title, description);
56
57 try {
58 await todo.save();
59 } catch (err) {
60 setError(err.message);
61 }
62
63 props.addTodo(todo);
64 };
65
66 return (
67 <View style={styles.header}>
68 <Text style={styles.text}>{props.text || "Your to do's"}</Text>
69 <View style={styles.buttonFrame}>
70 {!props.text ? (
71 <Button
72 onPress={() => setVisible(true)}
73 style={{marginLeft: 16}}
74 mode="outlined">
75 Add a todo
76 </Button>
77 ) : null}
78 </View>
79
80 <Portal>
81 <Dialog visible={visible} onDismiss={() => setVisible(false)}>
82 <Dialog.Title>Create a new todo</Dialog.Title>
83 <Dialog.Content>
84 <Paragraph>
85 Adding a new todo will save to in Strapi so you can use it later.
86 </Paragraph>
87 <View style={styles.divider} />
88 <TextInput
89 label="title"
90 placeholder="title"
91 onChangeText={text => {
92 setTitle(text);
93 setError(false);
94 }}>
95 {title}
96 </TextInput>
97 <View style={styles.divider} />
98 <TextInput
99 label="description"
100 placeholder="description"
101 multiline={true}
102 numberOfLines={4}
103 onChangeText={text => {
104 setDescription(text);
105 setError(false);
106 }}>
107 {description}
108 </TextInput>
109 <HelperText type="error">{error}</HelperText>
110 </Dialog.Content>
111
112 <Dialog.Actions>
113 <Button
114 onPress={() => {
115 setVisible(false);
116 setTitle('');
117 setDescription('');
118 setError('');
119 }}>
120 Cancel
121 </Button>
122 <Button onPress={() => createTodoFromDialog()}>Add</Button>
123 </Dialog.Actions>
124 </Dialog>
125 </Portal>
126 </View>
127 );
128 };
129
130 /**
131 * in case no todos were fetched on initial fetch
132 * we can assume that there are none for this specific
133 * user.
134 */
135 export const EmptyTodo = props => {
136 const [error, setError] = React.useState('');
137 const [title, setTitle] = React.useState('');
138 const [visible, setVisible] = React.useState(false);
139 const [description, setDescription] = React.useState('');
140
141 const createTodoFromDialog = async () => {
142 if (title.length === 0 || description.length === 0) {
143 setError('Title and description are required.');
144 return;
145 }
146
147 const user = store.getState().user;
148 const todo = new TodoModel(user, title, description);
149
150 try {
151 await todo.save();
152 } catch (err) {
153 setError(err.message);
154 }
155
156 props.addTodo(todo);
157 };
158
159 return (
160 <View style={styles.emptyBase}>
161 <TodoHeader text={'Pretty empty here ..'} />
162 <Button
163 onPress={() => setVisible(true)}
164 style={styles.btn}
165 mode="contained">
166 Create a new todo
167 </Button>
168
169 <Portal>
170 <Dialog visible={visible} onDismiss={() => setVisible(false)}>
171 <Dialog.Title>Create a new todo</Dialog.Title>
172 <Dialog.Content>
173 <Paragraph>
174 Adding a new todo will save to in Strapi so you can use it later.
175 </Paragraph>
176 <View style={styles.divider} />
177 <TextInput
178 label="title"
179 placeholder="title"
180 onChangeText={text => {
181 setTitle(text);
182 setError(false);
183 }}>
184 {title}
185 </TextInput>
186 <View style={styles.divider} />
187 <TextInput
188 label="description"
189 placeholder="description"
190 multiline={true}
191 numberOfLines={4}
192 onChangeText={text => {
193 setDescription(text);
194 setError(false);
195 }}>
196 {description}
197 </TextInput>
198 <HelperText type="error">{error}</HelperText>
199 </Dialog.Content>
200
201 <Dialog.Actions>
202 <Button
203 onPress={() => {
204 setVisible(false);
205 setTitle('');
206 setDescription('');
207 setError('');
208 }}>
209 Cancel
210 </Button>
211 <Button onPress={() => createTodoFromDialog()}>Add</Button>
212 </Dialog.Actions>
213 </Dialog>
214 </Portal>
215 </View>
216 );
217 };
218
219 /**
220 * the main list component holding all of the loading
221 * and pagination logic.
222 */
223 export const TodoList = props => {
224 const [data, setData] = React.useState([]);
225 const [limit] = React.useState(10);
226 const [start, setStart] = React.useState(0);
227 const [loading, setLoading] = React.useState(true);
228 const [loadingMore, setLoadingMore] = React.useState(true);
229 const [shouldLoadMore, setShouldLoadMore] = React.useState(true);
230
231 /**
232 * get the data from the server in a paginated manner
233 *
234 * 1. should no data be present start the normal loading
235 * animation.
236 *
237 * 2. should data be present start the loading more
238 * animation.
239 */
240 const getTodosForUser = React.useCallback(async () => {
241 if (!shouldLoadMore) {
242 return;
243 }
244
245 if (!loading && data.length === 0) {
246 setLoading(true);
247 }
248
249 if (!loadingMore && data.length > 0) {
250 setLoadingMore(true);
251 }
252
253 const url = `http://192.168.0.57:1337/todos?_start=${start}&_limit=${limit}`;
254 const jwt = store.getState().jwt;
255 const response = await fetch(url, {
256 headers: {Authorization: `Bearer ${jwt}`},
257 });
258 const json = await response.json();
259
260 if (json.length < 10) {
261 setShouldLoadMore(false);
262 } else {
263 setStart(start + limit);
264 }
265
266 setData([...data, ...json]);
267 setLoading(false);
268 setLoadingMore(false);
269 }, [data, limit, loading, loadingMore, shouldLoadMore, start]);
270
271 /**
272 * saves a new todo to the server by creating a new TodoModel
273 * from the dialog data and calling Todo.save()
274 */
275 const addTodo = todo => {
276 const {title, description, finished, user, id} = todo;
277 setData([...data, ...[{title, description, finished, user, id}]]);
278 };
279
280 /**
281 * callback method for the todo view. Deletes a todo from the list
282 * after it has been deleted from the server.
283 */
284 const removeTodo = id => {
285 setData(data.filter(item => item.id !== id));
286 };
287
288 React.useEffect(() => {
289 getTodosForUser();
290 }, [getTodosForUser]);
291
292 if (loading) {
293 return (
294 <View style={styles.loaderBase}>
295 <ActivityIndicator animating size="large" />
296 </View>
297 );
298 }
299
300 if (!shouldLoadMore && !loading && !loadingMore && data.length === 0) {
301 return <EmptyTodo addTodo={addTodo} />;
302 }
303
304 return (
305 <>
306 <FlatList
307 style={styles.base}
308 data={data}
309 ItemSeparatorComponent={() => <Divider />}
310 ListHeaderComponent={() => <TodoHeader addTodo={addTodo} />}
311 ListFooterComponent={() => (
312 <TodoFooter shouldLoadMore={shouldLoadMore} />
313 )}
314 onEndReachedThreshold={0.5}
315 onEndReached={() => getTodosForUser()}
316 renderItem={({item, index}) => (
317 <TodoView removeTodo={removeTodo} item={item} index={index} />
318 )}
319 />
320 </>
321 );
322 };
323
324 const styles = StyleSheet.create({
325 base: {
326 flex: 1,
327 backgroundColor: '#fff',
328 },
329 emptyBase: {
330 flex: 1,
331 backgroundColor: '#fff',
332 },
333 text: {
334 fontSize: 35,
335 lineHeight: 35,
336 fontWeight: '700',
337 padding: 32,
338 paddingLeft: 16,
339 },
340 header: {
341 flexDirection: 'row',
342 alignContent: 'center',
343 },
344 btn: {
345 height: 50,
346 paddingTop: 6,
347 marginLeft: 16,
348 marginRight: 16,
349 },
350 loaderBase: {
351 padding: 16,
352 alignContent: 'center',
353 justifyContent: 'center',
354 flex: 1,
355 },
356 divider: {
357 height: 16,
358 },
359 buttonFrame: {
360 justifyContent: 'center',
361 },
362 });
Now that we have our list set up, we are just one more step away from completing our app and that is the view that will be reused for each individual child of the data set.
Create a ./src/app/views/TodoView.js
file containing the following code:
1 // TodoView.js
2
3 import React from 'react';
4 import {StyleSheet, View} from 'react-native';
5 import {
6 List,
7 Colors,
8 Portal,
9 Dialog,
10 Paragraph,
11 TextInput,
12 HelperText,
13 Button,
14 Checkbox,
15 } from 'react-native-paper';
16 import TodoModel from '../models/TodoModel';
17 import {store} from '../../redux/Store';
18
19 export const TodoView = props => {
20 const {
21 title: passedPropsTitle,
22 description: passedPropsDesc,
23 finished: passedPropsFinished,
24 id,
25 } = props.item;
26
27 const [passedTitle, setPassedTitle] = React.useState(passedPropsTitle);
28 const [passedDesc, setPassedDesc] = React.useState(passedPropsDesc);
29 const [passedFinished, setPassedFinished] = React.useState(
30 passedPropsFinished,
31 );
32 const [error, setError] = React.useState('');
33 const [title, setTitle] = React.useState(passedTitle);
34 const [visible, setVisible] = React.useState(false);
35 const [description, setDescription] = React.useState(passedDesc);
36 const [finished, setFinished] = React.useState(passedFinished);
37
38 const editTodoFromDialog = async () => {
39 if (title.length === 0 || description.length === 0) {
40 setError('Title and description are required.');
41 return;
42 }
43
44 const user = store.getState().user;
45 const todo = new TodoModel(user, title, description, finished, id);
46
47 try {
48 await todo.edit();
49 } catch (err) {
50 setError(err.message);
51 return;
52 }
53
54 setPassedTitle(title);
55 setPassedDesc(description);
56 setPassedFinished(finished);
57 setVisible(false);
58 };
59
60 const deleteTodoFromDialog = () => {
61 const user = store.getState().user;
62 const todo = new TodoModel(user, title, description, finished, id);
63
64 try {
65 todo.dismiss();
66 } catch (err) {
67 setError(err.message);
68 return;
69 }
70
71 setVisible(false);
72 props.removeTodo(id);
73 };
74
75 return (
76 <>
77 <List.Item
78 onPress={() => {
79 setVisible(true);
80 }}
81 title={passedTitle}
82 description={passedDesc}
83 right={pprops => {
84 if (passedFinished) {
85 return (
86 <List.Icon
87 {...pprops}
88 color={Colors.green300}
89 icon="check-circle"
90 />
91 );
92 }
93
94 return null;
95 }}
96 />
97
98 <Portal>
99 <Dialog visible={visible} onDismiss={() => setVisible(false)}>
100 <Dialog.Title>Edit your todo</Dialog.Title>
101 <Dialog.Content>
102 <Paragraph>
103 Editing your todo will also change it in Strapi.
104 </Paragraph>
105
106 <View style={styles.divider} />
107
108 <TextInput
109 label="title"
110 placeholder="title"
111 onChangeText={text => {
112 setTitle(text);
113 setError(false);
114 }}>
115 {title}
116 </TextInput>
117
118 <View style={styles.divider} />
119
120 <TextInput
121 label="description"
122 placeholder="description"
123 multiline={true}
124 numberOfLines={4}
125 onChangeText={text => {
126 setDescription(text);
127 setError(false);
128 }}>
129 {description}
130 </TextInput>
131
132 <HelperText type="error">{error}</HelperText>
133 {error.length > 0 ? <View style={styles.divider} /> : null}
134
135 <View
136 style={{
137 flexDirection: 'row',
138 alignContent: 'center',
139 }}>
140 <Checkbox
141 status={finished ? 'checked' : 'unchecked'}
142 onPress={() => {
143 setFinished(!finished);
144 }}
145 />
146 <Paragraph style={{paddingLeft: 16, alignSelf: 'center'}}>
147 Finished
148 </Paragraph>
149 </View>
150 </Dialog.Content>
151
152 <Dialog.Actions>
153 <Button onPress={() => deleteTodoFromDialog()}>delete</Button>
154 <View style={{flex: 1}} />
155 <Button
156 onPress={() => {
157 setVisible(false);
158 setError('');
159 }}>
160 Cancel
161 </Button>
162 <Button onPress={() => editTodoFromDialog()}>Save</Button>
163 </Dialog.Actions>
164 </Dialog>
165 </Portal>
166 </>
167 );
168 };
169
170 const styles = StyleSheet.create({
171 divider: {
172 height: 16,
173 },
174 });
175
176 export default TodoView;
Finally, include the Views into the Overview screen created earlier.
1 // Overview.js
2
3 import React from 'react';
4 import {StatusBar} from 'react-native';
5 import {TodoList} from '../components/lists/TodoList';
6
7 const Overview = props => {
8 return (
9 <>
10 <StatusBar backgroundColor="#ffffff" barStyle="dark-content" />
11 <TodoList />
12 </>
13 );
14 };
15
16 export default Overview;
We created a mobile app that supports creating, editing, deleting to-dos on a user basis and can display them in a paginated list that is always up to date with the data on the server and thus synchronized across devices. [
Chigozie is a technical writer. He started coding since he was young, and entered technical writing recently.