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
Contents
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:
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
Practice
Setup React Application with Firebase Database
Please visit How to add Firebase to React App.
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
For running:
– yarn install
– yarn run dev-server
or yarn run build
, then yarn run serve
.