Subscribe now

Redux First Router - First Impressions [08.11.2017]

Introduction

Presently, most react apps have route state and data state, and they're disconnected. Redux First Router makes route changes emit actions like any other action in your app. This means you really only have a single state for your application.

Project

Let's create a react app.

create-react-app using-redux-first-router

Then add the dependencies. We need to add redux-first-router and its peer dependency history and the link package from redux-first-router. We will use immutable to use immutable data structures in our state. Also, we can add redux-devtools, because it's awesome for debugging.

yarn add history redux-first-router react-redux redux redux-devtools-extension immutable

We will have two components in our application. A list of products and once we click on a product, we go to the product details.

Store

Let's start our project by setting up our store.

vim src/configureStore.js
import { createStore, applyMiddleware, compose, combineReducers } from "redux";
import { connectRoutes } from "redux-first-router";
import productsReducer from "./reducers/productsReducer";
import createHistory from "history/createBrowserHistory";
import { composeWithDevTools } from "redux-devtools-extension";

const history = createHistory();

const routesMap = {
  HOME: "/", // action <-> url path
  PRODUCT: "/product/:id" // :id is a dynamic segment
};

const { reducer, middleware, enhancer } = connectRoutes(history, routesMap); // yes, 3 redux aspects

// and you already know how the story ends:
const rootReducer = combineReducers({
  location: reducer,
  products: productsReducer
});
const middlewares = applyMiddleware(middleware);

const composeEnhancers = (enhancer, middlewares) =>
  typeof window !== "undefined"
    ? composeWithDevTools(middlewares, enhancer)
    : compose(enhancer, middlewares);

const store = createStore(rootReducer, composeEnhancers(enhancer, middlewares));

export default store;

We are using history from history/createBrowserHistory. That is a library to be used with react-first-router. We use the method connectRoutes from redux-first-router, and we pass the history and the routesMap. The routes map is a JavaScript object where the keys are the action names and the values are the path in the URL. So, these keys can be used in our reducer. We will have two routes, HOME and PRODUCT. After that, we combine the reducers and we apply middlewares, a basic configuration for redux. Here we need to pass the reducer returned from connectRoutes as the key location. This is our store.

Reducer

In our reducer we can handle the actions we had in our routesMap, in our case: 'HOME' and 'PRODUCT'. In our state, we will have something we call Model and our Model, for the moment will just have a productId as an attribute.

In the case of HOME, we will set the productId as undefined, because we don't have productId. We don't need it in the home page. In the case of PRODUCT, we would go to our Product show page, and we can set the product id that we get from the payload.

Redux First Router also provides an action for NOT_FOUND, and we can use it if we don't have a route for it. It dispatches an action called NOT_FOUND, and we can do something here. But, we will do nothing right now.

mkdir src/reducers
vim src/reducers/productsReducer.js
import { NOT_FOUND } from "redux-first-router";
import { Record } from "immutable";

const Model = new Record({
  productId: undefined
});

const init = () => {
  return new Model({ productId: undefined });
};

const productsReducer = (state = init(), action = {}) => {
  switch (action.type) {
    case "HOME":
      return state.setIn(["productId"], undefined);
    case "PRODUCT":
      return state.setIn(["productId"], action.payload.id);
    case NOT_FOUND:
      return null;
    default:
      return state;
  }
};

export default productsReducer;

This is pretty interesting. The same route that is used in the navigation is also here in our reducer, and we can do something when the navigation is fired.

Connecting with our components

We need to get our components working with redux, let's go to our index.js, and connect with our redux store.

vim src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import registerServiceWorker from "./registerServiceWorker";
import store from "./configureStore";
import { Provider } from "react-redux";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
registerServiceWorker();

Here we get our store from our configureStore and use it in our Provider.

App

In our App component we already have redux, so we need to connect. Here the standard connection using our mapStateToProps and mapDispatchToProps. From our state, we will get the productId and related to the actions, we will get the function goToProducts that will change our route to products, and use the id as payload.

const mapStateToProps = state => {
  return {
    productId: state.products.productId
  };
};
const mapDispatchToProps = dispatch => ({
  goToProducts: id => dispatch({ type: "PRODUCT", payload: { id: id } })
});

const AppContainer = connect(mapStateToProps, mapDispatchToProps)(App);

In our App render method, we will have a function called switcher. We pass the props to it. The function switcher will render the correct component based on the props. In our case, our initial component or the product component. Let's see the switcher function.

render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>React First Router Example</h2>
        </div>
        <div className="App-intro">
          {switcher(this.props)}
        </div>
      </div>
    );
  }

Our switcher function checks if we have the productId. If we have the productId, we should render the <Product> component, otherwise the Initial component. Let's see our Product and Initial components.

const switcher = props => {
  const { productId } = props;
  const component = productId
    ? <Product props={props} />
    : <Initial props={props} />;
  return component;
};

Our Initial component will just have a button to go to the Products page.

const Initial = props => {
  const { goToProducts } = props.props;
  return (
    <div>
      <h1> Initial </h1>
      <button onClick={() => goToProducts(1)}>Go To Products</button>
    </div>
  );
};
vim src/Product.js

Our Product component will just have a button and render in the future the product information. Here we will have a goToHome function, and we also connect this component to redux.

import React, { Component } from "react";
import "./App.css";
import { connect } from "react-redux";

class Product extends Component {
  render() {
    const { goToHome } = this.props;
    return (
      <div className="App">
        <h2>Product</h2>
        <p className="App-intro">
          <button onClick={() => goToHome()}>Go To Home</button>
        </p>
      </div>
    );
  }
}

const mapDispatchToProps = dispatch => ({
  goToHome: () => dispatch({ type: "HOME" })
});

const ProductContainer = connect(null, mapDispatchToProps)(Product);

export default ProductContainer;

Ok, so these are the things we need to render our two components and have navigation between them.

import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";
import { connect } from "react-redux";
import Product from "./Product";

const Initial = props => {
  const { goToProducts } = props.props;
  return (
    <div>
      <h1> Initial </h1>
      <button onClick={() => goToProducts(1)}>Go To Products</button>
    </div>
  );
};

const switcher = props => {
  const { productId } = props;
  const component = productId
    ? <Product props={props} />
    : <Initial props={props} />;
  return component;
};

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>React First Router Example</h2>
        </div>
        <div className="App-intro">
          {switcher(this.props)}
        </div>
      </div>
    );
  }
}

const mapStateToProps = state => {
  return {
    productId: state.products.productId
  };
};
const mapDispatchToProps = dispatch => ({
  goToProducts: id => dispatch({ type: "PRODUCT", payload: { id: id } })
});

const AppContainer = connect(mapStateToProps, mapDispatchToProps)(App);

export default AppContainer;

Let's test it out! Running our server, let's see the navigation, we can also open the redux dev tools and see it working.

Here is our initial component. We can go to the product page, we might have a list of products here, but for the moment we just have a button simulating this behaviour.

Going to the product page, the URL changes and the action is dispatched. This is awesome! Coming back to the initial page, the action is dispatched and the URL changes. Here we can see the two things together, our routing and our redux actions.

Summary

Today we took a look at Redux First Router. They say it solves 80% of the cases in redux applications. We saw how to setup and use it. In future episodes, we will dive even deeper with more details.

Resources