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.
Photo used by Rami Al-zayat on Unsplash
Quick link to the final workshop code:
jankapunkt / meteor-react-native-starter
Boilerplate with auth, backend, db and many more! Clone, install, start coding!
Meteor React Native Starter
This is the final code repo for our workshop "Meteor and React Native" @ Meteor Impact 2022 After post-editing it resulted in a complete starter repo. 🤩
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…
Background
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:
Meteor is an ultra-simple environment for building modern web applications.
📚 Create your applications using modern JavaScript
Benefit from the latest technology updates to rapidly prototype and develop your applications.
✨ Integrate technologies you already use
Use popular frameworks and tools right out-of-the-box. Focus on building features instead of configuring disparate components yourself.
💻 Build apps for any device
Use the same code whether you’re developing for web, iOS, Android, or desktop for a seamless update experience for your users.
🔥 Getting Started
How about trying a tutorial to get started with your favorite technology?
Next, read the documentation and get some examples.
🚀 Quick Start
On your platform, use this line:
> npm install -g meteor
🚀 To create a project:
> meteor create my-app
☄️ Run it:
cd my-app
meteor
🧱 Developer Resources
Building an application with Meteor?
- Deploy…
facebook / react-native
A framework for building native applications using React
Learn once, write anywhere:
Build mobile apps with React
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.
Contents
Architecture 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:
Expo Go for iOS
Expo Go for Android
If one of the links won't work, you can also get them from the Expo website.
❗❗❗ 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.
The Expo devs provide a comprehensive guide on installing the Android emulator or iOS simulator for you.
1.2 Install Meteor
You can either install Meteor on various OS' and platforms via NPM (in case you have Node >= 14 and NPM already installed)
$ npm install -g meteor
or using their provided shell script on Linux or MacOS:
$ curl https://install.meteor.com/ | sh
If you want to install it on a Mac with M1, a docker container or Windows, please read the details from the official installation guide.
❗❗❗ Important
In this workshop you have to usemeteor npm
in your terminals from now on, instead of justnpm
. 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 callmeteor expo
instead of justexpo
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.
1.4.a Clone from GitHub via SSH
$ git clone git@github.com:myusername/mrntodos.git
1.4.b Clone from GitHub via HTTPS
$ git clone https://github.com/myusername/mrntodos.git
1.4.c Create local folder
$ mkdir -p mrntodos
2. Create and set up the Meteor backend
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
:
...
"start": "meteor --port=8000 --settings=setting.json"
Now start the Meteor app via
$ meteor npm start
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 also integrate third-party authentication with Facebook, Twitter, Apple, GitHub or your custom OAuth2 provider. However, in this workshop we add and configure the most basic authentication method, using accounts-passwords
:
$ meteor add accounts-password
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.
const defaultFieldSelector = {
_id: 1,
username: 1,
emails: 1,
firstName: 1,
lastName: 1
}
// merge our config from settings.json with fixed code
// and pass them to Accounts.config
Accounts.config({
...Meteor.settings.accounts.config,
defaultFieldSelector
})
Now add the following parts to your settings.json
file:
{
"accounts": {
"config": {
"forbidClientAccountCreation": true,
"ambiguousErrorMessages": true,
"sendVerificationEmail": true,
"loginExpirationInDays": null
}
}
}
All parameters of the configuration are documented, in case you want to further investigate on them.
📝 Notice
Theaccounts-base
package is automatically added whenaccounts-password
is added. When importingAccounts
in your code you will useimport { 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'
export const registerNewUser = function (options) {
check(options, Match.ObjectIncluding({
email: String,
password: String,
firstName: String,
lastName: String,
loginImmediately: Match.Maybe(Boolean)
}))
const { email, password, firstName, lastName, loginImmediately } = options
if (Accounts.findUserByEmail(email)) {
throw new Meteor.Error('permissionDenied', 'userExists', { email })
}
const userId = Accounts.createUser({ email, password })
// we add the firstName and lastName as toplevel fields
// which allows for better handling in publications
Meteor.users.update(userId, { $set: { firstName, lastName } })
// let them verify their new account, so
// they can use the full app functionality
Accounts.sendVerificationEmail(userId, email)
if (loginImmediately) {
// signature: { id, token, tokenExpires }
return Accounts._loginUser(this, userId)
}
// keep the same return signature here to let clients
// better handle the response
return { 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:
import { Accounts } from 'meteor/accounts-base'
import { Meteor } from 'meteor/meteor'
import { registerNewUser } from '../../accounts/methods'
Accounts.config(Meteor.settings.accounts.config)
Meteor.methods({ registerNewUser })
2.6 Add more endpoints to manage accounts
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:
// ... registerNewUser
export const updateUserProfile = 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 Method
const { userId } = this
if (!userId) {
throw new Meteor.Error('permissionDenied', 'notAuthenticated', { userId })
}
const updateDoc = { $set: {} }
if (firstName) {
updateDoc.$set.firstName = firstName
}
if (lastName) {
updateDoc.$set.lastName = lastName
}
return !!Meteor.users.update(userId, updateDoc)
}
export const deleteAccount = function () {
const { userId } = this
if (!userId) {
throw new Meteor.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 code
Meteor.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:
$ cd app
$ meteor expo install @meteorrn/core @react-navigation/native @react-navigation/native-stack @react-navigation/stack expo-secure-store expo-status-bar react-native-screens react-native-safe-area-context react-native-gesture-handler
❗❗❗ Important
We need to usemeteor 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):
{
"backend": {
"url": "ws://xxx.xxx.xxx.xxx:8000/websocket"
}
}
At this point, let's run the app the first time to let Expo generate a few files via:
$ meteor npm start
Use your mobile device and scan the QR code with the Expo Go app. You should see now that bundling is in progress:
Once this is done your device should display the default content from app/App.js
afterwards:
After running, check the newly created app/.expo/settings.json
file and make sure it looks like the following:
{
"hostType": "lan",
"lanType": "ip",
"dev": true,
"minify": false,
"urlRandomness": "mc-y7b",
"https": false,
"scheme": null,
"devClient": false
}
3.3 Set Hermes as JavaScript engine
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
:
"expo": {
"name": "app",
"slug": "app",
"version": "1.0.0",
"assetBundlePatterns": [
"**/*"
],
"jsEngine": "hermes"
}
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'
import Meteor from '@meteorrn/core'
import * as SecureStore from 'expo-secure-store'
import config from '../../config.json'
// get detailed info about internals
Meteor.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
}
})
export const useConnection = () => {
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 handled
useEffect(() => {
const onError = (e) => setConnectionError(e)
Meteor.ddp.on('error', onError)
const onConnected = () => 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 server
const onDisconnected = () => {
Meteor.ddp.autoConnect = true
if (connected !== false) {
setConnected(false)
}
Meteor.reconnect()
}
Meteor.ddp.on('disconnected', onDisconnected)
// remove all of these listeners on unmount
return () => {
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:
import React from 'react'
import { MainNavigator } from './src/screens/MainNavigator'
import { StyleSheet, View, Text, ActivityIndicator } from 'react-native'
import { useConnection } from './src/hooks/useConnection'
export default function App () {
const { connected, connectionError } = useConnection()
// use splashscreen here, if you like
if (!connected) {
return (
<View style={styles.container}>
<ActivityIndicator />
<Text>Connecting to our servers...</Text>
</View>
)
}
// use alert or other things here, if you like
if (connectionError) {
return (
<View style={styles.container}>
<Text>Error, while connecting to our servers!</Text>
<Text>{connectionError.message}</Text>
</View>
)
}
return (
<View style={styles.container}>
<Text>We are connected!</Text>
</View>
)
}
const styles = 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:
import { StyleSheet } from 'react-native'
export const defaultColors = {
placeholder: '#8a8a8a',
danger: '#981111',
white: '#eee',
black: '#1a1a1a',
primary: '#0B52AF'
}
export const defaultStyles = StyleSheet.create({
text: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10,
alignSelf: 'stretch',
color: defaultColors.black,
backgroundColor: defaultColors.white
},
panel: {
margin: 20
},
container: {
margin: 20,
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
danger: {
color: defaultColors.danger
},
dangerBorder: {
borderWidth: 1,
borderColor: defaultColors.danger
},
bold: {
fontWeight: 'bold'
},
row: {
flexDirection: 'row',
alignItems: 'center'
},
flex1: {
flex: 1
}
})
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:
import React from 'react'
import { Text, View } from 'react-native'
import { defaultStyles } from '../styles/defaultStyles'
export const ErrorMessage = ({ error, message }) => {
if (!error && !message) { return null }
return (
<View style={defaultStyles.container}>
<Text style={defaultStyles.danger}>{message || error.message}</Text>
</View>
)
}
With both of these abstractions we can, again, update app/App.js
and replace
// use alert or other things here, if you like
if (connectionError) {
return (
<View style={styles.container}>
<Text>Error, while connecting to our servers!</Text>
<Text>{connectionError.message}</Text>
</View>
)
}
with
// use alert or other things here, if you like
if (connectionError) {
return (<ErrorMessage error={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
:
import { createContext } from 'react'
export const AuthContext = createContext()
5.2.2 Create a useAuth
hook
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'
import Meteor from '@meteorrn/core'
const initialState = {
isLoading: true,
isSignout: false,
userToken: null
}
const reducer = (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
}
}
}
const Data = Meteor.getData()
export const useAuth = () => {
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 workflow
useEffect(() => {
const handleOnLogin = () => dispatch({ type: 'RESTORE_TOKEN', token: Meteor.getAuthToken() })
Data.on('onLogin', handleOnLogin)
return () => Data.off('onLogin', handleOnLogin)
}, [])
const authContext = 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.'
}
return onError(err)
}
const token = Meteor.getAuthToken()
const type = 'SIGN_IN'
dispatch({ type, token })
})
},
signOut: ({ onError }) => {
Meteor.logout(err => {
if (err) {
return onError(err)
}
dispatch({ type: 'SIGN_OUT' })
})
},
signUp: ({ email, password, firstName, lastName, onError }) => {
const signupArgs = { email, password, firstName, lastName, loginImmediately: true }
Meteor.call('registerNewUser', signupArgs, (err, credentials) => {
if (err) {
return onError(err)
}
// this sets the { id, token } values internally to make sure
// our calls to Meteor endpoints will be authenticated
Meteor._handleLoginCallback(err, credentials)
// from here this is the same routine as in signIn
const token = Meteor.getAuthToken()
const type = 'SIGN_IN'
dispatch({ type, token })
})
},
deleteAccount: ({ onError }) => {
Meteor.call('deleteAccount', (err) => {
if (err) {
return onError(err)
}
// removes all auth-based data from client
// as if we would call signOut
Meteor.handleLogout()
dispatch({ type: 'SIGN_OUT' })
})
}
}), [])
return { state, authContext }
}
📝 Notice, that this file requires some explanation, of course.
- the
reducer
handles the internal state, manipulated viadispatch
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 theuseEffect
hook to implement our "auto-login" feature - the
authContext
is retrieved in screens with the previously createdAuthContext
anduseContext
as you will see in the upcoming sections - the several functions
signIn
,signOut
,signUp
anddeleteAccounts
leverage mostly what the@meteorrn/core
package uses internally duringMeteor.loginWithPassword
orMeteor.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:
import React from 'react'
import { View, Text } from 'react-native'
import { defaultStyles } from '../styles/defaultStyles'
export const HomeScreen = () => {
return (
<View style={defaultStyles.container}>
<Text>Welcome home!</Text>
</View>
)
}
5.2.2 LoginScreen
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:
import React, { useState, useContext } from 'react'
import { View, Text, TextInput, Button } from 'react-native'
import { AuthContext } from '../contexts/AuthContext'
import { defaultColors, defaultStyles } from '../styles/defaultStyles'
import { ErrorMessage } from '../components/ErrorMessage'
export const LoginScreen = ({ navigation }) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const { signIn } = useContext(AuthContext)
// handlers
const onError = err => setError(err)
const onSignIn = () => signIn({ email, password, onError })
const onSignUp = () => navigation.navigate('SignUp')
// render login form
return (
<View style={defaultStyles.container}>
<TextInput
placeholder='Your Email'
placeholderTextColor={defaultColors.placeholder}
style={defaultStyles.text}
value={email}
onChangeText={setEmail}
/>
<TextInput
placeholder='Password'
placeholderTextColor={defaultColors.placeholder}
style={defaultStyles.text}
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<ErrorMessage error={error} />
<Button title='Sign in' color={defaultColors.primary} onPress={onSignIn} />
<View style={defaultStyles.panel}>
<Text>or</Text>
</View>
<Button title='Sign up' onPress={onSignUp} color={defaultColors.placeholder} />
</View>
)
}
📝 Notice two things here.
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:
import React, { 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'
export const RegistrationScreen = () => {
const [email, setEmail] = useState()
const [firstName, setFirstName] = useState()
const [lastName, setLastName] = useState()
const [password, setPassword] = useState()
const [error, setError] = useState()
const { signUp } = useContext(AuthContext)
const onError = err => setError(err)
const onSignUp = () => signUp({ email, password, firstName, lastName, onError })
return (
<View style={defaultStyles.container}>
<TextInput
placeholder='Your Email'
placeholderTextColor={defaultColors.placeholder}
style={defaultStyles.text}
value={email}
onChangeText={setEmail}
/>
<TextInput
placeholder='Your password'
placeholderTextColor={defaultColors.placeholder}
style={defaultStyles.text}
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TextInput
placeholder='Your first name (optional)'
placeholderTextColor={defaultColors.placeholder}
style={defaultStyles.text}
value={firstName}
onChangeText={setFirstName}
/>
<TextInput
placeholder='Your last name (optional)'
placeholderTextColor={defaultColors.placeholder}
style={defaultStyles.text}
value={lastName}
onChangeText={setLastName}
/>
<ErrorMessage error={error} />
<Button title='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.
import { defaultColors, defaultStyles } from '../styles/defaultStyles'
import { Button, Text, View } from 'react-native'
import { useState } from 'react'
import { ErrorMessage } from '../components/ErrorMessage'
export const ProfileScreen = () => {
const [error, setError] = useState(null)
const handleSignOut = () => console.log('sign out')
const handleDelete = () => console.log('delete account')
return (
<View style={defaultStyles.container}>
<View style={{ ...defaultStyles.dangerBorder, padding: 10, marginTop: 10, alignSelf: 'stretch' }}>
<Text style={defaultStyles.bold}>Danger Zone</Text>
<Button title='Sign out' color={defaultColors.danger} onPress={handleSignOut} />
<Button title='Delete account' color={defaultColors.danger} onPress={handleDelete} />
<ErrorMessage error={error} />
</View>
</View>
)
}
5.3 Implement the navigation
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:
import React from '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'
const Stack = createNativeStackNavigator()
export const MainNavigator = () => {
const { state, authContext } = useAuth()
const { userToken } = state
const renderScreens = () => {
if (userToken) {
// only authenticated users can visit these screens
const headerRight = () => (<NavigateButton title='My profile' route='Profile' />)
return (
<>
<Stack.Screen name='Home' component={HomeScreen} options={{ title: 'Welcome home', headerRight }} />
<Stack.Screen name='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.Screen
name='SignIn'
component={LoginScreen}
options={{ title: 'Sign in to awesome-app' }}
/>
<Stack.Screen
name='SignUp'
component={RegistrationScreen}
options={{ title: 'Register to awesome-app' }}
/>
</>
)
}
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
<Stack.Navigator screenOptions={{ 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 thereducer
inuseAuth
, while theauthContext
is injected into our components tree via<AuthContext.Provider value={authContext}>
; without this it would not be accessible within the screens usinguseContext
- 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:
import { Button } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { defaultColors } from '../styles/defaultStyles'
export const NavigateButton = ({ title, route }) => {
const navigation = useNavigation()
return (
<Button
title={title}
color={defaultColors.primary}
onPress={() => navigation.navigate(route)}
/>
)
}
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:
import React from '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'
export default function App () {
const { connected, connectionError } = useConnection()
// use splashscreen here, if you like
if (!connected) {
return (
<View style={defaultStyles.container}>
<ActivityIndicator />
<Text>Connecting to our servers...</Text>
</View>
)
}
// use alert or other things here, if you like
if (connectionError) {
return (<ErrorMessage error={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:
import Meteor from '@meteorrn/core'
import { useMemo, useState } from 'react'
const { useTracker } = Meteor
export const useAccount = () => {
const [user, setUser] = useState(Meteor.user())
useTracker(() => {
const reactiveUser = Meteor.user()
if (reactiveUser !== user) {
setUser(reactiveUser)
}
})
const api = useMemo(() => ({
updateProfile: ({ options, onError, onSuccess }) => {
Meteor.call('updateUserProfile', options, (err) => {
return err
? onError(err)
: onSuccess()
})
}
}), [])
return { user, ...api }
}
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
forAccounts.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'
export const ProfileScreen = () => {
const [editMode, setEditMode] = useState('')
const [editValue, setEditValue] = useState('')
const [error, setError] = useState(null)
const { signOut, deleteAccount } = useContext(AuthContext)
const { user, updateProfile } = useAccount()
const onError = err => setError(err)
if (!user) {
return null // 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
*/
const updateField = ({ fieldName }) => {
const options = {}
options[fieldName] = editValue
const onSuccess = () => {
setError(null)
setEditValue('')
setEditMode('')
}
updateProfile({ options, onError, onSuccess })
}
const renderField = ({ title, fieldName }) => {
const value = user[fieldName] || ''
if (editMode === fieldName) {
return (
<>
<Text style={styles.headline}>{title}</Text>
<View style={defaultStyles.row}>
<TextInput
placeholder={title}
autoFocus
placeholderTextColor={defaultColors.placeholder}
style={{ ...defaultStyles.text, ...defaultStyles.flex1 }}
value={editValue}
onChangeText={setEditValue}
/>
<ErrorMessage error={error} />
<Button title='Update' onPress={() => updateField({ fieldName })} />
<Button title='Cancel' onPress={() => setEditMode('')} />
</View>
</>
)
}
return (
<>
<Text style={styles.headline}>{title}</Text>
<View style={{ ...defaultStyles.row, alignSelf: 'stretch' }}>
<Text style={{ ...defaultStyles.text, flexGrow: 1 }}>{user[fieldName] || 'Not yet defined'}</Text>
<Button
title='Edit' onPress={() => {
setEditValue(value)
setEditMode(fieldName)
}}
/>
</View>
</>
)
}
return (
<View style={defaultStyles.container}>
<Text style={styles.headline}>Email</Text>
<Text style={{ ...defaultStyles.text, alignSelf: 'stretch' }}>{user.emails[0].address}</Text>
{renderField({ title: 'First Name', fieldName: 'firstName' })}
{renderField({ title: 'Last Name', fieldName: 'lastName' })}
<Text style={styles.headline}>Danger Zone</Text>
<View style={{ ...defaultStyles.dangerBorder, padding: 10, marginTop: 10, alignSelf: 'stretch' }}>
<Button title='Sign out' color={defaultColors.danger} onPress={() => signOut({ onError })} />
<Button title='Delete account' color={defaultColors.danger} onPress={() => deleteAccount({ onError })} />
<ErrorMessage error={error} />
</View>
</View>
)
}
const styles = 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
:
import { Mongo } from 'meteor/mongo'
export const TasksCollection = new Mongo.Collection('tasks')
6.1.2 Add Task Method endpoints and Publications
Client's should be able to create new tasks, update their checked
state or remove them. Data fetching should be realized by publish/subscribe.
A simple implementation could look the the following backend/imports/tasks/methods.js
file:
import { Meteor } from 'meteor/meteor'
import { TasksCollection } from './TasksCollection'
export const getMyTasks = function () {
const userId = this.userId
checkUser(userId)
return TasksCollection.find({ userId })
}
export const insertTask = function ({ text }) {
const userId = this.userId
checkUser(userId)
const checked = false
const createdAt = new Date()
return TasksCollection.insert({ text, userId, checked, createdAt })
}
export const checkTask = function ({ _id, checked }) {
const userId = this.userId
checkUser(userId)
return TasksCollection.update({ _id, userId }, { $set: { checked } })
}
export const removeTask = function ({ _id }) {
const userId = this.userId
checkUser(userId)
return TasksCollection.remove({ _id, userId })
}
const checkUser = userId => {
if (!userId) {
throw new Meteor.Error('permissionDenied', 'notSignedIn', { userId })
}
}
6.1.3 Add Tasks to the startup
First, register the Methods and Publication in a new file, backend/imports/startup/server/tasks.js
:
import { Meteor } from 'meteor/meteor'
import { checkTask, insertTask, getMyTasks, removeTask } from '../../tasks/methods'
Meteor.methods({
'tasks.insert': insertTask,
'tasks.setIsChecked': checkTask,
'tasks.remove': removeTask
})
Meteor.publish('tasks.my', getMyTasks)
Finally, make sure this file is imported in backend/server/main.js
:
import '../imports/startup/server/accounts'
import '../imports/startup/server/tasks'
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
:
import { Mongo } from '@meteorrn/core'
export const TasksCollection = new Mongo.Collection('tasks')
6.2.3 Create a Task component
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:
import React from 'react'
import { View, Text, Button, StyleSheet } from 'react-native'
import Checkbox from 'expo-checkbox'
import { defaultColors } from '../styles/defaultStyles'
export const Task = ({ task, onCheckboxClick, onDeleteClick }) => {
const handleCheck = (checked) => onCheckboxClick({ _id: task._id, checked })
return (
<View style={{ display: 'flex', flexDirection: 'row', width: '100%', padding: 5, justifyContent: 'space-between' }}>
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Checkbox
value={task.checked}
onValueChange={handleCheck}
style={{ padding: 12 }}
color={task.checked && defaultColors.placeholder}
readOnly
/>
<Text style={task.checked ? styles.checked : styles.unchecked}>{task.text}</Text>
</View>
<Button title='X' onPress={() => onDeleteClick(task)} style={{ justifySelf: 'flex-end' }} />
</View>
)
}
const styles = StyleSheet.create({
checked: {
color: defaultColors.placeholder,
marginLeft: 10
},
unchecked: {
marginLeft: 10
}
})
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:
import Meteor from '@meteorrn/core'
import React, { useState } from 'react'
import { View, Button, TextInput } from 'react-native'
import { defaultColors, defaultStyles } from '../styles/defaultStyles'
import { ErrorMessage } from '../components/ErrorMessage'
export const TaskForm = () => {
const [text, setText] = useState('')
const [error, setError] = useState('')
const handleSubmit = e => {
e.preventDefault()
if (!text) return
Meteor.call('tasks.insert', { text }, (err) => {
if (err) {
return setError(err)
}
setError(null)
})
setText('')
}
return (
<View>
<View style={defaultStyles.row}>
<TextInput
placeholder='Type to add new tasks'
value={text}
place
onChangeText={setText}
placeholderTextColor={defaultColors.placeholder}
style={defaultStyles.text}
/>
<Button title='Add Task' onPress={handleSubmit} />
</View>
<ErrorMessage error={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.
import Meteor from '@meteorrn/core'
import React, { 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 } = Meteor
const toggleChecked = ({ _id, checked }) => Meteor.call('tasks.setIsChecked', { _id, checked })
const deleteTask = ({ _id }) => Meteor.call('tasks.remove', { _id })
export const TaskList = () => {
const { user } = useAccount()
const [hideCompleted, setHideCompleted] = useState(false)
// prevent errors when authentication is complete but user is not yet set
if (!user) { return null }
const hideCompletedFilter = { checked: { $ne: true } }
const userFilter = { userId: user._id }
const pendingOnlyFilter = { ...hideCompletedFilter, ...userFilter }
const { tasks, pendingTasksCount, isLoading } = useTracker(() => {
const tasksData = { tasks: [], pendingTasksCount: 0 }
if (!user) {
return tasksData
}
const handler = Meteor.subscribe('tasks.my')
if (!handler.ready()) {
return { ...tasksData, isLoading: true }
}
const filter = hideCompleted
? pendingOnlyFilter
: userFilter
const tasks = TasksCollection.find(filter, { sort: { createdAt: -1 } }).fetch()
const pendingTasksCount = TasksCollection.find(pendingOnlyFilter).count()
return { tasks, pendingTasksCount }
}, [hideCompleted])
if (isLoading) {
return (
<View style={defaultStyles.container}>
<ActivityIndicator />
<Text>Loading tasks...</Text>
</View>
)
}
const pendingTasksTitle = `${pendingTasksCount ? ` (${pendingTasksCount})` : ''}`
return (
<SafeAreaView style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<View style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'scroll' }}>
<View style={defaultStyles.row}>
<Text>My Tasks {pendingTasksTitle}</Text>
<TaskForm />
</View>
<Button
title={hideCompleted ? 'Show All' : 'Hide Completed Tasks'}
color={defaultColors.placeholder}
onPress={() => setHideCompleted(!hideCompleted)}
/>
<FlatList
data={tasks}
renderItem={({ item: task }) => (
<Task
task={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
:
import React from 'react'
import { View } from 'react-native'
import { defaultStyles } from '../styles/defaultStyles'
import { TaskList } from '../tasks/TaskList'
export const HomeScreen = () => {
return (
<View style={defaultStyles.container}>
<TaskList />
</View>
)
}
6.3 Finally celebrate your first app 🥳
7. Summary and outlook
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.
Further Resources and links
Meteor
- Website
- Hosting
- Guide
- API Docs
- YouTube
- Slack
- Podcast
- Medium (Blog)
- Forums
- Github
React Native
About me
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.
You can also find (and contact) me on GitHub, Twitter and LinkedIn.
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.
Top comments (10)
Great to read some recent and well written code with meteor+rn 🥳
Any plans on a follow up covering offline first (maybe also with non reactive data fetching via methods)? Asking for a friend 😉
We (the research group I am working at) are still in the phase of reviewing options for offline and may soon begin our work. Once we have something stable I will write about it. Realm DB is currently my favorite but I have not reviewed all options. If you have suggestions please let us know, asking for my colleagues 😅
I'm not really even at the point of thinking about storage system; using Asyncstorage or even minimongo right now, just to do some testing, in combination with redux.
What is interesting to me is the ux of what happens when meteor reconnects and refreshes subscriptions. Without any further caching, on a reconnect (after an app resume for instance), all documents in a collection get removed and then added again, making them disappear for a few secs. For many of my use cases it would be nice to display stale data until the subscription is ready and then get back to live data. In some cases I even consider switching to methods instead of subscriptions.
Edit: a loading state is not a good option for most of my views, since the whole view is taken up by document views and the user would get to see any data while eg. a skeleton or activity indicator is displayed...
I'd really like to try out your implementation of the useConnection hook. However, the ddp events don't get fired. I have netinfo installed and I'm on expo sdk 47. Any idea where to look at?
Thanks for the info. There also seems to be an issue with the DDP connected event on GitHub. As a comparison you can try
And see what it returns
Yes, I‘m doing it like that already and it’s working; still trying to accomplish displaying stale data from Asyncstorage until the subscriptions are ready, so I thought maybe more control over reconnection behavior could be nice ;)
Interesting. Do you also face the issue of no connection to the server when the app has been in the background for a while?
I'm avoiding this problem by calling Meteor.connect in a useEffect hook with no dependencies in my App.js when I prepare the app, hide splash screen etc.
That sounds great! I have a starter repo on GtiHub and it would really love to see, whether I can improve on this issue with your approach. Would you mind taking a look at it? Maybe we can collaborate on this?
I'll give it a shot, got a fork and will play around a little bit. Process of getting it setup up was flawless (which is, tbh, unfortunately non-standard ;) )