React Redux – Firebase CRUD Operations example

React Redux – Firebase CRUD Operations example

In this tutorial, we’re gonna do CRUD operations with Firebase Realtime Database in a React Redux Application.

Related Article: How to use Firebase Database CRUD Operations in React Webpack

Async Redux Action

Overview

We have known how the Redux works with Action creators:
– A Component calls an Action creator.
– That creator returns an Object with type field.
– The Component dispatches the Object.
– Redux store changes.

With Async Redux Action:
– A Component calls an Action creator.
– That creator returns a Function instead of an Action object.
– The Component dispatches the Function.
– The Function runs. It can have side effects with asynchronous API calls or dispatch Actions.

This Function will get executed by the Redux Thunk middleware from package called redux-thunk.

Redux Thunk

Redux Thunk middleware allows us to write Action creators that return a function instead of an action.
So we can:
– perform asynchronous dispatch:


const _increment = () => ({
    type: 'INCREMENT'
});

const increment = () => {
    return (dispatch) => {
        setTimeout(() => {
            dispatch(_increment());
        }, 1000);
    };
}

– or perform conditional dispatch:


const incrementIfOdd = () => {
    return (dispatch, getState) => {
        const { counter } = getState();

        if (counter % 2 === 0) {
            return;
        }

        dispatch(increment());
    };
}

The inner function receives the store dispatch() and getState() methods as parameters.
Then, we must enable Redux Thunk using applyMiddleware() function:


import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

// API requires redux >= 3.1.0
const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

Example Overview

Goal

We will build a React Redux Application that can do CRUD Operations with Firebase Realtime Database:

react-redux-firebase-crud-example-goal

Technologies

– React 16.3.0
– Redux 3.7.2
– React Redux 5.0.7
– Webpack 4.4.1
– Firebase 4.13.1
– Redux Thunk 2.2.0

Project Structure

react-redux-firebase-crud-example-structure

Practice

Setup React Application with Firebase Database

Please visit How to add Firebase to React App.

react-redux-firebase-add-firebase-webapp-config

firebase/firebase.js


import * as firebase from 'firebase';

const config = {
    apiKey: "xxx",
    authDomain: "jsa-react-redux-firebase.firebaseapp.com",
    databaseURL: "https://jsa-react-redux-firebase.firebaseio.com",
    projectId: "jsa-react-redux-firebase",
    storageBucket: "jsa-react-redux-firebase.appspot.com",
    messagingSenderId: "xxx"
};

firebase.initializeApp(config);
const database = firebase.database();

export { firebase, database as default };

Redux Actions

We create, read, update, delete Book item with database object.
For more details, please visit: Firebase Database CRUD Operations.

actions/books.js


import database from '../firebase/firebase';

const _addBook = (book) => ({
    type: 'ADD_BOOK',
    book
});

export const addBook = (bookData = {
    title: '',
    description: '',
    author: '',
    published: 0
}) => {
    return (dispatch) => {
        const book = {
            title: bookData.title,
            description: bookData.description,
            author: bookData.author,
            published: bookData.published
        };

        return database.ref('books').push(book).then(ref => {
            dispatch(_addBook({
                id: ref.key,
                ...book
            }));
        });
    };
};

const _removeBook = ({ id } = {}) => ({
    type: 'REMOVE_BOOK',
    id
});

export const removeBook = ({ id } = {}) => {
    return (dispatch) => {
        return database.ref(`books/${id}`).remove().then(() => {
            dispatch(_removeBook({ id }));
        })
    }
};

const _editBook = (id, updates) => ({
    type: 'EDIT_BOOK',
    id,
    updates
});

export const editBook = (id, updates) => {
    return (dispatch) => {
        return database.ref(`books/${id}`).update(updates).then(() => {
            dispatch(_editBook(id, updates));
        });
    }
};

const _getBooks = (books) => ({
    type: 'GET_BOOKs',
    books
});

export const getBooks = () => {
    return (dispatch) => {
        return database.ref('books').once('value').then(snapshot => {
            const books = [];

            snapshot.forEach(item => {
                books.push({
                    id: item.key,
                    ...item.val()
                });
            });

            dispatch(_getBooks(books));
        });
    };
};

actions/filters.js

export const filterText = (text = '') => ({
    type: 'FILTER_TEXT',
    text
});

export const startYear = (startYear) => ({
    type: 'START_YEAR',
    startYear
});

export const endYear = (endYear) => ({
    type: 'END_YEAR',
    endYear
});

export const sortBy = (sortType) => ({
    type: 'SORT_BY',
    sortType
});

const filtersReducerDefaultState = {
    text: '',
    sortBy: '',
    startYear: undefined,
    endYear: undefined
};

export const clear = () => ({
    type: 'CLEAR',
    defaultFilter: filtersReducerDefaultState
});

Redux Reducers

reducers/books.js


const booksReducerDefaultState = [];

export default (state = booksReducerDefaultState, action) => {
    switch (action.type) {
        case 'ADD_BOOK':
            return [
                ...state,
                action.book
            ];
        case 'REMOVE_BOOK':
            return state.filter(({ id }) => id !== action.id);
        case 'EDIT_BOOK':
            return state.map((book) => {
                if (book.id === action.id) {
                    return {
                        ...book,
                        ...action.updates
                    };
                } else {
                    return book;
                }
            });
        case 'GET_BOOKs':
            return action.books;
        default:
            return state;
    }
};

reducers/filters.js


const filtersReducerDefaultState = {
    text: '',
    sortBy: '',
    startYear: undefined,
    endYear: undefined
};

export default (state = filtersReducerDefaultState, action) => {
    switch (action.type) {
        case 'FILTER_TEXT':
            return {
                ...state,
                text: action.text
            };
        case 'START_YEAR':
            return {
                ...state,
                startYear: action.startYear
            };
        case 'END_YEAR':
            return {
                ...state,
                endYear: action.endYear
            };
        case 'SORT_BY':
            return {
                ...state,
                sortBy: action.sortType
            };
        case 'CLEAR':
            return {
                ...state,
                text: action.defaultFilter.text,
                sortBy: action.defaultFilter.sortBy,
                startYear: action.defaultFilter.startYear,
                endYear: action.defaultFilter.endYear
            };
        default:
            return state;
    }
}

React Store

store/store.js


import { createStore, combineReducers, applyMiddleware } from "redux";
import booksReducer from '../reducers/books';
import filtersReducer from '../reducers/filters';
import thunk from 'redux-thunk';

const demoState = {
    books: [
        {
            id: '123abcdefghiklmn',
            title: 'Origin',
            description: 'Origin thrusts Robert Langdon into the dangerous intersection of humankind’s two most enduring questions.',
            author: 'Dan Brown',
            published: 2017
        }
    ],
    filters: {
        text: 'ori',
        sortBy: 'published', // published or title
        startYear: undefined,
        endYear: undefined
    }
};

export default () => {
    return createStore(
        combineReducers({
            books: booksReducer,
            filters: filtersReducer
        }),
        applyMiddleware(thunk));
};

Filter & Sort function

selectors/books.js


// getVisibleBooks
export default (books, { text, sortBy, startYear, endYear }) => {
    return books.filter(book => {
        const textMatch =
            book.title.toLowerCase().includes(text.toLowerCase()) ||
            book.description.toLowerCase().includes(text.toLowerCase());

        const startYearMatch = typeof startYear !== 'number' || startYear <= book.published;
        const endYearMatch = typeof endYear !== 'number' || book.published <= endYear;

        return textMatch && startYearMatch && endYearMatch;
    }).sort((book1, book2) => {
        if (sortBy === 'title') {
            return book1.title.localeCompare(book2.title);
        } else if (sortBy === 'published') {
            return book1.published < book2.published ? -1 : 1;
        }
    });
}

React Components

Data Model

components/Book.js

import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { removeBook } from '../actions/books';

const Book = ({ id, title, description, author, published, dispatch }) => (
    <div>
        <Link to={`/book/${id}`}>
            <h4>{title} ({published})</h4>
        </Link>
        <p>Author: {author}</p>
        {description && <p>{description}</p>}
        <button onClick={() => {
            dispatch(removeBook({ id }));
        }}>Remove</button>
    </div>
);

export default connect()(Book);

Show Item List

components/DashBoard.js

import React from 'react';
import BookList from './BookList';
import BookFilter from './BookFilter';

const DashBoard = () => (
    <div className='container__list'>
        <BookFilter />
        <BookList />
    </div>
);

export default DashBoard;

components/BookFilters.js

import React from 'react';
import { connect } from 'react-redux';
import { filterText, startYear, endYear, sortBy } from '../actions/filters';

class BookFilters extends React.Component {
    constructor(props) {
        super(props);
        this.filterYear = this.filterYear.bind(this);
    }

    filterYear() {
        let start = (+this.startYear.value) !== 0 ? (+this.startYear.value) : undefined;
        let end = (+this.endYear.value) !== 0 ? (+this.endYear.value) : undefined;
        this.props.dispatch(startYear(start));
        this.props.dispatch(endYear(end));
    }

    render() {
        return (
            <div style={{ marginBottom: 15 }}>
                <input type='text' placeholder='search'
                    value={this.props.filters.text}
                    onChange={(e) => {
                        this.props.dispatch(filterText(e.target.value));
                    }}></input>

                sorted By:
                <select
                    value={this.props.filters.sortBy}
                    onChange={(e) => {
                        this.props.dispatch(sortBy(e.target.value));
                    }}>
                    <option value='none'>---</option>
                    <option value='title'>Title</option>
                    <option value='published'>Published</option>
                </select>
                <br /><br />

                <input type='number' placeholder='startYear' style={{ width: 80 }}
                    ref={el => this.startYear = el}></input>
                <input type='number' placeholder='endYear' style={{ width: 80 }}
                    ref={el => this.endYear = el}></input>

                <button onClick={this.filterYear}>-></button>
            </div>
        );
    }
}

const mapStateToProps = (state) => {
    return {
        filters: state.filters
    }
}

export default connect(mapStateToProps)(BookFilters);

components/BookList.js

import React from 'react';
import { connect } from 'react-redux';
import Book from './Book';
import getVisibleBooks from '../selectors/books';

const BookList = (props) => (
    <div>
        Book List:
        <ul>
            {props.books.map(book => {
                return (
                    <li key={book.id}>
                        <Book {...book} />
                    </li>
                );
            })}
        </ul>
    </div>
);

const mapStateToProps = (state) => {
    return {
        books: getVisibleBooks(state.books, state.filters)
    };
}

export default connect(mapStateToProps)(BookList);

Add & Edit Item

components/AddBook.js

import React from 'react';
import BookForm from './BookForm';
import { connect } from 'react-redux';
import { addBook } from '../actions/books';

const AddBook = (props) => (
    <div>
        <h3>Set Book information:</h3>
        <BookForm
            onSubmitBook={(book) => {
                props.dispatch(addBook(book));
                props.history.push('/');
            }}
        />
    </div>
);

export default connect()(AddBook);

components/EditBook.js

import React from 'react';
import BookForm from './BookForm';
import { connect } from 'react-redux';
import { editBook } from '../actions/books';

const EditBook = (props) => (
    <div className='container__box'>
        <BookForm
            book={props.book}
            onSubmitBook={(book) => {
                props.dispatch(editBook(props.book.id, book));
                props.history.push('/');
            }}
        />
    </div>
);

const mapStateToProps = (state, props) => {
    return {
        book: state.books.find((book) =>
            book.id === props.match.params.id)
    };
};

export default connect(mapStateToProps)(EditBook);

components/BookForm.js

import React from 'react';

export default class BookForm extends React.Component {
    constructor(props) {
        super(props);
        this.onTitleChange = this.onTitleChange.bind(this);
        this.onAuthorChange = this.onAuthorChange.bind(this);
        this.onDescriptionChange = this.onDescriptionChange.bind(this);
        this.onPublishedChange = this.onPublishedChange.bind(this);
        this.onSubmit = this.onSubmit.bind(this);

        this.state = {
            title: props.book ? props.book.title : '',
            author: props.book ? props.book.author : '',
            description: props.book ? props.book.description : '',
            published: props.book ? props.book.published : 0,

            error: ''
        };
    }

    onTitleChange(e) {
        const title = e.target.value;
        this.setState(() => ({ title: title }));
    }

    onAuthorChange(e) {
        const author = e.target.value;
        this.setState(() => ({ author: author }));
    }

    onDescriptionChange(e) {
        const description = e.target.value;
        this.setState(() => ({ description: description }));
    }

    onPublishedChange(e) {
        const published = parseInt(e.target.value);
        this.setState(() => ({ published: published }));
    }

    onSubmit(e) {
        e.preventDefault();

        if (!this.state.title || !this.state.author || !this.state.published) {
            this.setState(() => ({ error: 'Please set title & author & published!' }));
        } else {
            this.setState(() => ({ error: '' }));
            this.props.onSubmitBook(
                {
                    title: this.state.title,
                    author: this.state.author,
                    description: this.state.description,
                    published: this.state.published
                }
            );
        }
    }

    render() {
        return (
            <div>
                {this.state.error && <p className='error'>{this.state.error}</p>}
                <form onSubmit={this.onSubmit} className='add-book-form'>

                    <input type="text" placeholder="title" autoFocus
                        value={this.state.title}
                        onChange={this.onTitleChange} />
                    <br />

                    <input type="text" placeholder="author"
                        value={this.state.author}
                        onChange={this.onAuthorChange} />
                    <br />

                    <textarea placeholder="description"
                        value={this.state.description}
                        onChange={this.onDescriptionChange} />
                    <br />

                    <input type="number" placeholder="published"
                        value={this.state.published}
                        onChange={this.onPublishedChange} />
                    <br />
                    <button>Add Book</button>
                </form>
            </div>
        );
    }
}

React Router

routers/AppRouter.js

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Header from '../components/Header';
import DashBoard from '../components/DashBoard';
import AddBook from '../components/AddBook';
import EditBook from '../components/EditBook';
import Help from '../components/Help';
import NotFound from '../components/NotFound';

const AppRouter = () => (
    <BrowserRouter>
        <div className='container'>
            <Header />
            <Switch>
                <Route path="/" component={DashBoard} exact={true} />
                <Route path="/add" component={AddBook} />
                <Route path="/book/:id" component={EditBook} />
                <Route path="/help" component={Help} />
                <Route component={NotFound} />
            </Switch>
        </div>
    </BrowserRouter>
);

export default AppRouter;

components/Header.js

import React from 'react';
import { NavLink } from 'react-router-dom';

const Header = () => (
    <header>
        <h2>Java Sample Approach</h2>
        <h4>Book Mangement Application</h4>
        <div className='header__nav'>
            <NavLink to='/' activeClassName='activeNav' exact={true}>Dashboard</NavLink>
            <NavLink to='/add' activeClassName='activeNav'>Add Book</NavLink>
            <NavLink to='/help' activeClassName='activeNav'>Help</NavLink>
        </div>
    </header>
);

export default Header;

app.js

import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './routers/AppRouter';
import getAppStore from './store/store';
import { addBook, getBooks } from './actions/books';
import './styles/styles.scss';

import { Provider } from 'react-redux';

const store = getAppStore();

const template = (
    <Provider store={store}>
        <AppRouter />
    </Provider>
);

store.dispatch(getBooks()).then(() => {
    ReactDOM.render(template, document.getElementById('app'));
});

Source Code

ReactReduxFirebase

For running:
- yarn install
- yarn run dev-server or yarn run build, then yarn run serve.



By grokonez | April 26, 2018.

Last updated on March 31, 2021.



Related Posts


Got Something To Say:

Your email address will not be published. Required fields are marked *

*