My goal in this post is to explore creating a Drupal powered mobile app with React Native. With no prior experience with React Native or React JS myself, this is truly an exploratory quest. I do have some experience with Angular 2-7 and AngularJS before it, so I admit I might have some more familiarity with concepts and code structure.
What is necessary however, is an understanding of a few core things, namely Javascript 6 (ES6), as React Native supports and uses it, as well as Promises and HTTP Requests.
To explore React Native, I opted to replicate the sample ReactJS + NextJS sample application available on GitHub and visible here powered by this Drupal site as a React Native application. In the process, I’ll explain the core concepts I felt I had to learn and provide code where applicable.
While it's not necessary to know ReactJS at all to get started with the React Native tutorial, as it assumes you have no prior knowledge of React or React JS, it would probably be helpful if you were at least a little familiar with ReactJS. I myself spent a little time familiarizing myself with how ReactJS works and even building a simple application before starting to work with React Native.
Building the application
Disclaimer: My aim is not to create a beautifully styled app, but simply get something up and working as quickly as possible. I added minimal styling. The app was built on a Mac for iOS. So let's begin.
You can find the finished code on GitHub.
First, create a new project, I named mine DrupalReactNative:
react-native init DrualReactNative
Project Structure
The first decision I made was how to structure the project. I came to Atomic Design because I stumbled upon an article that pointed out React follows the same principles as Atomic Design. I’d never used Atomic Design outside of the context of CSS, so I did look for examples of how this was implemented in React.
cd DrupalReactNative/
mkdir src
cd src
mkdir components
cd components
mkdir atoms molecules organisms pages templates
After some research, specifically looking at the site I was building, I settled on the following folder structure and breakdown of files:
This is by no means the “correct” way to organize folders, but this is what I settled on. We will come back to the code later.
Additional Libraries
I chose to use React Navigation (and related required modules) to handle the app navigation, and despite React implementing the straightforward Fetch API for network requests, I was a little lazy and used React Native Rest Client for network requests. This made the networking tasks even easier and React Native Vector Icons for menu icons.
From root directory:
npm install react-navigation react-navigation-stack
npm install react-native-gesture-handler react-native-rest-client
npm install react-native-screens react-native-reanimated
npm install react-native-vector-icons
Then install the pods for IOS:
cd ios
pod install
You will then have to manually install Vector Icons.
Note: I found the only thing that worked was adding font to Xcode. Failure to do this will result in an unknown font error when trying to compile.
Routing/Navigation
At this point, on running react-native run-ios, you should get the default app with the default screen. Now we will replace the contents of the App.js file with the following:
import React from 'react';
import RecipeApp from './src/RecipeApp';
const App = () => {
return (
<RecipeApp />
);
}
export default App;
Then create the referenced file, RecipeApp.js
in the src directory:
import React from "react";
import { createAppContainer,} from "react-navigation";
import { createStackNavigator } from "react-navigation-stack";
import HomePage from "./components/pages/Homepage/index";
import Recipes from "./components/pages/Recipes/index";
const MainNavigator = createStackNavigator({
Homepage: HomePage,
Recipes: Recipes,
},
{
initialRouteName: 'Homepage',
headerMode: 'none',
},
);
const RecipeApp = createAppContainer(MainNavigator);
export default RecipeApp;
The code references 2 custom items which we will also create.
First, the homepage which will be in the pages folder in the components directory:
import React from 'react';
import { Text, View, Button } from 'react-native';
export default class HomePage extends React.Component{
render() {
return (
<View style={{margin: 20, marginTop: 30, flex: 1}}>
<Text style={{marginTop: 10, fontSize: 28, fontWeight: 600}}>Contenta + React Native</Text>
<View style={{flex: 1, flexDirection: 'row'}}>
<Button
style={{flexDirection: 'row'}}
onPress={() => {
this.props.navigation.navigate('Homepage');
}}
title="Home"
/>
<Button
style={{flexDirection: 'row'}}
onPress={() => {
this.props.navigation.navigate('Recipes');
}}
title="Recipes"
/>
</View>
<View style={{flex: 1}}>
<Text style={{fontSize: 24, fontWeight: 600}}>Home</Text>
<Text>Lorem Ipsum</Text>
</View>
</View>
);
}
}
I added some very basic styling. It’s worth mentioning that styling in ReactNative, like styling in React, is in JavaScript, hence the camel case code.
I practically repeat the same for the Recipes page, which should now give us 2 screens and a working navigation:
import React from 'react';
import { Text, View, Button } from 'react-native';
export default class Recipes extends React.Component{
render() {
return (
<View style={{margin: 20, marginTop: 30, flex: 1}}>
<Text style={{marginTop: 10, fontSize: 28, fontWeight: 600}}>Contenta + React Native</Text>
<View style={{flex: 1, flexDirection: 'row'}}>
<Button
style={{flexDirection: 'row'}}
onPress={() => {
this.props.navigation.navigate('Homepage');
}}
title="Home"
/>
<Button
style={{flexDirection: 'row'}}
onPress={() => {
this.props.navigation.navigate('Recipes');
}}
title="Recipes"
/>
</View>
<View style={{flex: 1}}>
<Text style={{fontSize: 24, fontWeight: 600}}>Recipes</Text>
<Text>Ipsum Lorem</Text>
</View>
</View>
);
}
}
With that you should have an app with two screens and screen to screen navigation.
Components and Data
With the basic structure in place, we can now jump to creating components and retrieving data. For this I copied most of the structure of the React + NextJS repo and updated the code for React Native.
I had to create a custom class to transform the JSON:API response into a usable object (JSON:API, part of Drupal core, is one of the key modules used in Contenta as it’s API first). This was necessary because all attempts to normalize the response using various libraries failed. The following code is used to process all incoming responses:
import React from 'react';
export default class Transform extends React.Component {
transformJson(obj) {
let nodes = [];
if (typeof obj.data.type !== 'undefined' && obj.data.type == 'recipes') {
item = obj.data;
const node = {
id: item.id,
title: item.attributes.title,
difficulty: item.attributes.difficulty || '',
ingredients: item.attributes.ingredients || [],
numberofServices: item.attributes.numberofServices || 0,
preparationTime: item.attributes.preparationTime || '',
instructions: item.attributes.instructions || '',
totalTime: item.attributes.totalTime || '',
image: transformJsonGetImage(obj, item.relationships.image.data.id),
category:
typeof item.relationships.category !== 'undefined'
? transformJsonGetCat(
obj,
item.relationships.category.data.id,
'categories',
)
: '',
};
nodes[0] = node;
} else {
obj.data.forEach(function(item, index) {
const node = {
id: item.id,
title: item.attributes.title,
difficulty: item.attributes.difficulty || '',
ingredients: item.attributes.ingredients || [],
numberofServices: item.attributes.numberofServices || 0,
preparationTime: item.attributes.preparationTime || '',
instructions: item.attributes.instructions || '',
totalTime: item.attributes.totalTime || '',
image: transformJsonGetImage(obj, item.relationships.image.data.id),
category:
typeof item.relationships.category !== 'undefined'
? transformJsonGetCat(
obj,
item.relationships.category.data.id,
'categories',
)
: '',
tags:
typeof item.relationships.tags !== 'undefined'
? transformJsonGetTags(obj, item.relationships.tags)
: '',
};
nodes.push(node);
});
}
return nodes;
}
}
// Gets tags.
export function transformJsonGetTags(res, obj) {
let tags = [];
obj.data.forEach(function(item, index) {
tags.push(transformJsonGetCat(res, item.id, 'tags'));
});
return tags;
}
// Gets category of item.
export function transformJsonGetCat(obj, id, type = 'tags') {
name = '';
obj.included.forEach(function(item, index) {
if (item.id == id && item.type == type) {
name = item.attributes.name;
}
});
return name;
}
// Gets image
export function transformJsonGetImage(obj, id) {
fid = '';
src = '';
obj.included.forEach(function(item, index) {
if (item.id == id && item.type == 'images') {
fid = item.relationships.thumbnail.data.id;
}
});
obj.included.forEach(function(item, index) {
if (item.id == fid && item.type == 'files') {
src = item.attributes.url;
}
});
return src;
}
Now to update the homepage:
The homepage contained 5 components and api calls to supply data.
import React from 'react';
import {View} from 'react-native';
import Header from '../../../components/organisms/Header/index';
import {ScrollView} from 'react-native-gesture-handler';
import RecipeApi from '../../../api/recipe';
import PromotedRecipes from '../../organisms/PromotedRecipes/index';
import Transform from '../../../utils/Transform';
import MonthEdition from '../../organisms/MonthEdition/index';
import HomeWidgets from '../../organisms/HomeWidgets/index';
import RecipeList from '../../organisms/RecipeList/index';
import styles from '../../templates/styles';
export default class HomePage extends React.Component {
constructor(props) {
super(props);
this.state = {
promotedItems: [],
latestRecipes: [],
};
}
initialize = () => {
recep = new RecipeApi();
t = new Transform();
try {
recep
.getPromoted()
.then(response => response)
.then(responseJson => {
const transformed = t.transformJson(responseJson);
this.setState({promotedItems: transformed});
})
.catch(error => {
console.error(error);
});
} catch (e) {}
try {
recep
.getAll()
.then(response => response)
.then(responseJson => {
const transformed = t.transformJson(responseJson);
this.setState({latestRecipes: transformed});
})
.catch(error => {
console.error(error);
});
} catch (e) {}
};
componentDidMount() {
this.initialize();
}
render() {
return (
<View style={styles.container}>
<ScrollView stickyHeaderIndices={[0]}>
<Header navigation={this.props.navigation} style={styles.header} />
<View>
<PromotedRecipes
recipes={this.state.promotedItems}
navigation={this.props.navigation}
/>
<MonthEdition navigation={this.props.navigation} />
<HomeWidgets />
<RecipeList
recipes={this.state.latestRecipes}
navigation={this.props.navigation}
/>
</View>
</ScrollView>
</View>
);
}
}
I won't get into the details of each component on the homepage and will just link to the repository here.
The Recipe page contains 2 components, a node view of a node ID is supplied and a component that displays 10 recipes if there is no Node ID. Again, I won't get into the details of those components.
import React from 'react';
import {View, Text, StyleSheet, ScrollView, Button} from 'react-native';
import NodeView from '../../organisms/NodeView/index';
import RecipeApi from '../../../api/recipe';
import Transform from '../../../utils/Transform';
import styles from '../../templates/styles';
import Header from '../../organisms/Header/index';
import RecipeList from '../../organisms/RecipeList/index';
export default class Recipes extends React.Component {
constructor(props) {
super(props);
this.state = {
nodes: [],
allNodes: [],
};
}
initialize = () => {
recep = new RecipeApi();
t = new Transform();
nid = this.props.navigation.getParam('nid', null);
if (nid) {
try {
recep
.get(nid)
.then(response => response)
.then(responseJson => {
const transformed = t.transformJson(responseJson);
this.setState({nodes: transformed});
})
.catch(error => {
console.error(error);
});
} catch (e) {}
} else {
try {
recep
.getAll(20)
.then(response => response)
.then(responseJson => {
const transformed = t.transformJson(responseJson);
this.setState({allNodes: transformed});
})
.catch(error => {
console.error(error);
});
} catch (e) {}
}
};
componentDidMount() {
this.initialize();
}
goBack() {
this.setState({
allNodes: [],
node: [],
});
this.props.navigation.goBack();
}
render() {
return (
<View style={styles.container}>
<ScrollView>
<Header navigation={this.props.navigation} style={styles.header} />
<Button title="Go back"
onPress={() => {
this.goBack();
}}
/>
<View>
{this.state.allNodes.length > 0 ? (
<RecipeList
recipes={this.state.allNodes}
navigation={this.props.navigation}
/>
) : (
<NodeView
node={this.state.nodes}
nid={this.props.navigation.getParam('nid', '')}
/>
)}
</View>
</ScrollView>
</View>
);
}
}
The finished product looks something like this:
Conclusion
There was only a slight learning curve getting up to speed on most things with React Native and as such I certainly think an experienced developer could easily make the transition.
Prior to using ReactNative, there was a certain mystique, but it turned out to not be less complicated than I anticipated. There were certainly some challenges finding the right documentation for versions of modules I was using, but this is not unlike many Open Source projects. A few modules mentioned in tutorials or documentation I tried to follow were updated or deprecated, but save for a few issues with this, it was relatively straightforward to get the app up and running.
Judging from my experience so far, I may certainly be a convert to React Native and will explore opportunities to build more Drupal powered native apps.