Meteor and React Native - Create a native mobile app
Jan KΓΌster
Posted on October 26, 2022
This is a full-scale workshop. Estimated time: 60min to 120min overall. We are going to create a working mobile app that includes authentication, a profile page and a managed ToDo-list. The app is developed with React-Native and uses a backend, implemented with Meteor.
Please note, that I can't cover all operating systems out there.
About
Meteor and React Native are not integrated with each other by default.
However, there are great packages out there, that help us to make them integrate.
The best is, it's actually not that difficult!
This starter brings the most basic integration for a Meteor project as a backend for your react native app.
Just follow the instructions in this readme to get startet immediately.
Installation
You need to have Meteor installed on your system.
Follow the Meteor installation instructions on the Meteor website.
Create a new project from this template repo
This repo is a template repo so you can create your own project from itβ¦
This workshop is a result from my session at Meteor Impact 2022, which missed out the last part due to time constraints. Therefore I reviewed the workshop and the code and improved on everything. π
Get your Terminals and IDEs ready, grab a hot beverage and make yourself comfortable for a full hands-on session over the next two hours.
During the workshop we will cover these major topics:
installation of the dev environment
create the Meteor backend
create the React Native app
connect/reconnect the RN app to the backend
full authentication workflow using react navigation
CRUD a personal Tasks collection (using Meteor Methods and Publication/Subscription)
If you are new to Meteor or React Native, let me shortly introduce them to you (or skip to the next section if you already know).
Meteor is a fullstack cross-platform framework to develop modern JavaScript applications. It can either be used with your favorite frontend engine/library to create SPAs (single page applications) with Websockets and Publish/Subscribe. Nevertheless, it also supports SSR, HTTP routes and integrates well with the vast majority of packages, installed from NPM, because under the hood it's all NodeJS.
React Native is also a cross-platform framework but for developing native mobile apps by using one of the world's most famous JavaScript libraries: React. But don't get it wrong! This is not just building some WebView-containered HTML5 app. It will render all your code using the mobile platform's native engine capabilities.
π€ Together they provide a great developer experience for developing mobile apps. They both run on many platforms and deploy to different platforms at the same time. For example, we can run our development environment on Ubuntu Linux but run our development build on an iOS device.
You can also check out and contribute to their repositories on GitHub:
React Native brings React's declarative UI framework to iOS and Android. With React Native, you use native UI controls and have full access to the native platform.
Declarative. React makes it painless to create interactive UIs. Declarative views make your code more predictable and easier to debug.
Component-Based. Build encapsulated components that manage their state, then compose them to make complex UIs.
Developer Velocity. See local changes in seconds. Changes to JavaScript code can be live reloaded without rebuilding the native app.
Portability. Reuse code across iOS, Android, and other platforms.
React Native is developed and supported by many companies and individual core contributors. Find out more in our ecosystem overview.
One more thing, before we start with our hands-on workshop. Let me provide an overview for those of you who prefer a visual. Consider the Meteor app as server (backend) and the React Native app as one of it's clients:
As you can see the both will communicate using @meteorrn/core. The package implements most of the api as the Meteor client bundle on the web. However, it also provides an easy way to hook into some lower level API events that makes it in combination with useEffect very easy to act upon changes from the server.
The backend also manages the authentication via Accounts automatically, while we will use a Navigation with React contexts on the client. Notice, that authentication will also use features from @meteorrn/core that allow us to retrieve the login token to implement an "auto-login" functionality.
Now, let's finally start the actual workshop. β¨
1. Prerequisites π§πͺπͺ
In order to make the most out of the workshop you should gather at least one physical mobile device. It doesn't matter, whether it's Android or iOS or even both, React Native and Expo will take care of the bundling details.
1.1 Install Expo Go (or use emulators)
You should install the "Expo Go" app on these devices as it will be a huge help to us when we will preview our development builds. You can find the App in the stores by searching for "Expo Go" or directly under these links:
βStart building projects using web technologies with just your iOS device and your computer. Expo is a developer tool for creating experiences with interactive gestures and graphics using JavaScript and React.
Note: some programming experience is recommended.
Technical specs: this version of Expo usβ¦
βββ Notice, that you will not necessarily have to signup to use the Expo Go app, even if it looks like that at a first glance.
Install emulators as alternative
If you really don't have access to a physical device or you face certain issues with your devices during this workshop you might consider installing the respective emulators.
βββ Important
In this workshop you have to use meteor npm in your terminals from now on, instead of just npm. It is necessary to always use the correct linked binaries.
This is due to the Meteor tool ships with a bundled NPM that has it's own path on your system. It gets removed, including all installed dependencies, once you remove Meteor.
Verify Meteor and it's NPM via
$ meteor --version$ meteor npm -v
1.3 Install Expo CLI
React Native development is totally possible without Expo. However, with Expo it just starts to feel comfortable. No worries about building, no Android SDK or XCode to install. Hell, you can make a development build for iOS on a Linux machine. At that point they got me already.
π Notice
However, there are also a few downsides. First, you have to stick with the Expo Sdk and regularly update the Sdk.
Second, if you want to deploy your apps for the App Store / Play Store you will likely need to use their cloud services (EAS). Allthough they offer a free tier, it may introduce costs and finally, this is still a dependency to a third party when it comes to your deployment. The good side though is, that this services takes care of the really nasty sides of mobile app deployment, so it might not be that bad at all.
Enough talk, let's install the Expo CLI tool globally:
$ meteor npm install-g expo-cli
βββ Important
This causes the expo cli to be available only in the Meteor namespace, so you will need to call meteor expo instead of just expo later on.
1.4 Create project repository or local folder
Once the tools are installed we can actually create the new projects. In order to keep track of everything you can either create a new empty GitHub repository and clone it locally or create a local folder that you later connect with an existing repository. In both ways, let's name the project mrntodos for this workshop.
Create a new Meteor project is straight forward using meteor create [options] [name]. However, with Meteor we have many choices for our frontend: React, Vue, Svelte, Solid (coming in 2.8), Blaze, Apollo, headless and some more (read the docs for all options). Meteor integrates them for you automatically on project creation.
π Notice
The term "backend" is a bit misleading here. The Meteor application is a fullstack app, providing a server and client build for you. However, for this workshop we will only use the server environment, making the Meteor app act as the "backend" for the mobile app.
2.1 Create a new Meteor app
For the workshop we use Meteor's default create command (which uses React as frontend):
$ cd mrntodos # if you are not already in the project folder$ meteor create backend
$ cd backend
$ echo"{}"> settings.json
We can configure global app settings in Meteor using a JSON file (backend/settings.json), that gets injected into the process environment by starting meteor with --settings. It comes in very handy when you have to manage multiple deployments that differ in their configuration.
In order to make the Meteor app automatically start on a custom port and to use our settings.json, let's change one of the scripts in backend/package.json:
If you see the following message output, everything is fine and you can continue to the next section:
=> App running at: http://localhost:8000/
2.2 Add authentication layer to the Meteor backend
Meteor provides core packages for zero- to minimal-config authentication, named Accounts. Using accounts-password you get an out-of-the-box OAuth2 authentication and the servers also uses bcrypt to encrypt the password on the server.
Accounts is also configurable (documentation) and we should change their defaults according to our needs. In order to do that, add a new file under backend/imports/startup/server/accounts.js (create these folders, if they don't exist yet). Then add the following to this file:
import{Accounts}from'meteor/accounts-base'import{Meteor}from'meteor/meteor'// Here we define the fields that are automatically // available to clients via Meteor.user().// This extends the defaults (_id, username, emails) // by our profile fields.// If you want your custom fields to be immediately // available then place them here.constdefaultFieldSelector={_id:1,username:1,emails:1,firstName:1,lastName:1}// merge our config from settings.json with fixed code// and pass them to Accounts.configAccounts.config({...Meteor.settings.accounts.config,defaultFieldSelector})
Now add the following parts to your settings.json file:
π Notice
The accounts-base package is automatically added when accounts-password is added. When importing Accounts in your code you will use import { Accounts } from 'meteor/accounts-base'.
Finally, make sure our startup file is imported in backend/server/main.js. Open this file and replace it's default content with:
import'../imports/startup/server/accounts'
2.3 Add registration Method endpoint
By default Meteor Accounts allow clients to register themselves via Accounts.createUser (docs). This is 1:1 supported in the @meteorrn/core package, too.
However, we want to have additional functionality during the registration process, like sending an enrollment email and adding default profile fields to the user document.
Therefore, we create a new registration endpoint as Meteor Method. We create a new file in backend/imports/accounts/methods.js and add the following code to it:
import{Accounts}from'meteor/accounts-base'import{check,Match}from'meteor/check'exportconstregisterNewUser=function (options){check(options,Match.ObjectIncluding({email:String,password:String,firstName:String,lastName:String,loginImmediately:Match.Maybe(Boolean)}))const{email,password,firstName,lastName,loginImmediately}=optionsif (Accounts.findUserByEmail(email)){thrownewMeteor.Error('permissionDenied','userExists',{email})}constuserId=Accounts.createUser({email,password})// we add the firstName and lastName as toplevel fields// which allows for better handling in publicationsMeteor.users.update(userId,{$set:{firstName,lastName}})// let them verify their new account, so// they can use the full app functionalityAccounts.sendVerificationEmail(userId,email)if (loginImmediately){// signature: { id, token, tokenExpires }returnAccounts._loginUser(this,userId)}// keep the same return signature here to let clients// better handle the responsereturn{id:userId,token:undefined,tokenExpires:undefined}}
Finally, make sure this file is imported at startup by creating a new Meteor Method with it in in our already created backend/imports/startup/server/accounts.js file:
If you also want users to update their profile or delete their account then you need an endpoint Method for them, too.
Let's extend the backend/imports/accounts/methods.js file by these two methods:
// ... registerNewUserexportconstupdateUserProfile=function ({firstName,lastName}){check(firstName,Match.Maybe(String))check(lastName,Match.Maybe(String))// in a meteor Method we can access the current user// via this.userId which is only present when an// authenticated user calls a Methodconst{userId}=thisif (!userId){thrownewMeteor.Error('permissionDenied','notAuthenticated',{userId})}constupdateDoc={$set:{}}if (firstName){updateDoc.$set.firstName=firstName}if (lastName){updateDoc.$set.lastName=lastName}return!!Meteor.users.update(userId,updateDoc)}exportconstdeleteAccount=function (){const{userId}=thisif (!userId){thrownewMeteor.Error('permissionDenied','notAuthenticated',{userId})}return!!Meteor.users.remove(userId)}
Finally, update the startup file at backend/imports/startup/server/accounts.js:
import{Accounts}from'meteor/accounts-base'import{Meteor}from'meteor/meteor'import{registerNewUser,updateUserProfile,deleteAccount}from'../../accounts/methods'// ... other codeMeteor.methods({registerNewUser,deleteAccount,updateUserProfile})
At this point you have the minimal api defined for your app to sign up, sign in, sign out (builtin, via Meteor.logout()), delete account and update profile. Let's continue by building the mobile app now.
3. Create and set up the React Native app
In this section we will create a new React Native app with an Expo-managed workflow. Make sure you have set up all required tools from section 1. If you have no physical device available, you can also install the Android emulator or iOS simulator (also covered in section 1).
3.1 Create the new React Native app
First of all, open a new terminal in order to install and run the app. The backend and the app will use separate node processes. Therefore, to manage them both effectively you should work on them in separate terminals.
If you haven't installed expo-cli yet or you have updated your Meteor version, then you need to install it via:
$ meteor npm install-g expo-cli
$ meteor expo --version
6.0.6 # at the time of this workshop
Within the project root folder (mrntodos/), create a new expo project and answer the questions as following:
$ cd mrntodos # if not already there$ meteor expo init
? What would you like to name your app? βΊ app
? Choose a template: βΊ - Use arrow-keys. Return to submit.
----- Managed workflow -----
β― blank a minimal app as clean as an empty canvas
π Notice, that you can also use different workflows, for example if you prefer to use TypeScript. Choose on your own or stick with this workshop's preference.
Then we also need to install some dependencies here that will be necessary for us during the workshop:
βββ Important
We need to use meteor expo install to install our dependencies. This is, because Expo resolves the correct dependency versions for the current Sdk for us. By doing so, we don't need to fiddle with package versions that may break our builds or are fundamentally incompatible.
After adding the package you may get some warnings from the npm audit. If you are like me and care about this, please help us to release the next version of @meteorrn/core with updated dependencies by testing the latest commits with your local build. Add your review or issues to: https://github.com/meteorrn/meteor-react-native
3.2 Set proper network settings
Connecting your app to the backend requires some further configuration. You need to obtain your local network ip in order to make the RN app connect. The Meteor-typical localhost will not work here, since this would not resolve to the same local ip when the code runs on the mobile device.
You can usually obtain your local ip via one of these commands:
os
command
Linux
ip addr show
MacOs
ifconfig
Windows
ipconfig
Once you obtained the ip (at home this is often a 192.168.178.XXX or similar) please create a config file with this value:
$ cd app # if not already there$ echo"{}"> config.json
Place the following content in there (where xxx.xxx.xxx.xxx is replaced by your obtained local ip):
We can use a JavaScript engine, named Hermes, which is entirely optimized for React Native. In fact, we should. Consult the React Native docs page on Hermes to understand why.
In order to activate it with Expo, we need to edit the settings in app/app.json:
That's all and Expo takes care of the rest for us.
4. Connect the app to the Meteor backend
Nearly all functionality of our app requires a connection to our backend. Meteor establishes connections and exchanges data via DDP, a custom protocol that builds on top of Websockets. The great thing is, that you don't have to worry about this whole transport layer as Meteor abstracts all of this logic for you so you can focus on the important things.
4.1 Write a connection hook
We want to remain flexible with the connection and the way we deal with it's state. This is why we will abstract it into a custom React hook, which we name useConnection.
Create a new file at app/src/hooks/useConnection.js and add the following code there:
import{useEffect,useState}from'react'importMeteorfrom'@meteorrn/core'import*asSecureStorefrom'expo-secure-store'importconfigfrom'../../config.json'// get detailed info about internalsMeteor.isVerbose=true// connect with Meteor and use a secure store// to persist our received login token, so it's encrypted// and only readable for this very app// read more at: https://docs.expo.dev/versions/latest/sdk/securestore/Meteor.connect(config.backend.url,{AsyncStorage:{getItem:SecureStore.getItemAsync,setItem:SecureStore.setItemAsync,removeItem:SecureStore.deleteItemAsync}})exportconstuseConnection=()=>{const[connected,setConnected]=useState(null)const[connectionError,setConnectionError]=useState(null)// we use separate functions as the handlers, so they get removed// on unmount, which happens on auto-reload and would cause errors// if not handleduseEffect(()=>{constonError=(e)=>setConnectionError(e)Meteor.ddp.on('error',onError)constonConnected=()=>connected!==true&&setConnected(true)Meteor.ddp.on('connected',onConnected)// if the connection is lost, we not only switch the state// but also force to reconnect to the serverconstonDisconnected=()=>{Meteor.ddp.autoConnect=trueif (connected!==false){setConnected(false)}Meteor.reconnect()}Meteor.ddp.on('disconnected',onDisconnected)// remove all of these listeners on unmountreturn ()=>{Meteor.ddp.off('error',onError)Meteor.ddp.off('connected',onConnected)Meteor.ddp.off('disconnected',onDisconnected)}},[])return{connected,connectionError}}
Classic Meteor + React provides a useTracker hook for Reactivity. In this case, we hook directly into the DDP events in order to update the connection state.
4.3 Integrate the useConnection hook into App.js
Let's move two levels up and open app/App.js, our main entry point for our React app. Here, we now integrate the useConnection hook and render messages, based on the connection state:
importReactfrom'react'import{MainNavigator}from'./src/screens/MainNavigator'import{StyleSheet,View,Text,ActivityIndicator}from'react-native'import{useConnection}from'./src/hooks/useConnection'exportdefaultfunctionApp(){const{connected,connectionError}=useConnection()// use splashscreen here, if you likeif (!connected){return (<Viewstyle={styles.container}><ActivityIndicator/><Text>Connecting to our servers...</Text></View>)}// use alert or other things here, if you likeif (connectionError){return (<Viewstyle={styles.container}><Text>Error, while connecting to our servers!</Text><Text>{connectionError.message}</Text></View>)}return (<Viewstyle={styles.container}><Text>We are connected!</Text></View>)}conststyles=StyleSheet.create({container:{flex:1,backgroundColor:'#efefef',alignItems:'center',justifyContent:'center'}})
Start the app and see the We are connected! message on the screen. Now switch to the other terminal for our backend and hit ctrl+c to cancel the process. Take a look at your device - it will immediately show Connecting to our servers... as it tries to reconnect. Now start the backend server again and see the immediate change back to the connected state.
You have a reactive connection status now π₯³ You could use this hook even further to build a offline-first app but that won't be covered by our workshop.
From here we can move on to the authentication workflow, but before that we need to do a short refactoring!
4.4 Tidy up code
There are a few elements that we will reuse a few more times in this app. It's a good practice to not repeat to write the same code all over again.
One potential saving is to create a default stylesheet, which you can compare to having a main.css file on the web. It helps to provide a consistent layout across our app and a single point to manage this layout.
Let's create a new file at app/src/styles/defaultStyles.js and place the following content there:
Second, let's create a new ErrorMessage component. It will also use some of the default styles already. Create the new file at app/src/components/ErrorMessage.js and add the following code to it:
With both of these abstractions we can, again, update app/App.js and replace
// use alert or other things here, if you likeif (connectionError){return (<Viewstyle={styles.container}><Text>Error, while connecting to our servers!</Text><Text>{connectionError.message}</Text></View>)}
with
// use alert or other things here, if you likeif (connectionError){return (<ErrorMessageerror={connectionError}/>)}
5. Implement the authentication workflow
One of the most important parts of our application is to provide a fluent and accessible authentication workflow. Users should not have to sign in each time they use the app. Rather a login token should be stored securely (which is why we installed expo-secure-store) and used until expiration (which we defined in section 2 using Accounts.config).
If users don't have an account, they should sign up by providing email, password, first name and last name. Once the account is created, they should receive the login token and become signed-in automatically. Finally, users should be able to sign out and delete their account.
The following graphics summarizes the auth workflow from a more abstract perspective, including the screens to navigate:
This workflow is inspired by the React Nativation authentication workflow and we will use this library to implement the workflow within our own environmental boundaries.
5.1 Create the authentication context and api layer
As with the connection we want to decouple authentication from our rendering logic as much as possible in React. This also helps to keep the project not tightly coupled to any Meteor logic. At the same time we want to have a unified place of where the authentication state is managed.
5.1.1 Create a new authentication context
React contexts provides a way to access our auth layer without the need to passing it down through the whole component tree and helps to prevent tight coupling. Read more in the React docs on contexts, if you want to understand how it works in details.
Since there will be multiple screens, that make use of our authentication, we create and export our authentication context at app/src/contexts/AuthContext.js:
The whole "magic" will happen here. This is where calls from the components will be passed through as DDP requests to the backend and responses will update the state accordingly. In contrast, errors will be passed back to the components to make them render error messages.
Due to the all this resulting in a more complex state we should use a React reducer. Our authentication functions should also be only created once, which is why we wrap them in a useMemo hook.
Create the file app/src/hooks/useAuth.js and add the following content:
import{useReducer,useEffect,useMemo}from'react'importMeteorfrom'@meteorrn/core'constinitialState={isLoading:true,isSignout:false,userToken:null}constreducer=(state,action)=>{switch (action.type){case'RESTORE_TOKEN':return{...state,userToken:action.token,isLoading:false}case'SIGN_IN':return{...state,isSignOut:false,userToken:action.token}case'SIGN_OUT':return{...state,isSignout:true,userToken:null}}}constData=Meteor.getData()exportconstuseAuth=()=>{const[state,dispatch]=useReducer(reducer,initialState,undefined)// Case 1: restore token already exists// MeteorRN loads the token on connection automatically,// in case it exists, but we need to "know" that for our auth workflowuseEffect(()=>{consthandleOnLogin=()=>dispatch({type:'RESTORE_TOKEN',token:Meteor.getAuthToken()})Data.on('onLogin',handleOnLogin)return ()=>Data.off('onLogin',handleOnLogin)},[])constauthContext=useMemo(()=>({signIn:({email,password,onError})=>{Meteor.loginWithPassword(email,password,async (err)=>{if (err){if (err.message==='Match failed [400]'){err.message='Login failed, please check your credentials and retry.'}returnonError(err)}consttoken=Meteor.getAuthToken()consttype='SIGN_IN'dispatch({type,token})})},signOut:({onError})=>{Meteor.logout(err=>{if (err){returnonError(err)}dispatch({type:'SIGN_OUT'})})},signUp:({email,password,firstName,lastName,onError})=>{constsignupArgs={email,password,firstName,lastName,loginImmediately:true}Meteor.call('registerNewUser',signupArgs,(err,credentials)=>{if (err){returnonError(err)}// this sets the { id, token } values internally to make sure// our calls to Meteor endpoints will be authenticatedMeteor._handleLoginCallback(err,credentials)// from here this is the same routine as in signInconsttoken=Meteor.getAuthToken()consttype='SIGN_IN'dispatch({type,token})})},deleteAccount:({onError})=>{Meteor.call('deleteAccount',(err)=>{if (err){returnonError(err)}// removes all auth-based data from client// as if we would call signOutMeteor.handleLogout()dispatch({type:'SIGN_OUT'})})}}),[])return{state,authContext}}
π Notice, that this file requires some explanation, of course.
the reducer handles the internal state, manipulated via dispatch calls
the @meteorrn/core packages will (once connected) automatically check for an existing token in the provided secure store; if found, it will try to login with token and emit a ' onLogin' event, if that succeeded; we can leverage this in the useEffect hook to implement our "auto-login" feature
the authContext is retrieved in screens with the previously created AuthContext and useContext as you will see in the upcoming sections
the several functions signIn, signOut, signUp and deleteAccounts leverage mostly what the @meteorrn/core package uses internally during Meteor.loginWithPassword or Meteor.logout
you could replace their code entirely if you need to migrate off Meteor (I hope you don't β€οΈ)
5.2 Create the screens
In the following step we will create all necessary screens that are involved in the workflow.
All screens should be created in app/src/screens, which you can create via
$ cd app # if not already in app folder$ mkdir-p src/screens
5.2.1 HomeScreen
The home screen at app/src/screens/HomeScreen.js will contain nothing but a message for now:
The login screen provides a simple input for email and password, where the password field uses secureTextEntry to hide characters.
If the authentication fails, there should be an error message being displayed. If the user no account yet, the "Sign up" button should trigger the navigation to signup:
First, the signIn method will be provided by useContext, where AuthContext acts as kind of a 'key' to make react return the correct value. Second, the component props will contain a navigation property, which is always injected when we use React Navigation.
5.2.3 RegistrationScreen
The register screen provides a similar form but will use a different auth method:
importReact,{useContext,useState}from'react'import{TextInput,Button,View}from'react-native'import{defaultColors,defaultStyles}from'../styles/defaultStyles'import{AuthContext}from'../contexts/AuthContext'import{ErrorMessage}from'../components/ErrorMessage'exportconstRegistrationScreen=()=>{const[email,setEmail]=useState()const[firstName,setFirstName]=useState()const[lastName,setLastName]=useState()const[password,setPassword]=useState()const[error,setError]=useState()const{signUp}=useContext(AuthContext)constonError=err=>setError(err)constonSignUp=()=>signUp({email,password,firstName,lastName,onError})return (<Viewstyle={defaultStyles.container}><TextInputplaceholder='Your Email'placeholderTextColor={defaultColors.placeholder}style={defaultStyles.text}value={email}onChangeText={setEmail}/><TextInputplaceholder='Your password'placeholderTextColor={defaultColors.placeholder}style={defaultStyles.text}value={password}onChangeText={setPassword}secureTextEntry/><TextInputplaceholder='Your first name (optional)'placeholderTextColor={defaultColors.placeholder}style={defaultStyles.text}value={firstName}onChangeText={setFirstName}/><TextInputplaceholder='Your last name (optional)'placeholderTextColor={defaultColors.placeholder}style={defaultStyles.text}value={lastName}onChangeText={setLastName}/><ErrorMessageerror={error}/><Buttontitle='Create new account'onPress={onSignUp}/></View>)}
5.2.4 ProfileScreen
The profile screen will only be available, once authenticated. For now we create on this screen only the sign-out and delete-account features.
As you might have realized already there are no "cancel" or "back" buttons on the screens and no "titles" rendered. This will all be handled by our React Navigation library. It allows us to remain flexible in the way screens are connected to each other.
On top of that, it helps us to render different screens, based on our authentication state and provides options on how the navigation bar is rendered.
5.3.1 Create the main navigation
Let's create the navigation at app/src/screens/MainNavigator.js and add the following code:
importReactfrom'react'import{CardStyleInterpolators}from'@react-navigation/stack'import{AuthContext}from'../contexts/AuthContext'import{NavigationContainer}from'@react-navigation/native'import{createNativeStackNavigator}from'@react-navigation/native-stack'import{useAuth}from'../hooks/useAuth'import{HomeScreen}from'./HomeScreen'import{LoginScreen}from'./LoginScreen'import{RegistrationScreen}from'./RegistrationScreen'import{ProfileScreen}from'./ProfileScreen'import{NavigateButton}from'../components/NavigateButton'constStack=createNativeStackNavigator()exportconstMainNavigator=()=>{const{state,authContext}=useAuth()const{userToken}=stateconstrenderScreens=()=>{if (userToken){// only authenticated users can visit these screensconstheaderRight=()=>(<NavigateButtontitle='My profile'route='Profile'/>)return (<><Stack.Screenname='Home'component={HomeScreen}options={{title:'Welcome home',headerRight}}/><Stack.Screenname='Profile'component={ProfileScreen}options={{title:'Your profile'}}/></>)}// non authenticated users need to sign in or register// and can only switch between the two screens below:return (<><Stack.Screenname='SignIn'component={LoginScreen}options={{title:'Sign in to awesome-app'}}/><Stack.Screenname='SignUp'component={RegistrationScreen}options={{title:'Register to awesome-app'}}/></>)}return (<AuthContext.Providervalue={authContext}><NavigationContainer><Stack.NavigatorscreenOptions={{cardStyleInterpolator:CardStyleInterpolators.forVerticalIOS}}>{renderScreens()}</Stack.Navigator></NavigationContainer></AuthContext.Provider>)}
Again, this code needs some further explanation:
the Stack is a navigator that renders a native push/pop animation when navigation back and forth; it looks really good out-of-the-box
state is the current state from the reducer in useAuth, while the authContext is injected into our components tree via <AuthContext.Provider value={authContext}>; without this it would not be accessible within the screens using useContext
the userToken is only present, when @meteorrn/core received a token or loaded it from the secure store and sucessfully signed in with it
5.3.2 Create a navigation button component
After adding the navigation, please also add a new NavigateButton component at app/src/components/NavigateButton.js and add the following code to it:
It helps us to avoid passing down navigation through the whole components tree and handles routing internally.
5.3.3 Integrate the navigation into App.js
There will be nothing new, if we start the app at this stage. This is because we need to integrate the MainNavigation into our App. Therefore, update app/App.js to the following final code:
importReactfrom'react'import{MainNavigator}from'./src/screens/MainNavigator'import{View,Text,ActivityIndicator}from'react-native'import{useConnection}from'./src/hooks/useConnection'import{ErrorMessage}from'./src/components/ErrorMessage'import{defaultStyles}from'./src/styles/defaultStyles'exportdefaultfunctionApp(){const{connected,connectionError}=useConnection()// use splashscreen here, if you likeif (!connected){return (<Viewstyle={defaultStyles.container}><ActivityIndicator/><Text>Connecting to our servers...</Text></View>)}// use alert or other things here, if you likeif (connectionError){return (<ErrorMessageerror={connectionError}/>)}return (<MainNavigator/>)}
5.4 Test all workflow steps
At this point you should be able to run the app and test the whole authentication workflow. Here are some screenshots of what to expect:
5.4.1 LoginScreen
Plain on enter:
When sign in failed:
5.4.2 RegistrationScreen
5.4.3 HomeScreen
5.4.4 ProfileScreen
5.5 Add a minimal user profile
This step provides an important insight on handling data reactivity from the Meteor backend in a way, that decouples from the rendering.
We want to update our user's profile (right now just the firstName and lastName fields) and reflect these changes immediately without the need to manually fetch the updated profile!
5.5.1 Create a useAccount hook
In order to do that we first create a new hook, named useAccount at app/src/hooks/useAccount.js and add the following code:
With this hook we create a similar (but not same) approach as with our auth context. However, in this case we want to keep the updateProfile functionality close to the user profile and therefore won't use a context here.
Note the useTracker, which is one of the fundamental pillars of Meteor's reactivity model. In combination with publish/subscribe we can receive an updated user profile in the moment the server updated it on the db-level.
π Notice, that you have not subscribed to any publications, yet the user document automatically updates when changed on the server. This is due to Meteor always auto-publishes changes to a user's own document and why we set defaultPublishFields for Accounts.config in section 2.
5.5.2 Integrate the useAccount hook into the ProfileScreen
In order to update the profile fields we need to provide another simple form and handle some of it's state. I won't go too deep into details here as this is only apply the dry principle to have a form for multiple fields.
This is now the updated app/screens/ProfileScreen.js file:
import{AuthContext}from'../contexts/AuthContext'import{defaultColors,defaultStyles}from'../styles/defaultStyles'import{Button,Text,TextInput,View,StyleSheet}from'react-native'import{useContext,useState}from'react'import{ErrorMessage}from'../components/ErrorMessage'import{useAccount}from'../hooks/useAccount'exportconstProfileScreen=()=>{const[editMode,setEditMode]=useState('')const[editValue,setEditValue]=useState('')const[error,setError]=useState(null)const{signOut,deleteAccount}=useContext(AuthContext)const{user,updateProfile}=useAccount()constonError=err=>setError(err)if (!user){returnnull// if sign our or delete}/**
* Updates a profile field from given text input state
* by sending update data to the server and let hooks
* reactively sync with the updated user document. *magic*
* @param fieldName {string} name of the field to update
*/constupdateField=({fieldName})=>{constoptions={}options[fieldName]=editValueconstonSuccess=()=>{setError(null)setEditValue('')setEditMode('')}updateProfile({options,onError,onSuccess})}constrenderField=({title,fieldName})=>{constvalue=user[fieldName]||''if (editMode===fieldName){return (<><Textstyle={styles.headline}>{title}</Text><Viewstyle={defaultStyles.row}><TextInputplaceholder={title}autoFocusplaceholderTextColor={defaultColors.placeholder}style={{...defaultStyles.text,...defaultStyles.flex1}}value={editValue}onChangeText={setEditValue}/><ErrorMessageerror={error}/><Buttontitle='Update'onPress={()=>updateField({fieldName})}/><Buttontitle='Cancel'onPress={()=>setEditMode('')}/></View></>)}return (<><Textstyle={styles.headline}>{title}</Text><Viewstyle={{...defaultStyles.row,alignSelf:'stretch'}}><Textstyle={{...defaultStyles.text,flexGrow:1}}>{user[fieldName]||'Not yet defined'}</Text><Buttontitle='Edit'onPress={()=>{setEditValue(value)setEditMode(fieldName)}}/></View></>)}return (<Viewstyle={defaultStyles.container}><Textstyle={styles.headline}>Email</Text><Textstyle={{...defaultStyles.text,alignSelf:'stretch'}}>{user.emails[0].address}</Text>{renderField({title:'First Name',fieldName:'firstName'})}{renderField({title:'Last Name',fieldName:'lastName'})}<Textstyle={styles.headline}>Danger Zone</Text><Viewstyle={{...defaultStyles.dangerBorder,padding:10,marginTop:10,alignSelf:'stretch'}}><Buttontitle='Sign out'color={defaultColors.danger}onPress={()=>signOut({onError})}/><Buttontitle='Delete account'color={defaultColors.danger}onPress={()=>deleteAccount({onError})}/><ErrorMessageerror={error}/></View></View>)}conststyles=StyleSheet.create({headline:{...defaultStyles.bold,alignSelf:'flex-start'}})
6. Simple ToDos with CRUD functionality
This could now be the early end of the workshop, if you intend to go another path and continue from here with your own code. However, for those who want to stay and implement a simple ToDo app, hang tight. It's just another few steps.
6.1 Add Tasks functionality to the backend
The very heart of our app's functionality is always what the backend provides. Therefore we start by defining in the backend what Methods and Publications are actually available.
6.1.1 Add a new Tasks collection
We begin by creating a new Mongo collection, which is in Meteor just one line of code. Add the following code to backend/imports/tasks/TasksCollection.js:
At this point the backend is ready to handle your personal tasklists aka "simple todos".
6.2 Add Tasks functionality to the app
This section involves the communication with the backend and rendering of the tasks. It contains a bit more code than the previous one. However, we will introduce only one new concept, the Meteor.subscribe mechanism. Apart from that, it's all things you already know by now.
6.2.1 Install Checkbox component
Prior to this workshop React Native contained an own Checkbox component, which is now deprecated. While there are multiple UI component systems out there that provide great checkboxes, let's stick with our current environment and use one, that Expo provides:
$ meteor expo install expo-checkbox
6.2.2 Add a client Mongo Collection
One of the core aspects of Meteor is isomorphism - certain concepts implement the same API and behavior on the client as on the server. The same applies to the data handling, which is realized by "Minimongo" a lightweight counterpart to the server's Mongo Collections. It is a core part of Meteor's publish-subscribe system and a source for data reactivity.
In order to make use of these effects, the client collection needs to be named the same way as the collection in the server publication. Add therefore the following code to the new file app/src/tasks/TasksCollection.js:
Since we are creating a Task List it is good to have each Task rendered using an own component. It should display the task's text and checked status and also provide a way to pass up some actions to the parents. For the checkbox we will use the previously installed expo-checkbox.
Create a new file app/src/tasks/Task.js with the following content:
In this case we will bubble-up the handlers to the parent, because of the way we will render the task items in the list.
6.2.4 Create a Task form
We also need a form to create new tasks. It is very similar to what we already did before. Create a new file at app/src/tasks/TaskForm.js and add the following content:
importMeteorfrom'@meteorrn/core'importReact,{useState}from'react'import{View,Button,TextInput}from'react-native'import{defaultColors,defaultStyles}from'../styles/defaultStyles'import{ErrorMessage}from'../components/ErrorMessage'exportconstTaskForm=()=>{const[text,setText]=useState('')const[error,setError]=useState('')consthandleSubmit=e=>{e.preventDefault()if (!text)returnMeteor.call('tasks.insert',{text},(err)=>{if (err){returnsetError(err)}setError(null)})setText('')}return (<View><Viewstyle={defaultStyles.row}><TextInputplaceholder='Type to add new tasks'value={text}placeonChangeText={setText}placeholderTextColor={defaultColors.placeholder}style={defaultStyles.text}/><Buttontitle='Add Task'onPress={handleSubmit}/></View><ErrorMessageerror={error}/></View>)}
6.2.5 Combine them in a Task list component
Now it's time to create a new component at app/src/tasks/TaskList.js and make all the prior created files work together, providing the complete CRUD functionality for our simple todos.
importMeteorfrom'@meteorrn/core'importReact,{useState}from'react'import{Text,View,SafeAreaView,FlatList,Button,ActivityIndicator}from'react-native'import{TasksCollection}from'./TasksCollection'import{Task}from'./Task'import{TaskForm}from'./TaskForm'import{useAccount}from'../hooks/useAccount'import{defaultColors,defaultStyles}from'../styles/defaultStyles'const{useTracker}=MeteorconsttoggleChecked=({_id,checked})=>Meteor.call('tasks.setIsChecked',{_id,checked})constdeleteTask=({_id})=>Meteor.call('tasks.remove',{_id})exportconstTaskList=()=>{const{user}=useAccount()const[hideCompleted,setHideCompleted]=useState(false)// prevent errors when authentication is complete but user is not yet setif (!user){returnnull}consthideCompletedFilter={checked:{$ne:true}}constuserFilter={userId:user._id}constpendingOnlyFilter={...hideCompletedFilter,...userFilter}const{tasks,pendingTasksCount,isLoading}=useTracker(()=>{consttasksData={tasks:[],pendingTasksCount:0}if (!user){returntasksData}consthandler=Meteor.subscribe('tasks.my')if (!handler.ready()){return{...tasksData,isLoading:true}}constfilter=hideCompleted?pendingOnlyFilter:userFilterconsttasks=TasksCollection.find(filter,{sort:{createdAt:-1}}).fetch()constpendingTasksCount=TasksCollection.find(pendingOnlyFilter).count()return{tasks,pendingTasksCount}},[hideCompleted])if (isLoading){return (<Viewstyle={defaultStyles.container}><ActivityIndicator/><Text>Loading tasks...</Text></View>)}constpendingTasksTitle=`${pendingTasksCount?` (${pendingTasksCount})`:''}`return (<SafeAreaViewstyle={{display:'flex',flexDirection:'column',height:'100%'}}><Viewstyle={{display:'flex',flexDirection:'column',flexGrow:1,overflow:'scroll'}}><Viewstyle={defaultStyles.row}><Text>My Tasks {pendingTasksTitle}</Text><TaskForm/></View><Buttontitle={hideCompleted?'Show All':'Hide Completed Tasks'}color={defaultColors.placeholder}onPress={()=>setHideCompleted(!hideCompleted)}/><FlatListdata={tasks}renderItem={({item:task})=>(<Tasktask={task}onCheckboxClick={toggleChecked}onDeleteClick={deleteTask}/>)}keyExtractor={task=>task._id}/></View></SafeAreaView>)}
The Meteor magic happens here within the useTracker hook. There we subscribe to the backend's publication tasks.my and apply optional post-filter to the retrieved data. We do all that by manipulation the live-synced Mongo.Collection. The useTracker will also trigger a new render cycle on data updates and this provides the "automagical" feeling of live updates.
One last step and we are finally done! Make sure, the TaskList is included in the HomeScreen:
In this workshop we created a fully functional mobile app with React Native and Meteor as it's backend. It tackles the biggest challenges of making them both work together - connection, authentication and communication.
There are many topics to continue from here, since this is just a minimal app prototype. Many topics can't be covered in just one workshop, which is why I tried to shrink them down to the "essentials". If there are any uncovered topics that you think are essential, too, then please leave a comment.
If you liked the workshop, please like and subscribe to my channel.
I regularly publish articles here on dev.to about Meteor and JavaScript. If you like what you are reading and want to support me, you can sponsor me on GitHub or send me a tip via PayPal.
Keep up with the latest development on Meteor by visiting their blog and if you are the same into Meteor like I am and want to show it to the world, you should check out the Meteor merch store.