How to use Redux in React Firebase

 by Robin Wieruch
 - Edit this Post

Interested in reading this tutorial as one of many chapters in my advanced React with Firebase book? Checkout the entire The Road to Firebase book that teaches you to create business web applications without the need to create a backend application with a database yourself.

The previous tutorial series covered a lot of ground for Firebase in React. So far, it was fine to rely only on React's local state and React's Context API. This tutorial dives into using Redux on top of React and Firebase for state management. You will exchange React's local state (e.g. users on admin page, messages on home page) and React's context (e.g. session management for authenticated user) with Redux. It will show you how to accomplish the same thing with Redux in case you want to integrate it into a tech stack.

This section is divided into two parts, the first of which will set up Redux. You will add the state layer separately from the view layer. Afterward, you will connect Redux with React by providing the Redux store with React's Context API to your React components. The second part exchanges the current React state layer with the Redux state layer:

  • Authenticated User in React Local State + React Context -> Authenticated User in Redux Store.
  • Users in React Local State -> Users in Redux Store.
  • Messages in React Local State -> Messages in Redux Store.

If you are not familiar with Redux, I recommend to check out The Road to Redux. Most of the Redux knowledge about Actions, Reducers, and the Store are required for the following migration from only using React to Redux.

Redux Setup in React Firebase Application

Let's get started by installing redux and react-redux on the command line:

npm install redux react-redux

We focus on the Redux setup without worrying about Firebase or React. First is the Redux store implementation. Create a folder and file for it using the src/ folder type:

mkdir store
cd store
touch index.js

Second, add the store in the new file as singleton instance, because there should be only one Redux store. The store creation takes a root reducer which isn't defined.

import { createStore } from 'redux';
import rootReducer from '../reducers';
const store = createStore(rootReducer);
export default store;

Third, create a dedicated module for the reducers. There's a reducer for the session state (e.g. authenticated user) and reducers for the user and message states (e.g. list of users and messages from the Firebase realtime database). There's an entry point file to the module to combine those reducers as root reducer to pass it to the Redux store, like the previous step. Again, from your src/ folder type:

mkdir reducers
cd reducers
touch index.js session.js user.js message.js

First, add the session reducer which manages the authUser object. The authenticated user represents the session in the application. The reducer deals only with one incoming action which either sets the authUser to the actual authenticated user or null:

const INITIAL_STATE = {
authUser: null,
};
const applySetAuthUser = (state, action) => ({
...state,
authUser: action.authUser,
});
function sessionReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case 'AUTH_USER_SET': {
return applySetAuthUser(state, action);
}
default:
return state;
}
}
export default sessionReducer;

The user reducer deals with the list of users from the Firebase realtime database. It sets either the whole object of users as dictionary, or a single user identified by a unique identifier:

const INITIAL_STATE = {
users: null,
};
const applySetUsers = (state, action) => ({
...state,
users: action.users,
});
const applySetUser = (state, action) => ({
...state,
users: {
...state.users,
[action.uid]: action.user,
},
});
function userReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case 'USERS_SET': {
return applySetUsers(state, action);
}
case 'USER_SET': {
return applySetUser(state, action);
}
default:
return state;
}
}
export default userReducer;

The message reducer deals with the list of messages from the Firebase realtime database. Again, it sets the whole object of messages as dictionary, but also a limit for the pagination feature we implemented earlier:

const INITIAL_STATE = {
messages: null,
limit: 5,
};
const applySetMessages = (state, action) => ({
...state,
messages: action.messages,
});
const applySetMessagesLimit = (state, action) => ({
...state,
limit: action.limit,
});
function messageReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case 'MESSAGES_SET': {
return applySetMessages(state, action);
}
case 'MESSAGES_LIMIT_SET': {
return applySetMessagesLimit(state, action);
}
default:
return state;
}
}
export default messageReducer;

Finally, combine all reducers into a root reducer in the src/reducers/index.js file to make it accessible for the store creation:

import { combineReducers } from 'redux';
import sessionReducer from './session';
import userReducer from './user';
import messageReducer from './message';
const rootReducer = combineReducers({
sessionState: sessionReducer,
userState: userReducer,
messageState: messageReducer,
});
export default rootReducer;

You have passed the root reducer with all its reducers to the Redux store creation, so the Redux setup is done. Now you can connect your state layer with your view layer. The Redux store can be provided for the component hierarchy using Redux's Provider component. This time, the Provider component from the Redux library passes down the whole store instead of only the authenticated user. In src/index.js file:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './components/App';
import Firebase, { FirebaseContext } from './components/Firebase';
ReactDOM.render(
<Provider store={store}>
<FirebaseContext.Provider value={new Firebase()}>
<App />
</FirebaseContext.Provider>
</Provider>,
document.getElementById('root'),
);

That's it for connecting both worlds, so we'll refactor almost everything from React's local state to Redux. We want to have everything in the Redux store that should be persisted when we navigate from route to route. This includes users, messages, and the authenticated user, but maybe not the loading states.

Exercises:

Manage Firebase's authenticated User in Redux Store

We are managing the authenticated user with React's Context API. We provide the authenticated user in a Provider component and consume it wherever we want with a Consumer component. Let's change this by storing the authenticated user in the Redux store instead and connecting all components that are interested in it to the Redux store. In the authentication higher-order component, we make the dispatchable action that stores the authenticated user in the Redux store, which is available as function in the props of the connected component:

import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { withFirebase } from '../Firebase';
const withAuthentication = Component => {
class WithAuthentication extends React.Component {
...
}
const mapDispatchToProps = dispatch => ({
onSetAuthUser: authUser =>
dispatch({ type: 'AUTH_USER_SET', authUser }),
});
return compose(
withFirebase,
connect(
null,
mapDispatchToProps,
),
)(WithAuthentication);
};
export default withAuthentication;

Next, use the function to set the authenticated user in the Redux store by setting it to React's local state like before. We don't need to provide the authenticated user anymore with React's Context Provider component, because it will be available for every component that connects to the store:

const withAuthentication = Component => {
class WithAuthentication extends React.Component {
constructor(props) {
super(props);
this.props.onSetAuthUser(
JSON.parse(localStorage.getItem('authUser')),
);
}
componentDidMount() {
this.listener = this.props.firebase.onAuthUserListener(
authUser => {
localStorage.setItem('authUser', JSON.stringify(authUser));
this.props.onSetAuthUser(authUser);
},
() => {
localStorage.removeItem('authUser');
this.props.onSetAuthUser(null);
},
);
}
componentWillUnmount() {
this.listener();
}
render() {
return <Component {...this.props} />;
}
}
...
};
export default withAuthentication;

That's it for storing and providing the authenticated user for the Redux store. Let's see how we can consume it in the Navigation component for the conditional rendering of the routes without React's Context, and with the Redux store instead:

import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import SignOutButton from '../SignOut';
import * as ROUTES from '../../constants/routes';
import * as ROLES from '../../constants/roles';
const Navigation = ({ authUser }) =>
authUser ? (
<NavigationAuth authUser={authUser} />
) : (
<NavigationNonAuth />
);
...
const mapStateToProps = state => ({
authUser: state.sessionState.authUser,
});
export default connect(mapStateToProps)(Navigation);

We can do the same in our other components that are interested in the authenticated user. For instance, the authorization higher-order component can rely on the Redux store as well:

import React from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { withFirebase } from '../Firebase';
import * as ROUTES from '../../constants/routes';
const withAuthorization = condition => Component => {
class WithAuthorization extends React.Component {
...
render() {
return condition(this.props.authUser) ? (
<Component {...this.props} />
) : null;
}
}
const mapStateToProps = state => ({
authUser: state.sessionState.authUser,
});
return compose(
withRouter,
withFirebase,
connect(mapStateToProps),
)(WithAuthorization);
};
export default withAuthorization;

Also, our other authorization component for the email verification can make use of it:

import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { withFirebase } from '../Firebase';
...
const withEmailVerification = Component => {
class WithEmailVerification extends React.Component {
...
render() {
return needsEmailVerification(this.props.authUser) ? ( ... ) : (
<Component {...this.props} />
);
}
}
const mapStateToProps = state => ({
authUser: state.sessionState.authUser,
});
return compose(
withFirebase,
connect(mapStateToProps),
)(WithEmailVerification);
};
export default withEmailVerification;

And last but not least, the AccountPage component which displays the authenticated user but also renders the component that manages all the sign in methods for the user:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { withAuthorization, withEmailVerification } from '../Session';
import { withFirebase } from '../Firebase';
import { PasswordForgetForm } from '../PasswordForget';
import PasswordChangeForm from '../PasswordChange';
...
const AccountPage = ({ authUser }) => (
<div>
<h1>Account: {authUser.email}</h1>
<PasswordForgetForm />
<PasswordChangeForm />
<LoginManagement authUser={authUser} />
</div>
);
...
const mapStateToProps = state => ({
authUser: state.sessionState.authUser,
});
const condition = authUser => !!authUser;
export default compose(
connect(mapStateToProps),
withEmailVerification,
withAuthorization(condition),
)(AccountPage);

Now you can remove the React Context for providing and consuming the authenticated user in the src/components/Session/context.js and src/components/Session/index.js files:

import withAuthentication from './withAuthentication';
import withAuthorization from './withAuthorization';
import withEmailVerification from './withEmailVerification';
export {
withAuthentication,
withAuthorization,
withEmailVerification,
};

That's it for storing the authenticated user in the Redux store, which takes place in the authentication higher-order component and for consuming the authenticated user in every component which is interested in it by connecting the Redux store.

Exercises:

Manage Firebase's Users in Redux Store

We implemented the session management with the authenticated user with Redux instead of React's local state and context API. Next, we will migrate the user management over to Redux. The users are mainly used in the AdminPage component's UserList and UserItem components. Our goal here is to navigate from UserList to UserItem and back with React Router without losing the state of the users. The UserList component fetches and shows a list of users, while the UserItem component fetches and shows a single user entity. If the data is already available in the Redux store, we only keep track of new data with the realtime feature of the Firebase database, starting with the UserList component:

import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { withFirebase } from '../Firebase';
import * as ROUTES from '../../constants/routes';
class UserList extends Component {
...
}
const mapStateToProps = state => ({
users: Object.keys(state.userState.users || {}).map(key => ({
...state.userState.users[key],
uid: key,
})),
});
const mapDispatchToProps = dispatch => ({
onSetUsers: users => dispatch({ type: 'USERS_SET', users }),
});
export default compose(
withFirebase,
connect(
mapStateToProps,
mapDispatchToProps,
),
)(UserList);

React Redux's connect higher-order component is used to marry React with Redux. We can tell what state of Redux should be mapped to props for the React component in the mapStateToProps function, and we can pass dispatchable Redux actions as functions to the React component as props with the mapDispatchToProps function. In our case, we are interested in a user object that encapsulates all users in the Redux store. We transform this user object--which is the Firebase representation of all users--into an array, to make it easier for us to render them. The point is to dispatch an action that sets the user object as state in the Redux store. Check t he src/reducers/user.js to see how our reducer deals with this action. Both users and onSetUsers are received as props in the UserList component.

Next, make sure the users are fetched from Firebase's realtime database and persisted in the Redux store with our new dispatchable action:

class UserList extends Component {
componentDidMount() {
this.props.firebase.users().on('value', snapshot => {
this.props.onSetUsers(snapshot.val());
});
}
componentWillUnmount() {
this.props.firebase.users().off();
}
...
}

Each time the Firebase listener is called, or when a user was added, edited, or removed from the list, the most recent user object that has all users from Firebase is stored with the onSetUsers() function to the Redux store. Another UX improvement is the loading indicator when there are no users in the Redux store. Every other time, when there are users in the store but the Firebase listener is updating the Redux store with a new user object, no loading indicator is shown:

class UserList extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
};
}
componentDidMount() {
if (!this.props.users.length) {
this.setState({ loading: true });
}
this.props.firebase.users().on('value', snapshot => {
this.props.onSetUsers(snapshot.val());
this.setState({ loading: false });
});
}
...
}

The users are no longer managed in the local state of the component, but are now handled in Redux. You set the users with a dispatchable action from mapDispatchToProps and access them again in mapStateToProps. Both state and actions are passed as props to your component.

The users and loading indicator are rendered as before, but only the loading state comes from the local state. The Link component only navigates to the UserItem component, but it doesn't send any user objects. We wanted the user at our disposal via the Link component, and we want to let Redux handle it.

class UserList extends Component {
render() {
const { users } = this.props;
const { loading } = this.state;
return (
<div>
<h2>Users</h2>
{loading && <div>Loading ...</div>}
<ul>
{users.map(user => (
<li key={user.uid}>
<span>
<strong>ID:</strong> {user.uid}
</span>
<span>
<strong>E-Mail:</strong> {user.email}
</span>
<span>
<strong>Username:</strong> {user.username}
</span>
<span>
<Link to={`${ROUTES.ADMIN}/${user.uid}`}>
Details
</Link>
</span>
</li>
))}
</ul>
</div>
);
}
}

The UserList component renders a list of users as before, fetches the recent user object, which has all users, from Firebase with a realtime connection, but stores the result into the Redux store this time instead of React's local state. Let's continue with the UserItem component that shall be connected to the Redux store too:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { withFirebase } from '../Firebase';
class UserItem extends Component {
...
}
const mapStateToProps = (state, props) => ({
user: (state.userState.users || {})[props.match.params.id],
});
const mapDispatchToProps = dispatch => ({
onSetUser: (user, uid) => dispatch({ type: 'USER_SET', user, uid }),
});
export default compose(
withFirebase,
connect(
mapStateToProps,
mapDispatchToProps,
),
)(UserItem);

Similar to the UserList component, it receives a function that is a dispatchable action that sets a user instead of users. Check the user reducer to see what's happening when this action is dispatched. The component receives a single user from the Redux store. Because the initial state of users in the Redux store is null, we have to conditionally choose an empty object for not running into null pointer exceptions.

Let's make sure the user is fetched from Firebase's realtime database and persisted in the Redux store with our new dispatchable action.

class UserItem extends Component {
componentDidMount() {
this.props.firebase
.user(this.props.match.params.id)
.on('value', snapshot => {
this.props.onSetUser(
snapshot.val(),
this.props.match.params.id,
);
});
}
...
}

When the component mounts, we register Firebase's listener to the realtime database. Every time the user changes, we update it in the Redux store. If there is already a user, we don't show a loading indicator. If there is no user, we show it:

class UserItem extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
};
}
componentDidMount() {
if (!this.props.user) {
this.setState({ loading: true });
}
this.props.firebase
.user(this.props.match.params.id)
.on('value', snapshot => {
this.props.onSetUser(
snapshot.val(),
this.props.match.params.id,
);
this.setState({ loading: false });
});
}
...
}

We render the user and loading state as before, except the user comes from the props:

class UserItem extends Component {
onSendPasswordResetEmail = () => {
this.props.firebase.doPasswordReset(this.props.user.email);
};
render() {
const { user } = this.props;
const { loading } = this.state;
return (
<div>
<h2>User ({this.props.match.params.id})</h2>
{loading && <div>Loading ...</div>}
{user && ( ... )}
</div>
);
}
}

That's it for the UserItem component. It renders a user, fetches the recent user from Firebase with a realtime connection, but stores the result into the Redux store. The advantage of using Redux instead of React's local state is a persistent state of users between routes. That means you don't need to fetch the users every time you navigate from UserItem to UserList or any other route, because they remain in Redux's global state.

Exercises:

Manage Message Entities in Redux Store

We migrated the users and session management from React's local state and React's Context as well, which is why we have refactored the session management. What's missing is connecting the Messages component to the Redux store too in order to store and get messages in and from the Redux state:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { withFirebase } from '../Firebase';
import MessageList from './MessageList';
class Messages extends Component {
...
}
const mapStateToProps = state => ({
authUser: state.sessionState.authUser,
messages: Object.keys(state.messageState.messages || {}).map(
key => ({
...state.messageState.messages[key],
uid: key,
}),
),
limit: state.messageState.limit,
});
const mapDispatchToProps = dispatch => ({
onSetMessages: messages =>
dispatch({ type: 'MESSAGES_SET', messages }),
onSetMessagesLimit: limit =>
dispatch({ type: 'MESSAGES_LIMIT_SET', limit }),
});
export default compose(
withFirebase,
connect(
mapStateToProps,
mapDispatchToProps,
),
)(Messages);

The Messages component has access to the authenticated user, which is used for associating the user to the written message, the transformed list of messages and the limit integer for the pagination feature. Also it receives functions for dispatching actions that set messages and the limit. Check the src/reducers/message.js file again to see how the state and the actions are used there. Because we are using lots of things from the Redux store, only a couple of properties are left in the local state of the component:

class Messages extends Component {
constructor(props) {
super(props);
this.state = {
text: '',
loading: false,
};
}
...
}

The Messages component only deals with the loading indicator and the text for the message that can be written from within the component as local state. Everything else will be managed with Redux.

class Messages extends Component {
...
componentDidMount() {
if (!this.props.messages.length) {
this.setState({ loading: true });
}
this.onListenForMessages();
}
onListenForMessages = () => {
this.props.firebase
.messages()
.orderByChild('createdAt')
.limitToLast(this.props.limit)
.on('value', snapshot => {
this.props.onSetMessages(snapshot.val());
this.setState({ loading: false });
});
};
componentWillUnmount() {
this.props.firebase.messages().off();
}
...
}

The other logic for creating, updating, and removing a message stays the same, because it only uses the Firebase API. The listener of the Firebase database makes sure to update all messages in the Redux store again. Only the class method to update the limit uses the function provided by the Redux store:

class Messages extends Component {
...
onNextPage = () => {
this.props.onSetMessagesLimit(this.props.limit + 5);
};
...
}

Every time this state in the Redux store changes, we receive the new limit in the Messages component as props due to the connect higher-order component. If the limit changes, we register a new Firebase listener with the new limit:

class Messages extends Component {
...
componentDidUpdate(props) {
if (props.limit !== this.props.limit) {
this.onListenForMessages();
}
}
...
}

The rendering of the component didn't change a lot. It only receives the messages from the props instead of the local state.

class Messages extends Component {
...
render() {
const { messages } = this.props;
const { text, loading } = this.state;
return (
<div>
{!loading && messages && (
<button type="button" onClick={this.onNextPage}>
More
</button>
)}
{loading && <div>Loading ...</div>}
{messages && (
<MessageList ... />
)}
{!messages && <div>There are no messages ...</div>}
<form
onSubmit={event =>
this.onCreateMessage(event, this.props.authUser)
}
>
<input
type="text"
value={text}
onChange={this.onChangeText}
/>
<button type="submit">Send</button>
</form>
</div>
);
}
}

The MessageList and MessageItem components didn't change at all, and only the HomePage and the Messages components were connected to the Redux store. They receive the necessary functions to update the state in the Redux store, but also the state as props such as users, messages, and limit. Every time a Firebase listener that receives the latest entities from the Firebase database is called, it stores the result in the Redux store. It happens as well when a user creates, edits, or deletes a message. If the limit for the pagination feature changes, the listener is registered with this new limit again. Everything else, such as the text of the new message or the loading indicator, is still managed in the local state of React.

Exercises:


That's it for the React Firebase with Redux tutorial. You have introduced Redux as state management library to manage your session, user, and message state. Instead of relying on React's context API for the authenticated user object and React's local state for the list of users and messages from the Firebase database, you are storing these objects in the Redux store. You can find the project in this GitHub repository.

Keep reading about 

So far, it was fine to rely only on React's local state and React's Context API. This tutorial dives into using MobX on top of React and Firebase for the state management. We'll exchange React's local…

React's Function Components come with React Hooks these days. Not only can React Hooks be used for State in React (e.g. useState and useReducer ) but also for consuming React's Context…

The Road to React

Learn React by building real world applications. No setup configuration. No tooling. Plain React in 200+ pages of learning material. Learn React like 50.000+ readers.

Get it on Amazon.