Skip to content
This repository has been archived by the owner on Nov 24, 2022. It is now read-only.

Tutorial 3 (Customize)

Nick Dreckshage edited this page Jan 23, 2016 · 8 revisions

This tutorial builds off the [previous](Tutorial 2 (Organize)), and add fancy reducers, immutable data, and handle data-fetching redirects / errors.

Combine Reducers

When a child route changes, application state updates, clearing out state at the corresponding level in the nested reducer hierarchy. However, parent route state is maintained, making this an easy place to store data that should persist.

Let's add a currentUser reducer at the application level. But rather than that being the only thing on application reducer, lets namespace it with combinedReducers.

In components/AppComponent.js:

// ...
import { IndexLink, Link } from 'react-router';
import { combineReducers } from 'redux';

const initialCurrentUserState = { name: 'Nick' };
const currentUserReducer = (state = initialCurrentUserState, action) => {
  return state;
};

export const reducer = combineReducers({
  currentUser: currentUserReducer
});

// ...

<div style={{border:'1px solid green'}}>
  <p>App: {this.props.data.currentUser.name}</p>
// ...

In routes.js:

// ...
import { component as AppComponent, reducer as appReducer } from './components/AppComponent';

// ...

export default (
  <Route path="/" component={AppComponent} reducer={appReducer}>
//...

Accessing parent data.

We'd like to be able to see currentUser in a childRoute as well. In order to accomplish this, we need to use context.

In components/AppComponent.js:

// ...
export const component = React.createClass({
  childContextTypes: {
    currentUser: React.PropTypes.object
  },

  getChildContext: function() {
    return {
      currentUser: this.props.data.currentUser
    };
  },
  
  render() {
// ...

In components/AsyncComponent.js:

// ...
export const component = React.createClass({
  contextTypes: {
    currentUser: React.PropTypes.object
  },

  render() {
    const items = this.props.loading ? ['x', 'y', 'z'] : this.props.data.items;
    const user = this.context.currentUser.name;
    return (
      <div>
        <p>Async Component (User: {user}) ({this.props.loading ? 'Loading...' : 'Loaded!'})</p>
// ...

Fancy reducers

Let's make it so we can update the current user name. But theres some boilerplate with Redux. So let's minimize it with our helper lib of preference. I prefer actions that create a unique id, so you don't have to worry about reducer action collisions. Then, lets update that currentUser name in a deeply nested component.

First, install redux-act:

npm i --save-dev redux-act

Then, in components/AppComponent.js:

// ...
import { combineReducers } from 'redux';
import { createReducer, createAction } from 'redux-act';

export const actions = {
  update: createAction()
};

const initialCurrentUserState = { name: 'Nick' };
const currentUserReducer = createReducer({
  [actions.update]: (state, payload) => {
    return { name: payload };
  }
}, initialCurrentUserState);
// ...

And in components/AsyncComponent2.js:

import React from 'react';
import { actions as rootActions } from './AppComponent';

export const reducer = (state = 'nested-route-1') => state;

class component extends React.Component {
  render() {
    const { dispatch } = this.props;
    const user = this.context.currentUser.name;
    return (
      <div>
        <p>nested-route-1</p>
        <input type='text' value={user} onChange={(e) => {
          dispatch(rootActions.update(e.target.value));
        }} />
      </div>
    );
  }
}

component.contextTypes = {
  currentUser: React.PropTypes.object
};

export { component };

Immutable data

We are persisting data at application level, and can interact with it throughout our route components. But let's take it a step further and deal with immutable data in reducers.

First, install immutable.js:

npm i --save-dev immutable

Then in components/AppComponent.js

// ...
import { createReducer, createAction } from 'redux-act';
import { Map } from 'immutable';

// ...

const initialCurrentUserState = Map({ name: 'Nick' });
const currentUserReducer = createReducer({
  [actions.update]: (state, payload) => {
    return state.set('name', payload);
  }
}, initialCurrentUserState);

// ...

Oops!

Our current user name not showing up! This is because we'd have to access currentUser.name as currentUser.get('name') in each component. You can do this if you'd like. But, it's helpful to add a serializer, and just deal with POJO's in components (so components don't have to care about reducer data API / getters).

In component/AppComponent.js,

// ...
export const serializer = data => {
  return {
    ...data,
    currentUser: data.currentUser.toJS()
  };
};

export const component = React.createClass({
// ...

And add that serializer to your route in routes.js,

// ...
import { component as AppComponent, reducer as appReducer, serializer as appSerializer } from './components/AppComponent';

// ...

export default (
  <Route path="/" component={AppComponent} reducer={appReducer} serializer={appSerializer}>
// ...

Use immutable a lot?

Let's add an app-level serializer.

First, in components/AsyncComponent3.js:

import React from 'react';
import { Map } from 'immutable';
export const reducer = (state = Map({ content: 'nested-route-2' })) => state;
export const component = ({ data }) => <p>{data.content}</p>;

Then, in routes.js, lets define a custom immutable property on the route.

// ...
<Route path="/async-route/async-nested" component={AsyncComponent3} reducer={asyncReducer3} immutable={true} />
// ...

And set our app-level serializer in client.js:

// ...
const serializer = (route, data) => {
  if (route.immutable) return data.toJS();
  return data;
};

render((
  <Router routes={routes} history={browserHistory} render={(props) => (
    <GroundControl {...props} store={store} serializer={serializer} />
// ...

Error Handling

React router has a nice way of handling redirects, and not found routes at router level. But how do we handle it when we fetch data? Add fetchData and handle an error state for /async-route/async-nested.

In components/AsyncComponent3.js:

// ...

export const fetchData = (done, { err }) => {
  setTimeout(() => {
    err({ message: 'Oops!' });
  }, 1000);
};

export const reducer = (state = Map({ content: 'nested-route-2' })) => state;
export const component = ({ data, loadingError }) => {
  return (
    <div>
      {loadingError ? <p>{loadingError.message}</p> : <p>{data.content}</p>}
    </div>
  );
};

In routes.js:

// ...
import { component as AsyncComponent3, reducer as asyncReducer3, fetchData as async3FetchData } from './components/AsyncComponent3';
// ...
<Route path="/async-route/async-nested" component={AsyncComponent3} reducer={asyncReducer3} immutable={true} asyncEnter={async3FetchData} />
// ...

Redirect

Let's redirect instead of showing an error.

In routes.js:

//...
export const fetchData = (done, { redirect }) => {
  setTimeout(() => {
    redirect({ pathname: '/' });
  }, 1000);
};
// ...

Done!

That was a good bit of work but thanks for bearing with it!

You...

  • added structure to large reducers with combineReducers.
  • accessed root application data throughout deeply nested routes.
  • customized the app with however we'd like to create reducers / actions.
  • added a library like immutable.js and serialized that data into components.
  • used a bit more of the fetchData API to handle redirects & errors.

Next, lets [make this a universal app!](Tutorial 4 (Universal))