Seat booking with Redux

Let's enhance our seat booking app by integrating the Redux.

We can install React Bindings explicitly, using the following command, since they are not included in Redux by default:

npm install --save react-redux

Now, we will extend our seat booking app by integrating Redux. There will be a lot of changes as it will impact all of our components. Here, we will start with our entry point.

src/index.js:

import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import SeatBookingApp from './containers/SeatBookingApp'
import { Provider } from 'react-redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk'
import reducer from './reducers'
import { getAllSeats } from './actions'


const middleware = [thunk];
//middleware will print the logs for state changes
if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger());
}

const store = createStore(
reducer,
applyMiddleware(...middleware)
)

store.dispatch(getAllSeats())

render(
<Provider store={store}>
<SeatBookingApp />
</Provider>,
document.getElementById('root')
)

Let's understand our code:

  • <Provide store>: Makes the Redux Store available to the component hierarchy. Note that you cannot use connect() without wrapping a parent component in a provider.
  • <SeatBookingApp>: It is our parent component and will be defined under Container components package. It will have code similar to what we have in App.js, which we saw earlier.
  • Middleware: It is like an interceptor in other languages that provides a third-party extension point between dispatching an action and the moment it reaches the reducer, for example, Logging or Logger. If you don't apply a middleware, you would need to add loggers manually in all the actions and reducers.
  • applyMiddleware: It tells Redux store how to handle and set up the middleware. Note the usage of a rest parameter (...middleware); it denotes that the applyMiddleware function accepts multiple arguments (any number) and can get them as an array. A key feature of Middleware is that multiple middleware can be combined together.

We will also need to change our presentational components little bit as per Redux state management.

Let's start with Cart component.

components/Cart.js:

import React from 'react'
import PropTypes from 'prop-types'
const Cart = ({seats,total, onCheckoutClicked}) => {

const hasSeats = seats.length > 0
const nodes = hasSeats ? (

seats.map(seat =>
<div>
Seat Number: {seat.number} - Price: {seat.price}
</div>
)

) : (
<em>No seats selected</em>
)

return (
<div>
<b>Selected Seats</b> <br/>
{nodes} <br/>
<b>Total</b> <br/>
{total}
<br/>
<button onClick={onCheckoutClicked}>
Checkout
</button>
</div>
)
}

Cart.propTypes = {
seats: PropTypes.array,
total: PropTypes.string,
onCheckoutClicked: PropTypes.func
}
export default Cart;

Our cart component receives a checkout function from the parent component that will dispatch checkout action along with seats, which are added in cart.

components/Seat.js:

import React from 'react'
import PropTypes from 'prop-types'

const Seat = ({ number, price, status, rowNo, handleClick }) => {
return (

<li className={'seatobj ' + status} key={number.toString()}>
<input type="checkbox" disabled={status === "booked" ? true : false} id={number.toString()} onClick={handleClick} />
<label htmlFor={number.toString()}>{number}</label>
</li>

)
}

Seat.propTypes = {
number: PropTypes.number,
price: PropTypes.number,
status: PropTypes.string,
rowNo: PropTypes.number,
handleClick: PropTypes.func
}

export default Seat;

Our Seat component receives its state from the parent component along with the handleClick function that dispatches the ADD_TO_CART action.

components/SeatList.js:

import React from 'react'
import PropTypes from 'prop-types'

const SeatList = ({ title, children }) => (

<div>
<h3>{title}</h3>
<ol className="list">
{children}
</ol>
</div>
)

SeatList.propTypes = {
children: PropTypes.node,
title: PropTypes.string.isRequired
}

export default SeatList;

SeatList receives seats data from the container component.

components/SeatRow.js:

import React from 'react'
import PropTypes from 'prop-types'
import Seat from './Seat';

const SeatRow = ({ seats, rowNumber, onAddToCartClicked }) => {
return (
<div>
<li className="row row--1" key="1">
<ol className="seatrow">
{seats.map(seat =>
<Seat key={seat.number} number={seat.number}
price={seat.price}
status={seat.status}
rowNo={seat.rowNo}
handleClick={() => onAddToCartClicked(seat)}
/>
)}

</ol>
</li>
</div>
)
}
SeatRow.propTypes = {
seats: PropTypes.arrayOf(PropTypes.shape({
number: PropTypes.number,
price: PropTypes.number,
status: PropTypes.string,
rowNo: PropTypes.number
})),
rowNumber: PropTypes.number,
onAddToCartClicked: PropTypes.func.isRequired
}

export default SeatRow;

SeatRow receives all seats for that particular row.

Let's check our container components.

containers/SeatBookingApp.js:

import React from 'react'
import SeatContainer from './SeatContainer'
import CartContainer from './SeatCartContainer'
import '../App.css';

const SeatBookingApp = () => (
<div className="layout">
<h2>Ticket Booking App</h2>
<hr />
<SeatContainer />
<hr />
<CartContainer />
</div>
)
export default SeatBookingApp;

It is our parent component and includes the other child container components: SeatContainer and CartContainer.

container/SeatCartContainer.js:

import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { bookSeats } from '../actions'
import { getTotal, getCartSeats } from '../reducers'
import Cart from '../components/Cart'

const CartContainer = ({ seats, total, bookSeats }) => {

return (

<Cart
seats={seats}
total={total}
onCheckoutClicked={() => bookSeats(seats)}
/>
)
}
CartContainer.propTypes = {
seats: PropTypes.arrayOf(PropTypes.shape({
number: PropTypes.number.isRequired,
rowNo: PropTypes.number.isRequired,
price: PropTypes.number.isRequired,
status: PropTypes.string.isRequired
})).isRequired,
total: PropTypes.string,
bookSeats: PropTypes.func.isRequired
}
const mapStateToProps = (state) => ({
seats: getCartSeats(state),
total: getTotal(state)
})

export default connect(mapStateToProps, {bookSeats})(CartContainer)

This container component will be interacting with the store and will pass data to child component—Cart.

Let's understand the code:

  1. mapStateToProps: It is a function that will be called each time the Store is updated which means component is subscribed to store updates.
  2. {bookSeats}: It can be a function or an object that Redux provides so that container can easily pass that function to the child component on its props. We are passing the bookSeats function so that the Checkout button in the Cart component can call it.
  3. connect(): Connects a React component to a Redux store.

Let's see our next container—SeatContainer.

containers/SeatContainer.js:

import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { addSeatToCart } from '../actions'
import SeatRow from '../components/SeatRow'
import SeatList from '../components/SeatList'
import { getAllSeats } from '../reducers/seats';

const SeatContainer = ({ seatrows, addSeatToCart }) => {
return (

<SeatList title="Seats">
{seatrows.map((row, index) =>
<SeatRow key={index}
seats={row.seats}
rowNumber={index}
onAddToCartClicked={addSeatToCart} />

)}

</SeatList>

)
}
SeatContainer.propTypes = {
seatrows: PropTypes.arrayOf(PropTypes.shape({
number: PropTypes.number,
price: PropTypes.number,
status: PropTypes.string,
rowNo: PropTypes.number
})).isRequired,
addSeatToCart: PropTypes.func.isRequired
}

const mapStateToProps = state => ({
seatrows: getAllSeats(state.seats)
})

export default connect(mapStateToProps, { addSeatToCart })(SeatContainer)

As explained earlier for CartContainer, we will have a similar code structure for SeatContainer.

Now, we will create a constants file that defines the constants for our Actions. Though you can directly define constants in the file where you have your action, it is a good practice to define the constants in a separate file as it is much easier to maintain the clean code.

constants/ActionTypeConstants.js:

//get the list of seats
export const GET_SEATS = 'GET_SEATS'
//add seats to cart on selection
export const ADD_TO_CART = 'ADD_TO_CART'
//book seats
export const CHECKOUT = 'CHECKOUT'
We will have three actions:
  • GET_SEATS: To fetch the seats data from Firebase and populate on UI
  • ADD_TO_CART: To add the selected seats in user cart
  • CHECKOUT: Book the seats

Let's define the actions in a file called index.js.

actions/index.js:

import { getSeats,bookSelSeats } from '../api/service';
import { GET_SEATS, ADD_TO_CART, CHECKOUT } from '../constants/ActionTypeConstants';

//action creator for getting seats
const fetchSeats = rows => ({
type: GET_SEATS,
rows
})

//action getAllSeats
export const getAllSeats = () => dispatch => {
getSeats().then(function (rows) {
dispatch(fetchSeats(rows));
});
}

//action creator for add seat to cart
const addToCart = seat => ({
type: ADD_TO_CART,
seat
})

export const addSeatToCart = seat => (dispatch, getState) => {
dispatch(addToCart(seat))

}

export const bookSeats = seats => (dispatch, getState) => {
const { cart } = getState()
bookSelSeats(seats).then(function() {
dispatch({
type: CHECKOUT,
cart
})
});

}

Actions send data from your application to your store using the dispatcher() method. Here, we have two functions:

  • fetchSeats(): It is action creator, which creates the GET_SEATS action
  • getAllSeats(): It is actual action that dispatches data to store, which we get by calling the getSeats() method of our service

Likewise, we can define our actions for the rest of the two actions: ADD_TO_CART and CHECKOUT.

Now, let's see the reducers. We will start with seats reducer.

reducers/seats.js:

import { GET_SEATS } from "../constants/ActionTypeConstants";
import { combineReducers } from 'redux'

const seatRow = (state = {}, action) => {
switch (action.type) {
case GET_SEATS:
return {
...state,
...action.rows.reduce((obj, row) => {
obj[row.id] = row
return obj
}, {})
}
default:
return state
}
}

const rowIds = (state = [], action) => {
switch (action.type) {
case GET_SEATS:
return action.rows.map(row => row.id)
default:
return state
}
}

export default combineReducers({
seatRow,
rowIds
})

export const getRow = (state, number) =>
state.seatRow[number]



export const getAllSeats = state =>
state.rowIds.map(number => getRow(state, number))

Let's understand this piece of code:

  • combineReducers: We have split our Reducer into different functions—rowIds and seatRow—and defined the root reducer as a function that calls the reducers managing different parts of the state and combines them into a single object

Similarly, we will have cart reducer.

reducers/cart.js:

import {
ADD_TO_CART,
CHECKOUT
} from '../constants/ActionTypeConstants'

const initialState = {
addedSeats: []
}

const addedSeats = (state = initialState.addedSeats, action) => {
switch (action.type) {
case ADD_TO_CART:
//if it is already there, remove it from cart
if (state.indexOf(action.seat) !== -1) {
return state.filter(seatobj=>seatobj!=action.seat);
}
return [...state, action.seat]
default:
return state
}
}

export const getAddedSeats = state => state.addedSeats

const cart = (state = initialState, action) => {
switch (action.type) {
case CHECKOUT:
return initialState
default:
return {
addedSeats: addedSeats(state.addedSeats, action)
}
}
}

export default cart

It exposes reducer functions related to cart operations.

Now, we will have a final combine reducer.

reducers/index.js:

import { combineReducers } from 'redux'
import seats from './seats'
import cart, * as cartFunc from './cart'

export default combineReducers({
cart,
seats
})

const getAddedSeats = state => cartFunc.getAddedSeats(state.cart)

export const getTotal = state =>
getAddedSeats(state)
.reduce((total, seat) =>
total + seat.price,
0
)
.toFixed(2)

export const getCartSeats = state =>
getAddedSeats(state).map(seat => ({
...seat
}))

It is a combineReducers for seats and cart. It also exposes some common function to calculate total in cart and to get the seats added in cart. That's it. We have finally introduced Redux to manage our state of the application, and we have our seat booking app ready using React, Redux, and Firebase.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset