Fairtasker: Firebase backend
Home About Portfolio Testimonials Blog Contact

The Web Dev Geek Blog

Your regular dose of geeky, businessy, websitey information

Fairtasker: Firebase backend

Now that we have the basics of our layout setup after the first two tutorials it’s time to take the app up a notch and stop generating the tasks every time the app is loaded, we want to be able to create, persist and load the created tasks, to do this we will be using simple datastorage made available by the firebase realtime database. The realtime database provides a restful API that we will use to save and fetch our data, it doesn’t provide advanced query options but as this is just a demo application it will suit our needs.

Knowledge requirements

  • Firebase
  • Axios

Setup firebase backend

Lets get started by first getting the url to a firebase realtime database, head on over to firebase and create a new project. If you haven’t already got a firebase account it’s free for what we are going to be doing so once you’re there sign up and create a project. Name the project whatever you like, I went with fairtasker, and once the project is created you will see the option Add Firebase to your web app clicking on this will present you will the code that can be copied to your site, we don’t need all of it, just copy the databaseURL and save it as an export in our constants file export const DB_BASE_URL = 'https://example.firebaseio.com/';.

To see the setting up visually, watch this video, for around 50 seconds.

Seed database

Now that we have a database we need to create a collection of tasks and store them in said database. I am going to save you some time and have created a small little app that will seed a firebase database with 50 randomly generated tasks, all you need to do is provide the name of your project, the part after https:// and before .firebaseio.com/, click Seed and then sit back and wait. Once the database is seeded you will see the response Firebase has been seeded and you can move on to the next part.

The new tasks contain the additional fields price, postedBy, date and time which we will make use of later in the tutorial.

Show and tell

As we are now going to be getting our tasks asyncronously we won’t have tasks to display on initial load. To prevent user’s from thinking that the site is broken we will indicate that something is happening by showing a spinner while the required data is being fetched.

Create a UI folder in components and add a new file Spinner.js and CSS module Spinner.module.css. For our Spinner we will get the HTML and CSS from css spinner, we simply need to choose one we like, view it’s source and copy the HTML into our component and the CSS into our CSS file.

// ./components/Spinner/Spinner.js

import React from 'react';
import classes from "./Spinner.module.css";

const Spinner = () => <div className={classes.loader}>Loading...</div>;
 
export default Spinner;
/* ./components/UI/Spinner/Spinner.module.css */

.loader,
.loader:before,
.loader:after {
  border-radius: 50%;
}

.loader {
  color: #8ecfe8;
  font-size: 11px;
  text-indent: -99999em;
  margin: 55px auto;
  position: relative;
  width: 10em;
  height: 10em;
  box-shadow: inset 0 0 0 1em;
  -webkit-transform: translateZ(0);
  -ms-transform: translateZ(0);
  transform: translateZ(0);
}

.loader:before,
.loader:after {
  position: absolute;
  content: '';
}

.loader:before {
  width: 5.2em;
  height: 10.2em;
  background: #E9EFF1;
  border-radius: 10.2em 0 0 10.2em;
  top: -0.1em;
  left: -0.1em;
  -webkit-transform-origin: 5.2em 5.1em;
  transform-origin: 5.2em 5.1em;
  -webkit-animation: load2 2s infinite ease 1.5s;
  animation: load2 2s infinite ease 1.5s;
}

.loader:after {
  width: 5.2em;
  height: 10.2em;
  background: #E9EFF1;
  border-radius: 0 10.2em 10.2em 0;
  top: -0.1em;
  left: 5.1em;
  -webkit-transform-origin: 0px 5.1em;
  transform-origin: 0px 5.1em;
  -webkit-animation: load2 2s infinite ease;
  animation: load2 2s infinite ease;
}

@-webkit-keyframes load2 {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

@keyframes load2 {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

Get tasks from Database

Start by installing the axios module npm install axios --save.

For all our calls to the database we will create our own instance of axios. In our utils folder add the file axios-tasks.js, from here we will export the new axios instance with the baseURL configured to our database url. Once the file is created import it into the Tasks component.

import axios from "axios";
import { DB_BASE_URL } from "./constants";

const instance = axios.create({
    baseURL: DB_BASE_URL
})

export default instance;

We need to wait for <Tasks /> to mount before we can request our tasks from the database so add the componentDidMount() lifecylce method and from here we will make our initial data request. We want to get the ten latest tasks so when we request the tasks from the database we will need to pass along some parameteres, orderBy and limitToLast. orderBy will sort the createdAt values in ascending order and limitToLast will get the last 10. The data returned will be an object which we will turn into an array so we can reverse the order of the tasks before we set the tasks in our state.

Create the function initDataLoad() outside of the class and call it in componentDidMount

function initDataLoad(){
  return axios.get('tasks.json', {
        params: {
            orderBy: '"createdAt"',
            limitToLast: 10
        }
    })
}
 componentDidMount(){
        initDataLoad().then(({data:tasks})=>{    
            const tasksArray = Object.entries(tasks).reverse()        
            this.setState({ tasks: tasksArray} ) 
        })
    }  

state.tasks will now be populated after <Tasks /> has been mounted so we will initialise state.tasks to null and can remove the getStatus() function and TaskStatus import from our Tasks component.

state = {
        selectedTask: null,
        tasks: null
    }

After the tasks are set state.tasks will be an array of arrays where each array has two values, an id and the task data, this is different to our original data structure so we must update the logic used to generate of our <TaskCard />’s in TasksLeft. While we have no <TaskCard>’s we will show the <Spinner />

// ./components/TasksLeft/TasksLeft.js

// ...
import Spinner from '../UI/Spinner/Spinner';

const TasksLeft = ({ tasks, visible, width }) => {
    let taskCards = null;
    
    if (tasks) {
        taskCards = tasks.map(([id, task]) => {
                task.id = id;
                return <TaskCard key={id} task={task} />
        })        
    }

    const display = visible ? {} : { display: 'none' } 

    return (<Grid item xs={width} className="i-scroll tlc-width" style={display}>
        {taskCards || <Spinner /> }
    </Grid> );
}
 
export default TasksLeft;

Load more tasks

When we get to the near the bottom of our list of <TaskCard />’s we want to load the next ten oldest tasks and keep doing so until there are no more to load. We will render a new component in TasksLeft that will, once it comes into the viewport, make a request for the next lot of tasks.

First Let’s work on updating Tasks, we will add the following properties to state: hasMore, isFetching, taskIds. hasMore will indicate if there are possibly more tasks to retrieve from the database, isFetching indicates if we are currently fetching more tasks from the database, used to prevent sending concurrent requests, and taskIds will contain all the task ids of retrieved tasks.

state:{
	hasMore: true,
	isFetching: false,
	taskIds: []
	// ...
}

Next we need a loadMoreTasks() function that we can call to retrieve more tasks if they are available, if no more are available we will set hasMore to false.

loadMoreTasks = () => {
        const { hasMore, isFetching, tasks } = this.state;

        if (hasMore && !isFetching) {
            this.setState({isFetching: true});

            axios.get('tasks.json',{
                params:{
                    orderBy:'"createdAt"',
                    limitToLast: tasksToFetch + 1,
                    endAt: tasks[tasks.length -1][1].createdAt
                }
            }).then(({data:tasks}) => {
                const tasksArray = Object.entries(tasks).reverse();
                tasksArray.shift()
                const newTaskIds = tasksArray.map(([id]) => id)
                
                if (tasksArray.length > 0) {
                    this.setState(({tasks, taskIds}) => {
                        // possibly more tasks available if equal
                        const hasMore = tasksArray.length === tasksToFetch; 
                        
                        return {
                            isFetching: false,
                            tasks:[...tasks, ...tasksArray],
                            taskIds: [...taskIds, ...newTaskIds ],
                            hasMore
                        }
                    })
                }else{
                    this.setState({ hasMore: false, isFetching: false })                    
                }
            })
        }
    }

We will soon be needing to use loadMoreTasks in TasksLeft so we will go ahead and pass it into <TasksLeft /> ready to use in the next step.

<TasksLeft ... loadMore={this.loadMoreTasks}/>

Next we need to make the component that we can watch, we will use the scrollmonitor-react library and the Watch and ScrollContainer HOC’s to implement this.

Add the scrollmonitor-react package to our app npm install scrollmonitor-react --save

In our UI folder add a new component Watched, this will be a functional component that returns an empty span and is then passed to the Watch HOC.

// UI/Watched/Watched.js

import React from 'react';
import { Watch } from 'scrollmonitor-react';

export default Watch(() => <span></span>);

Next we need to import ScollContainer HOC from scollmonitor-react and our Watched component into TasksLeft, update the export and add <Watched />. We need to set a few properties on <Watched />, ScrollContainer() is going to give us a new prop scrollContainer which we need to pass to <Watched /> on it’s scrollContainer prop, fullyEnterViewport takes the function that will be called everytime the watched component enters the viewport, to this we will pass the loadMore function the we passed in from <Tasks /> earlier and we’ll add key with the value 'watched'.

// tasksleft

// ...
import { ScrollContainer } from "scrollmonitor-react";
import Watched from "../UI/Watched/Watched";

const TasksLeft = ({ tasks, visible, width, scrollContainer, loadMore }) => {
    let taskCards = null;
    
    if (tasks) {
        taskCards = tasks.map(([id, task]) => {
                task.id = id;
                return <TaskCard key={id} task={task} />
        })
        
        taskCards.splice(-2,0,<Watched 
            key={'watched'}
            scrollContainer={scrollContainer}
            fullyEnterViewport={loadMore}
        />)
    }

    const display = visible ? {} : { display: 'none' } 

    return (<Grid item xs={width} className="i-scroll tlc-width" style={display}>
        {taskCards || <Spinner /> }
    </Grid> );
}
 
export default ScrollContainer(TasksLeft);

Instead of outputting taskCards and <Watched /> independently we only want <Watched /> rendered if there are taskCards so we will splice <Watched /> into the taskCards array before the last two cards, this way we will also begin loading the next lot of tasks before we’ve scrolled to the bottom, giving us infinite scrolling. As <Watched /> is now rendered as part of an array this is why we gave it a key.

Selecting a task

At this point if we start up the development server npm run start and view our app we will see our initial tasks loaded on the left and as we scroll down we will get the next lot of tasks loaded from the database, awesome!!!. Now click on a task…not awesome, as the state.tasks data structure was changed we need to add a line and change two lines in componentDidUpdate() in Tasks to get task selection working again. We will now find the position of taskId in taskIds and then use that position to select the task from tasks.

Before we can access taskIds we will need to update initDataLoad in componentDidMount() to also set our initial taskIds.

componentDidMount(){
        initDataLoad().then(({data:tasks}) => {
            const  tasksArray = Object.entries(tasks).reverse();
            const newTaskIds = tasksArray.map(([id]) => id);
            this.setState({ tasks: tasksArray, taskIds: newTaskIds})
        })
    }
 componentDidUpdate(prevProps) {
        const { taskId: previousId } = prevProps.match.params;
        const { taskId } = this.props.match.params;

        if (previousId !== taskId) {  

            let selectedTask = null;

            if (taskId) {
                const { tasks, taskIds } = this.state;
                let taskIndex = taskIds.findIndex(id => id === taskId);
                selectedTask = tasks[taskIndex][1]; 
            }

            this.setState({ selectedTask })
        }
    }

Now that we can again select tasks from within <TasksLeft> there is one more issue to resolve, if we select a task and refresh the page we aren’t shown the task even though there is a taskId in the url. Currently our selected task is set when the <Tasks /> updates, we need to now set it up so that it is also set when the component did mount so let’s update componentDidMount().

We can’t wait for the initial loading of tasks and then select the task as there is no guarantee that the task we are after is one of the ten latest tasks instead we will solve this so that when <Tasks /> mounts we do our initDataLoad and if a taskId is provided we will also load the task.

Add a loadTask() function to our Tasks file which takes one parameter, taskId, and requests that task from the database.

// ./containers/Tasks/Tasks.js

// ...

function loadTask(taskId) {
    return axios.get(`tasks/${taskId}.json`)
}

class Tasks extends Component {

	// ...

}

export default Tasks;

now update componentDidUpdate(). We will do our initDataLoad() and if there is a taskId we will do a loadTask() and then wait for all responses before we do this.setState().

componentDidMount() {

        const promises = [initDataLoad()];
        const { taskId } = this.props.match.params;
      
        if (taskId) {
            promises.push(loadTask(taskId))
        }

        Promise.all(promises).then((responses) => {
            const { data: tasks } = responses[0];
            const tasksArray = Object.entries(tasks).reverse();
            const taskIds = tasksArray.map(([id]) => id)       
            const newState = { tasks: tasksArray, taskIds };

            if (responses.length === 2) {                
                newState.selectedTask = responses[1].data
            }

            this.setState(newState);
        })
    }

To demonstrate this is working we can scoll down and select a task after the 10 from the initial data load, reload the page and we will see that it is displayed even though it is not in the data from the initDataLoad() function.

The End

Now that the app is looking better on the task front in the next tutorial in the series we will update the components to use all the new task data fields and update the stying to better match the design of Airtasker.