Getting Started With React, Redux, and Firestore.

Olalekan Odukoya
15 min readJan 1, 2019

--

In this article, I’ll be showing you how to create a simple Todo application called — Goalz using React, Redux, and Firebase. The gif below shows what we’ll be making. However, before we start let’s have a little introduction to what React, Redux, and Firebase actually mean.

React is a javascript library created by the Facebook team for creating user interface components. According to the official React website, React was explained to be a declarative, efficient, and flexible JavaScript library for building user interfaces. It lets you compose complex UIs from small and isolated pieces of code called “components”. So, when creating an application with React, you think “component”.

Redux: When creating a bigger application, managing data within your application using props to pass around data can become a little bit difficult to handle because you might need to pass data from one component to another (related components or no), or mutate data to change the value of the data. The way to fix this is to deploy an agent into your application that helps you manage your data. This agent serves as the central store in the application in which all of the components can easily have access to and by making sure that whatever data in the store is being mutated or changed an action is being dispatched. The act of you separating the data from the main application, and making sure data are not changed or mutated without “permission” can be termed as you maintaining a “single source of truth”. The official website of Redux explains it to be — a predictable state container for JavaScript apps.

Cloud Firestore: This is a scalable NoSQL cloud-based storage solution for storing data created by Google. According to its official website, Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud Platform. Data in a Firestore database are stored in collections (a collection of documents) and documents.

Let’s Get Started!

use the code below to create a React app and start the development server.

npx create-react-app goalz
cd goalz
npm start

We’ll need to also install the following dependencies: Redux — for state management, react-router-dom — for routing.

npm i redux react-router-dom

Replace the whole of your index.html file with the HTML below. The new file contains a cdn link to google fonts, material-icons, and materialize css.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<!-- Compiled and minified CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<link href="https://fonts.googleapis.com/css?family=Sedgwick+Ave" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<!-- <script src="https://openlibrary.org/api/books?bibkeys=ISBN:0451526538&callback=mycallback"></script> -->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>

</body>
</html>

After completing the above process, go the to src folder, then create a component directory. In this directory, create a Navbar.js file, then paste the code below into the file.

import React, { Component } from 'react';class Navbar extends Component {
render(){
return (
<div>
<nav>
<div className="nav-wrapper">
<div className="container">
<a className="brand-logo center">Goalz</a>
</div>
</div>
</nav>
</div>
)
}
}
export default Navbar

Create a Goals, Home, and Todo .js files and paste the code below into their respective files.

import React from 'react'
import { Link } from 'react-router-dom'
const Goals = ({ goals }) => {let goalgoals.length === 0 ?
goal = <h5>You haven't set a goal</h5> :
goal = goals.map( g => {
return (
<li className="collection-item" key={ g.id } >
{ g.goal }
<Link className='secondary-content' to='/'>
<i className='material-icons'>edit</i>
</Link>
<Link className='secondary-content' to='/'>
<i className='material-icons'>delete</i>
</Link>
</li>
)
})
return (
<div>
<ul className="collection">
{ goal }
</ul>
</div>
)
}
export default Goals

Home.js

import React from 'react'import Todo from './Todo'
import Goals from './Goals'
class Home extends React.Component {state = {
goals: [
{id:1, goal:"Become better at testing apps"},
{id:2, goal:"Become a google dev expert"}
]
}
addAGoal = goals => {
goals.id = Math.random()*100000000000;
let goal = [...this.state.goals, goals]
this.setState({
goals: goal
})
}
render(){
return(
<div className="container">
<Todo addAGoal={ this.addAGoal } />
<Goals goals={ this.state.goals } />
</div>
)
}
}
export default Home

Todo.js

import React from 'react'class Todo extends React.Component {state = {
goal: ''
}
getGoal = e => {
e.preventDefault()
if(this.state.goal !== ''){
this.props.addAGoal(this.state)
this.setState({ goal: '' })
}
}
render(){
return(
<div>
<div className="row">
<form className="col s12" style={{ marginTop: '70px' }} onSubmit={ this.getGoal }>
<input type="text"
onChange={ e => { this.setState({ goal: e.target.value })} }
value={ this.state.goal }
required />
<div className='center'>
<button className='btn btn-large blue' onClick={ this.getGoal }>Add Goal</button>
</div>
</form>
</div>
</div>
)
}
}
export default Todo

also, change the code in your App.js to the code below

import React, { Component } from 'react';
import './index.css'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import Navbar from './components/NavBar'
import Home from './components/Home'
class App extends Component {
render() {
return (
<BrowserRouter>
<div>
<Navbar />
<Switch>
<Route path='/' component={ Home } />
</Switch>
</div>
</BrowserRouter>
);
}
}
export default App;

Add the following stylings to your index.css file

.secondary-content{
color: #2196f3 !important;
}
.material-icons{
margin: 0 10px !important;
}
.delete{
color: red;
}
.edit{
color: green;
}

If you correctly followed the steps, your app should look very similar to the pic below.

A pic showing how my directories are arranged. Your’s should look similar.

Explanation

At the stage of the application, Redux and firebase haven’t been added to the application. Data are still being passed around using props. One can visualize the props as the dynamic data in the application. You’ll also notice this a Home component which serves as the parent component to the Goals and Todo components. The Goals component is a “class-based” component while the Todo component is “function based component”. Data Management in this application isn’t at its best yet. You’ll notice in the Home component, there is a function named addAGoal. What this function does is to get data from the Goals component and then update the state of the Home component. If we were to use Redux, the function wouldn’t be useful because we’ll be getting our data from the store and then any component which is connected to the store can easily have access to the data.

Adding Redux

For us to be able to use Redux properly in the application, we’ll need to add react-redux library which is responsible for helping us to connect our component to the redux store. Head on to your terminal and type the command below to install the package.

npm i react-redux

Create a store directory, in this directory, also create actions and reducers directory. In the actions directory, create a createGoal file and paste the code below into the file.

export const createGoalAction = (goal) => {
goal.id = Math.random()*100000000000;
return {
type: 'CREATE_GOAL',
goal
}
}

in the reducers directory, create two files and name them createGoal and rootReducer respectively. Then paste the code below into their respective files.

createGoal.js

let goals = [
{id:1, goal:"Become better at testing apps"},
{id:2, goal:"Become a google dev expert"}
]
const createGoal = (state = goals, action) => {const { goal } = actionswitch(action.type){
case 'CREATE_GOAL':
return [
...state,
goal
]
default:
return state
}
}
export default createGoal

rootReducer.js

import { combineReducers } from 'redux';
import createGoal from './createGoal'
const rootReducer = combineReducers({
goals: createGoal
})
export default rootReducer

change your index.js, Todo.js, and Goals.js files to reflect the current changes in the application.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import rootReducer from './store/reducers/rootReducer'
const Store = createStore(rootReducer)ReactDOM.render(<Provider store={Store}><App /></Provider>, document.getElementById('root'));
registerServiceWorker();

Goals.js

import React from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
const Goals = (props) => {
let goals = props.goal
let goal
goals.length === 0 ?
goal = <h5>You haven't set a goal</h5> :
goal = goals.map( g => {
return (
<li className="collection-item" key={ g.id } >
{ g.goal }
<Link className='secondary-content' to='/'>
<i className='material-icons'>edit</i>
</Link>
<Link className='secondary-content' to='/'>
<i className='material-icons'>delete</i>
</Link>
</li>
)
})
return (
<div>
<ul className="collection">
{ goal }
</ul>
</div>
)
}
const mapStateToProps = state => {
return {
goal: state.goals
}
}
export default connect(mapStateToProps)(Goals)

Todo.js

import React from 'react'
import { connect } from 'react-redux'
import { createGoalAction } from '../store/actions/createGoal'
class Todo extends React.Component {state = {
goal: ''
}
getGoal = e => {
e.preventDefault()
if(this.state.goal !== ''){
this.props.createGoal(this.state)
this.setState({ goal: '' })
}
}
render(){
return(
<div>
<div className="row">
<form className="col s12" style={{ marginTop: '70px' }} onSubmit={ this.getGoal }>
<input type="text"
onChange={ e => { this.setState({ goal: e.target.value })} }
value={ this.state.goal }
required />
<div className='center'>
<button
className='btn btn-large blue'
onClick={ this.getGoal }>
Add Goal
</button>
</div>
</form>
</div>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
createGoal: (goal) => { dispatch(createGoalAction(goal))}
}
}
export default connect(null, mapDispatchToProps)(Todo)

By now you should be able to add a goal using Redux.

Explanation

Let’s start with the changes in the index.js file. We made three key changes to this file. The first being that we imported a createStore function from redux. Secondly, we imported a Provider component from react-redux. Finally, we then wrapped the Provider component around our App component and also passed down the createStore function as a props into the Provider component. All of these steps help us create a store and also connect our application to the store. i.e we create a store with the createStore function from redux, and then, connect the store to our application with the Provider component which takes the store we have created as a props.

In the actions directory, we created a function which we named “createGoalAction”. This function is fired when we want to add a goal to the store or when we dispatch an action for adding a goal to the store.

In the Todo.js file, we imported connect which is a higher-order component from react-redux. This component returns a function that takes in the component which we want to connect to the store as an argument. In this case, it is the Todo component. We also created a function named “mapDispatchToProps”. This function returns an object, the object is then automatically attached to the props or properties of our component. The connect component actually takes in two arguments, one is the “mapStateToProps” function which enables us to have access to the data in the store. Hence, data is easily accessed from the store via this function which is then automatically attached to the props of the component. The “mapDispatchToProps” function, on the other hand, is mainly used to dispatch an action which is to mutate the state or the data in the store. This function is also automatically attached to the props of the connected component.

The same thing in which we did in the Todo.js was also done in the Goals.js only that in the Goals.js we didn’t have a “mapDispatchToProps” function. Instead, the “mapStateToProps” function was used. This was used so we could get the data in the store, which is then attached to the props of the component.

Let’s move to the reducers directory, where we have the createGoal and rootReducer files. In the createGoal file, there’s a function named “createGoal”. This function takes in the two arguments; the first is the state, and second is the action. The state was also set or assigned to a goals object. When creating a reducer, the first argument which is the state in this instance needs to have an initial state or data. The action argument, on the other hand, is the type of action we have dispatched from a component which has access or is connected to the store. In this instance, we dispatched a “CREATE_GOAL” action type.

In the rootReducer file, we imported a function named “combineReducers” from redux. This function takes in an object as a parameter. In this object, we created a “goals” key which we use to access the state or our data from the library. Finally, we then exported the Function. It is this function we pass in as an argument into the createStore function in the Index.js file. Alternatively, instead of using the “combineReducers” function to combine all of our reducers into one object, we can just pass in the “createGoal” reducer into the createStore function. However, this can only be done if there’s just going to be just a reducer.

Firebase/Firestore

To begin using firestore in our application, we first need to set up a firebase project which is going to be associated with this project we are working on. To begin with the setup, head on the link provided below.

https://firebase.google.com/

Click on the “Go to console” link at the top right corner of the page, click on the “Add Project” button, then give your project a name, finally, then click on the terms and condition checkbox and the “create project” button to create the project.

After your project is being created, you’re going to be redirected to your project’s dashboard. On the dashboard, you’ll see an HTML logo, click on it. After clicking on it, you’ll see a modal with a bunch of code which we’re are going to use to add firebase to our application.

In your SRC folder, create a config folder with an index file. Then include the code you copied from the dashboard into the file. After that, head on to the terminal to install the following packages.

npm i firebase react-redux-firebase redux-firestore redux-thunk

react-redux-firebase — for syncing data in firestore to our store.

react-firestore — for connecting the whole application to the firestore database.

redux-thunk- allows us to call an asynchronous function in our app before action is being dispatched.

In the index file copy and paste the following code into it.

import firebase from 'firebase/app'
import 'firebase/firestore'
import { API_KEY } from './key'// Initialize Firebase
const config = {
apiKey: API_KEY,
authDomain: "goalz-7e008.firebaseapp.com",
databaseURL: "YOUR_DB_URL",
projectId: "goalz-7e008",
storageBucket: "goalz-7e008.appspot.com",
messagingSenderId: "169013087966"
};
firebase.initializeApp(config);
firebase.firestore().settings({ timestampsInSnapshots: true });
export default firebase

Go back to your project dashboard and click on the database tab which is at the left-hand corner of the screen. After clicking on the click, you should be re-directed to the page below. Finally, click on the “ create database” button. A modal will pop up after clicking the button, click on the “start in test mode” checkbox (A warning message will come up. You can ignore for now).

To add a collection to the database, click on the “add collection” button. In the collection id input box, type “goalz” as the collection id. Then paste the following texts into the value input box.

Become better at testing apps
Become a google dev expert
Date Ariana Grande for a week

Change the following files with the code below.

Index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { createStore, applyMiddleware, compose } from 'redux'
import { Provider } from 'react-redux'
import { reduxFirestore, getFirestore } from 'redux-firestore'
import thunk from 'redux-thunk'
import rootReducer from './store/reducers/rootReducer'
import firebase from './config/'
const Store = createStore(rootReducer, compose(
applyMiddleware(thunk.withExtraArgument({getFirestore})),
reduxFirestore(firebase)
))
ReactDOM.render(<Provider store={Store}><App /></Provider>, document.getElementById('root'));
registerServiceWorker();

App.js

import React, { Component } from 'react';
import './index.css'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import Navbar from './components/NavBar'
import Home from './components/Home'
import singleGoal from './components/Agoal'
class App extends Component {
render() {
return (
<BrowserRouter>
<div>
<Navbar />
<Switch>
<Route exact path='/' component={ Home } />
<Route path='/:id' component={singleGoal} />
</Switch>
</div>
</BrowserRouter>
);
}
}
export default App;

reducers/createStore.js

const getGoal = (state = {}, action) => {const { data } = actionswitch(action.type){
case 'GET_GOAL':
return {
...state,
data
}
default:
return state
}
}
export default getGoal

rootDirectory.js

import { combineReducers } from 'redux';
import getGoal from './createGoal'
import { firestoreReducer } from 'redux-firestore'
const rootReducer = combineReducers({
firestoreGoals: firestoreReducer,
getGoal: getGoal
})
export default rootReducer

In the components folder, create an Agoal.js file and paste the following code in it.

import React from 'react'
import Form from './form'
import { connect } from 'react-redux'
import { getAGoal, updateGoal } from '../store/actions/createGoal'class singleGoal extends React.Component {state = {
goal: ''
}
componentDidMount(){
let {id} = this.props.match.params
this.props.getAGoal(id)
}
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.getAGoalData !== prevProps.getAGoalData) {
const goal = this.props.getAGoalData.data
this.setState({goal: goal.goal})
}
}
handleChange = value => {
this.setState({ goal: value })
}
updateGoal = e => {
e.preventDefault()
this.props.updateGoal(this.state, this.props.match.params.id)
this.props.history.push('/')
}
render() {
return (
<div>
<div className='center'>
<h4>Change Goal</h4>
<div className='container'>
<Form
value={this.state.goal}
btnName={`Update Goal`}
handleChange={this.handleChange}
formAction={this.updateGoal}
/>
</div>
</div>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
getAGoal: (id) => { dispatch(getAGoal(id))},
updateGoal: (updatedGoal, id) => { dispatch(updateGoal(updatedGoal, id)) }
}
}
const mapStateToProps = (state) => {
return {
getAGoalData: state.getGoal
}
}
export default connect(mapStateToProps, mapDispatchToProps)(singleGoal)

Also, create a file in the components directory and name it form.js then paste in the code below into the file.

import React from 'react'const Form = (props) => (
<form className="col s12" style={{ marginTop: '70px' }} onSubmit={ props.formAction }>
<input
type="text"
onChange={ e => { props.handleChange(e.target.value) } }
value={props.value}
required
/>
<div className='center'>
<button
className='btn btn-large blue'
onClick={ props.formAction }>
{ props.btnName }
</button>
</div>
</form>
)
export default Form

Update the following files;

Goal.js

import React from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { firestoreConnect } from 'react-redux-firebase'
import { compose } from 'redux'
import { deleteGoal, getAGoal } from '../store/actions/createGoal'
const Goals = (props) => {
console.log(props)
const { goal, deleteGoal, getAGoal } = props
let styling
let goals
if(goal){styling = "collection"
goals = ( goal.map( g => {
return (
<li className="collection-item" key={ g.id } >
{ g.goal }
<Link className='secondary-content' to='/' onClick={ () => deleteGoal(g.id) }>
<i className='material-icons delete'>delete</i>
</Link>
<Link className='secondary-content' to={`${g.id}`}>
<i className='material-icons edit'>edit</i>
</Link>
</li>
)
})
)
}else{
goals = <h4 style={{ textAlign: 'center' }}>Loading...</h4>
styling = ''
}
return (
<div>
<ul className={`${styling}`} style={{ marginTop: '70px' }}>
{ goals }
</ul>
</div>
)
}
const mapDispatchToProps = (dispatch) => {
return {
deleteGoal: (id) => { dispatch(deleteGoal(id))},
getAGoal: (id) => { dispatch(getAGoal(id))}
}
}
const mapStateToProps = (state) => {
console.log(state)
return {
goal: state.firestoreGoals.ordered.goalz
}
}
export default compose(
connect(mapStateToProps, mapDispatchToProps),
firestoreConnect([
{ collection: 'goalz'}
])
)(Goals)

Todo.js

import React from 'react'
import { connect } from 'react-redux'
import { createGoalAction } from '../store/actions/createGoal'
import Form from './form'
class Todo extends React.Component {state = {
goal: ''
}
getGoal = e => {
e.preventDefault()
this.props.createGoal(this.state)
this.setState({ goal: '' })

}
handleChange = value => {
this.setState({ goal: value })
}
render(){
return(
<div>
<div className="row">
<Form
formAction={this.getGoal}
value={this.state.goal}
handleChange={this.handleChange}
btnName={`Add Button`}
/>
</div>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
createGoal: (goal) => { dispatch(createGoalAction(goal))}
}
}
export default connect(null, mapDispatchToProps)(Todo)

Action/createStore.js

export const createGoalAction = (goal) => {
return (dipatch, getState, {getFirestore}) => {
const firestore = getFirestore()
firestore.collection('goalz').add({
...goal
}).then(() => {
dipatch({ type: 'CREATE_GOAL', goal })
}).catch((err) => {
dipatch({ type: 'CREATE_GOAL_ERROR', err })
})
}
}
export const deleteGoal = (id) => {
return (dispatch, getState, { getFirestore }) => {
const firestore = getFirestore()
firestore.collection('goalz').doc(id).delete();
}
}
export const updateGoal = (updatedGoal, id) => {
return (dispatch, getState, { getFirestore }) => {
const firestore = getFirestore()
firestore.collection('goalz').doc(id).update({
...updatedGoal
});
}
}
export const getAGoal = (goalID) => {
return (dispatch, getState, { getFirestore }) => {
const firestore = getFirestore()
firestore.collection('goalz').doc(goalID).get().then((doc) => {
if(doc.exists){
const data = doc.data()
dispatch({ type: 'GET_GOAL', data })
}else{
console.log('does not exist')
}

})
}
}

We have finally come to the end of the article. You can fork this project on GitHub via the link provided below for an easy follow-up. You can also reach me on twitter for any further explanations.

https://github.com/olamilekan000/Goalzhttps://twitter.com/Principino__ or @Principino__

Arigato!

--

--