Notes:

This guide assumes you know some React Native, Redux, and Redux Saga .

Github Repo: https://github.com/jefelewis/firebase-atomicity-demo

1. What Is Atomicity?

In computer science, ACID (Atomicity, Consistency, Isolation, Durability) is a set of properties of database transactions intended to guarantee validity even in the event of errors, connection issues, power failure, and etc.

An Atomic Transaction is unique such that all of the operations occur successfully or the entire atomic transaction fails.

2. Why Do We Use Atomicity?

A. Data Accuracy

Since an Atomic Transaction fails completely if all of the operations are successful, Atomicity mitigates the risk of updates to the database from partially completing, which could cause data issues down the line as your application scales.

For example, say you are creating a concert ticket booking app and a user purchases a ticket. If the application has to purchase the ticket but fails to reserve the seat, the ticket purchase shouldn’t go through. Either both the ticket purchase and seat are reserved or nothing should happen at all.

What would happen if you have a bunch of users purchasing tickets, but no seats were reserved because that part of the process failed? A lot of angry charged customers with no concert seats reserved.

B. Data Concurrency

Atomicity is also beneficial in solving data concurrency issues. Data concurrency means that multiple users can read write data at the same time.

For example, all of your users of their team update a shared count. If multiple users are reading and writing data to the same field value, we want that value to be accurate. What if two users update the count at the same time or a user is having connection issues while updating the count? Atomicity solves those issues.

3. When To Use Atomicity and Issues it Prevents

Bank Transactions (Account withdrawal AND account deposit both happen OR nothing occurs)

Counter (User A + User B update a counter at the same time, which could cause data concurrency issues)

Likes (User A + User B update likes at the same time, which could cause data concurrency issues)

Review Count (User A + User B update review count at the same time, which could cause data concurrency issues)

Booking Flight (Receive payment AND reserve seat both happen OR nothing occurs)

Booking Concert Ticket (Receive payment AND reserve seat both happen OR nothing occurs)

4. How Do We Use Atomicity + Firebase Cloud Firestore?

In March of 2019, Firebase introduced FieldValue.increment() to atomically increment and decrement values in Firebase Cloud Firestore.

A. App Overview

Github Repo: https://github.com/jefelewis/firebase-atomicity-demo

This demo application will use Redux + Redux Saga Firebase. We will be using Redux Saga to make asynchronous calls to Firebase Cloud Firestore to increment and decrement the value stored in Firebase Cloud Firestore. Using Firebase’s FieldValue.increment() will allows us to atomically increment and decrement a value in the database.

FieldValue.increment() takes a number as an argument, so since we are just going to increment and decrement by 1, we will pass it the following:

Increment By 1: FieldValue.increment(1)

Decrement By 1: FieldValue.increment(-1)

B. App Screenshot

Counter.js

C. App File Structure

This example will be using 8 files:

firebase.js (Firebase Config + Atomic Increment/Decrement) App.js (React Native App) Counter.js (Counter Screen) store.js (Redux Store) index.js (Redux Root Reducer) counterReducer.js (Redux Counter Reducer) index.js (Redux Root Saga) counterSaga.js (Redux Counter Saga)

D. App Files

firebase.js

// Imports: Dependencies

import firebase from 'firebase';

import '@firebase/firestore';

import ReduxSagaFirebase from 'redux-saga-firebase'; // Imports: Firebase Config

import firebaseConfig from '../config/config.js'; // Firebase: Initialize

const firebaseApp = firebase.initializeApp({

apiKey: firebaseConfig.apiKey,

authDomain: firebaseConfig.authDomain,

databaseURL: firebaseConfig.databaseURL,

projectId: firebaseConfig.projectId,

storageBucket: firebaseConfig.storageBucket,

messagingSenderId: firebaseConfig.messagingSenderId,

}); // Redux Saga Firebase: Initialize

const reduxSagaFirebase = new ReduxSagaFirebase(firebaseApp); // Increment/Decrement

export const atomicIncrement = firebase.firestore.FieldValue.increment(1);

export const atomicDecrement = firebase.firestore.FieldValue.increment(-1); // Exports

export default reduxSagaFirebase;

App.js

// Imports: Dependencies

import React from 'react';

import { Provider } from 'react-redux'; // Imports: Screens

import Counter from './screens/Counter'; // Imports: Redux Store

import { store } from './redux/store'; // React Native App

export default function App() {

return (

// Redux: Global Store

<Provider store={store}>

<Counter />

</Provider>

);

}

Counter.js

// Imports: Dependencies

import React, { Component } from 'react';

import { Button, Dimensions, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';

import { connect } from 'react-redux'; // Imports: Redux Actions

import { increaseCounter, decreaseCounter } from '../redux/actions/counterActions'; // Screen Dimensions

const { height, width } = Dimensions.get('window'); // Screen: Counter

class Counter extends React.Component {

render() {

return (

<SafeAreaView style={styles.container}>

<Text style={styles.counterTitle}>Counter</Text> <View style={styles.counterContainer}>

<TouchableOpacity onPress={this.props.reduxIncreaseCounter}>

<Text style={styles.buttonText}>+</Text

</TouchableOpacity> <TouchableOpacity onPress={this.props.reduxDecreaseCounter}>

<Text style={styles.buttonText}>-</Text

</TouchableOpacity>

</View>

</SafeAreaView>

)

}

} // Styles

const styles = StyleSheet.create({

container: {

flex: 1,

justifyContent: 'center',

alignItems: 'center',

},

counterContainer: {

display: 'flex',

flexDirection: 'row',

justifyContent: 'center',

alignItems: 'center',

},

counterTitle: {

fontFamily: 'System',

fontSize: 32,

fontWeight: '700',

color: '#000',

},

buttonText: {

fontFamily: 'System',

fontSize: 50,

fontWeight: '300',

color: '#007AFF',

marginLeft: 40,

marginRight: 40,

},

}); // Map Dispatch To Props (Dispatch Actions To Reducers. Reducers Then Modify The Data And Assign It To Your Props)

const mapDispatchToProps = (dispatch) => {

return {

// Increase Counter

reduxIncreaseCounter: () => dispatch(increaseCounter()),

// Decrease Counter

reduxDecreaseCounter: () => dispatch(decreaseCounter()),

};

}; // Exports

export default connect(null, mapDispatchToProps)(Counter);

store.js

// Imports: Dependencies

import { createStore, applyMiddleware } from 'redux';

import { createLogger } from 'redux-logger';

import createSagaMiddleware from 'redux-saga'; // Imports: Redux Root Reducer

import rootReducer from '../reducers/index'; // Imports: Redux Root Saga

import { rootSaga } from '../sagas/index'; // Middleware: Redux Saga

const sagaMiddleware = createSagaMiddleware(); // Redux: Store

const store = createStore(

rootReducer,

applyMiddleware(

sagaMiddleware,

createLogger(),

),

); // Middleware: Redux Saga

sagaMiddleware.run(rootSaga); // Exports

export {

store,

}

index.js (Root Reducer)

// Imports: Dependencies

import { combineReducers } from 'redux'; // Imports: Reducers

import counterReducer from './counterReducer'; // Redux: Root Reducer

const rootReducer = combineReducers({

counter: counterReducer,

}); // Exports

export default rootReducer;

counterReducer.js

// Initial state

const initialState = {

loading: false,

}; // Document Reducer

export default function documentReducer (state = initialState, action) {

switch (action.type) {

// Increase Counter

case 'INCREASE_COUNTER':

return {

...state,

loading: true,

} // Decrease Counter

case 'DECREASE_COUNTER':

return {

...state,

loading: false,

} // Default

default:

return state;

}

}

index.js (Root Saga)

// Imports: Dependencies

import { all, fork} from 'redux-saga/effects'; // Imports: Redux Sagas

import { watchIncreaseCounter, watchDecreaseCounter } from './counterSaga'; // Redux Saga: Root Saga

export function* rootSaga () {

yield all([

fork(watchIncreaseCounter),

fork(watchDecreaseCounter),

]);

};

counterSaga.js

// Imports: Dependencies

import { call, takeEvery } from 'redux-saga/effects'; // Imports: Firebase + Atomic Incrementers

import reduxSagaFirebase from '../../firebase/firebase';

import { atomicIncrement, atomicDecrement } from '../../firebase/firebase'; // Redux Saga: Increase Counter

function* increaseCounter() {

try {

// Update Data: Increment Counter By 1

yield call(reduxSagaFirebase.firestore.updateDocument, 'counter/counter', {

counter: atomicIncrement,

});

}

catch (error) {

console.log(error);

}

}; // Redux Saga: Decrease Counter

function* decreaseCounter() {

try {

// Update Data: Decrement Counter By 1

yield call(reduxSagaFirebase.firestore.updateDocument, 'counter/counter', {

counter: atomicDecrement,

});

}

catch (error) {

console.log(error);

}

}; // Watcher: Increase Counter

export function* watchIncreaseCounter() {

// Take Every Action

yield takeEvery('INCREASE_COUNTER', increaseCounter);

}; // Watcher: Decrease Counter

export function* watchDecreaseCounter() {

// Take Every Action

yield takeEvery('DECREASE_COUNTER', decreaseCounter);

};

Conclusion

And that’s it! The value is now atomically incremented/decremented in Firebase Cloud Firestore.

No one’s perfect. If you’ve found any errors, want to suggest enhancements, or expand on a topic, please feel free to send me a message. I will be sure to include any enhancements or correct any issues.