Adding Redux to the React Native TaskList App

In my last blog post, I created the TaskList app and it worked, despite a couple of bugs. I want to convert it to use the Flux architecture. This architecture is prevalent in the React space and basically says there is one source of truth (the store) for the app. The most well-known flux implementation doesn’t technically implement flux, but it still stays true to the tenets of Flux. It’s called Redux and I’ve written about it for web apps before. Switching my TaskList app to Redux will enable me to switch the whole app over to a cloud-enabled app much more easily later on. Note that you don’t have to implement Redux in order to store the data in the cloud. It just makes it easier.

With that, let’s take a look at what I will need to do.

  1. Create the redux store, reducers and action creators.
  2. Move the main app into a component and wire it up to redux.
  3. Create a new main app that wraps the component in a Redux store.

Three parts and all are fairly straight forward.

Create the Store, Reducers and Action Creators

If you look at the main app, you will note that I have two distinct parts – the modal manager and the list of items. I think I want to leave the modal manager with the main app, so that leaves the list of items in the store. Create a directory application/redux to store everything. Let’s put the store in application/redux/store.js with the following boilerplate code:

import { createStore } from 'redux';
import reducers from './reducers';

let store = createStore(reducers);

export default store;

I love boilerplate. The Redux team have done a great job of making this stuff simple. Now, on to the action creators. Create a new directory application/redux/actions and edit the file tasks.js within that directory with the following code:

export function saveItem(item) {
    return {
        type: 'tasks-save-item',
        item: item
    };
}

export function deleteItem(id) {
    return {
        type: 'tasks-delete-item',
        id: id
    };
}

export function completeItem(id, flag) {
    return {
        type: 'tasks-complete-item',
        id: id,
        flag: flag
    };
}

There is one function for each activity on the store:

  • Save item
  • Delete item
  • Complete item

Each one returns something that is dispatched to the store eventually. The reducers handle these actions. My reducers for these actions are placed in application/redux/reducers/tasks.js:

const defaultState = [
    { id: '1', text: 'First Item', completed: false },
    { id: '2', text: 'Second Item', completed: true }
];

export default function tasksReducer (state = defaultState, action) {
    switch (action.type) {
        case 'tasks-save-item':
            let idx = state.findIndex(t => t.id === action.item.id);
            if (idx < 0) {
                return [ ...state, action.item ];
            }
            return state.map((t, i) => i === idx ? action.item : t);

        case 'tasks-delete-item':
            return state.filter(t => t.id !== action.id);

        case 'tasks-complete-item':
            return state.map(item => {
                if (item.id === action.id) {
                    item.completed = action.flag;
                }
                return item;
            });

        default:
            return state;
    }
}

Each case statement is based on the original code I wrote last time. It’s just been placed in a reducer instead. The important fact about the reducer is that you never CHANGE state. You return the new state without adjusting the old one. A reducer is said to be idempotent – you can run the same function with the same state and the same action and it will always producer the same output. This is great for testing. Note that my store imports reducers, and not individual reducers. Basically, Redux only deals with one root reducer (which can be a combination of many reducers), so if you want to deal with more, you need to combine them. Here is application/redux/reducers/index.js that does exactly that:

import { combineReducers } from 'redux';
import tasks from './tasks';

const reducers = combineReducers({
    tasks
});

export default reducers;

Again – this is boilerplate. When I am writing my redux implementation, I’m really only writing the reducer and the action creators. Everything else is boilerplate.

Move the Main App into a Component

The React bindings for redux are also a solid library. In essence, any component can get a view of the store by writing two methods (mapStateToProps and mapDispatchToProps) and then call connect to wrap the component in the redux store. To assist with that, I’ve created a new component called application/TaskListApp.js that does this. I’ve removed all the state handling and delegated that to the redux implementation. I’ve highlighted the lines that have changed here:

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Alert, Modal, StyleSheet, View } from 'react-native';
import ActionButton from 'react-native-action-button';
import * as taskActions from './redux/actions/tasks';
import ViewItem from './components/ViewItem';
import TaskList from './components/TaskList';

class Application extends Component {
  constructor (props) {
    super(props);

    this.state = {
      modalVisible: false,
      modalItem: {}
    }
  }

  onSaveItem (item) {
      this.props.saveItem(item);
      this.setState({ modalVisible: false });
  }

  onDeleteItem (id) {
      this.props.deleteItem(id);
  }

  onCompleteItem (id, flag) {
      this.props.completeItem(id, flag);
  }

  onSelectItem (id) {
    this.setState({
      modalItem: this.props.items.find(item => item.id === id),
      modalVisible: true
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <Modal animationType="slide" transparent={false} visible={this.state.modalVisible} onRequestClose={() => this.setState({ modalVisible: false })}>
          <ViewItem
            item={this.state.modalItem}
            onSaveItem={(item) => this.onSaveItem(item)}
            onCancel={() => this.setState({ modalVisible: false })} />
        </Modal>
        <TaskList
          items={this.props.items}
          onCompleteItem={(id, flag) => this.onCompleteItem(id, flag)}
          onDeleteItem={(id) => this.onDeleteItem(id)}
          onSelectItem={(id) => this.onSelectItem(id)}/>
        <ActionButton
          buttonColor="#9b59b6"
          onPress={() => this.setState({ modalItem: { id: null, text: '', completed: false }, modalVisible: true })} />
      </View>
    );
  }
}

Application.propTypes = {
    items: PropTypes.arrayOf(PropTypes.shape({
        id: PropTypes.string.isRequired,
        text: PropTypes.string.isRequired,
        completed: PropTypes.bool.isRequired
    })).isRequired,
    saveItem: PropTypes.func.isRequired,
    deleteItem: PropTypes.func.isRequired,
    completeItem: PropTypes.func.isRequired
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',
    alignItems: 'flex-start',
    backgroundColor: '#F5FCFF',
    marginTop: 22
  }
});

const mapStateToProps = (state, ownProps) => {
    return {
        items: state.tasks
    };
}

const mapDispatchToProps = (dispatch) => {
    return {
        saveItem: (item) => { dispatch(taskActions.saveItem(item)); },
        deleteItem: (id) => { dispatch(taskActions.deleteItem(id)); },
        completeItem: (id, flag) => { dispatch(taskActions.completeItem(id, flag)); }
    };
}

export default connect(mapStateToProps, mapDispatchToProps)(Application);

A summary of the changes:

  • Line 5 brings in the action creators for the tasks.
  • Lines 13-16 are a reduced footprint for state – only the modal controls are in the state now – the items have been moved to the store.
  • Lines 20, 25 and 29 call the dispatch methods to dispatch the action to the redux store instead of directly updating state.
  • Line 49 adjusted the location of the items – it used to be in the component state and now its in the props passed in from redux.
  • Lines 61-70 are the definition of the props that are linked to redux. There were no props before because everything was in state.
  • Lines 82-96 actually connect the component properties to the redux store, doing the appropriate mapping.

Create the Main App

The old main app consisted of the component that I just created. The only thing I need to do now is to set up redux, hook up a store provider and then bring in the new app component:

// This is application/index.js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import store from './redux/store';
import TaskListApp from './TaskListApp';

class Application extends Component {
  render() {
    return (
      <Provider store={store}>
        <TaskListApp />
      </Provider>
    );
  }
}

export default Application;

Don’t forget to also add the required libraries to the project:

yarn add redux react-redux 

Wrap Up

It’s amazing how much easier it is to write and understand Redux now. This stage of the project took me less than 30 minutes. It’s easy to see why Redux is the preferred implementation for Flux. However, there are others out there. The one I hear about most often as an alternative is MobX, which gets rid of the reducers – actions mutate state directly. If I were choosing one for a commercial project, then I’d likely try to implement a simple async app with network activity (like this one will be) with each one and decide which is better for my situation. However, for my personal projects, I like the simplicity of Redux.

Next time, I’m going to deal with async actions and hook up my TaskList app to a store in the cloud. Until then, you can find the code for this project on my GitHub Repository.

One thought

  1. Pingback: Dew Drop - May 23, 2017 (#2485) - Morning Dew

Comments are closed.