DEV Community

Eder Sanchez
Eder Sanchez

Posted on

Migrating a large Flux app to Redux without everything falling apart

react to redux image

This post was originally published in GumGum's tech blog.


We are big fans of React at GumGum. In fact, most of our apps have been built with it and almost all of them use Redux as well.

However, it wasn't always like this. One of our first applications was built with Flux and while it works perfectly fine, there is some degree of context switching fatigue, especially for engineers working on newer Redux apps. Also, being unfamiliar with Flux can allow for some bugs when the store is read on component mount but not updated again afterwards. Since Redux passes the state as props, we can be sure that the information we're reading from the store is always up to date. And last but not least, Redux's implementation is only cumbersome the first time (as we will see in the next sections), while Flux requires adding a store listener to components as well as ensuring removal of said listener when unmounting the component.

This application is widely used internally and also by some of our clients, so trying to migrate it all at once would be quite the challenge. Doing it in one go would also require a lot of coding hours that would prevent us from developing new features (and an awful Pull Request for anyone to review). So, we decided to migrate the app slowly, whenever there's free time from the usual new features and paying technical debt.

If you are like me and remained confused on how to migrate from Flux to Redux after reading the Redux documentation, you have come to the right place to learn how to do it.

This approach will help you migrate a section of a React app to Redux reducers and actions, while other sections still use your old Flux store.


Prerequisites

There are some libraries that make using Redux with React much easier, so let's go ahead and install them. These will probably be different depending on your project's structure, and some may not even be needed.

In our example application, we use react-router, so we will need to connect the router props to pass them along with the store. This can be accomplished by using the react-router-redux middleware (we're using react-router v3, so if your project uses v4, use connected-react-router instead).

To easily connect React to Redux we will use the react-redux middleware, and of course, we will need Redux as well.

Finally, our Flux stores perform many requests to the server, but since Redux actions are not asynchronous by default, we will use the redux-thunk middleware to allow this behavior. You could use something fancier if needed, but this simple middleware is more than enough for our purposes.

If you want to install all of that in a single line, try:

npm -i redux react-redux react-router-redux redux-thunk

This tutorial assumes your project has a working Flux store.

A bridge between stores

Now that we have installed the required dependencies, we need a way for our app to handle both Redux and Flux' action calls. To do this, we will copy a simplified version of Redux createStore and change it so that it handles objects including either type or actionType properties for Redux and Flux respectively.

You can go ahead and copy this createFluxStore file to save time, but be aware that it uses lodash's isPlainObject, so if you don't use it in your project, just delete line 4 and 158 to 162, and everything should still work fine.

Sample App structure

The sample application we will use, has the following structure:

    Home
    ├── Products
    ├── Shipments
    └── Clients
Enter fullscreen mode Exit fullscreen mode

In this scenario, we will start by migrating the Clients section, and assume each one has their corresponding Flux stores and actions.

Creating the first reducer

Our clients section is rather simple, it displays a list of clients where the sorting can be reversed.

The store is using a slightly old syntax, but should be understandable enough:

Note: error handling was omitted for brevity.

// ClientStore.js

// Creates an instance of a flux store, will be replaced later
import Store from './Store';

// Some helpers to handle async calls
import * as http from './helpers/http';

// Instance of a flux store, more on that later
import Store from './Store';

// Flux dispatcher
import Dispatcher from 'flux/lib/Dispatcher';

// Instance of flux dispatcher
const AppDispatcher = new Dispatcher();

// store's state
let _state = {
    clients: []
};

// the store
class ClientStore extends Store {
    getState() {
        return _state;
    }
}

// Create a new instance of the store
const clientStoreInstance = new ClientStore();

// Async function that makes a server request to get all clients, returns a Promise
const getClients = () =>
    http.get('/clients').then(clients => {
        // Update the state with the successful server response
        _state.clients = clients;
    });

// Toggles the direction of the results
const toggleSorting = () => {
    _state.clients = _state.clients.reverse();
};

// listen for actions and define how handle them
clientStoreInstance.dispatchToken = AppDispatcher.register(async action => {
    switch (action.actionType) {
        case 'GET_CLIENTS':
            await getClients();
            break;
        case 'TOGGLE_SORTING':
            await toggleSorting();
            break;
    }

    // Notify of the store change
    clientStoreInstance.emitChange();
});

// Export the new instance of the store
export default clientStoreInstance;
Enter fullscreen mode Exit fullscreen mode

The getClients function is async, so this will not translate nicely to Redux, since the reducer should be a pure function. (this means having no side effects elsewhere - ie. an async request). It should just be an input and an output, but more on that later.

The sorting function on the other hand, doesn't have any side effects and therefore, fits nicely with the reducer:

// clientsReducer.js

// Set the initial state to be used
const initialState = {
    clients: []
};

// define and export reducer
export default function clientsReducer(state = initialState, action) {
    // handle action's results
    switch (action.type) {
        // Set the result of the async request to state
        case 'GET_CLIENTS': {
            return {
                clients: action.clients
            };
        }

        // Toggles the direction of the results
        case 'TOGGLE_SORTING': {
            return {
                clients: state.clients.reverse()
            };
        }

        // return the default state if no action was found
        default:
            return state;
    }
}
Enter fullscreen mode Exit fullscreen mode

Great, our first reducer! The problem now is that we are not handling the server request (yet), and the reducer is not connected to the app (yet).

Next, we will connect the brand new reducer to the flux store.

Redux reducer with a coat of Flux store

At this point, the Flux store and Redux reducer operate independently of each other, so this is the time to use the createFluxStore function to connect both. With this, actions intended for either store will be handled by the corresponding store, and both will share the same data origin. One downside of this implementation is that even though Flux uses Redux as the origin of its state, both will have a copy of the object.

We need to make a few changes to the ClientStore to read the state from Redux.

The first change is creating the ClientStore as an instance of EventEmitter instead of an instance of Store. This step will vary from project to project, and may not even be necessary.

// ClientStore.js
// Modified to instance directly from EventEmitter instead of Store for better control of its methods

// Removed flux Store class: "import Store from './Store';"

// will notify components when the store is updated
import EventEmitter from 'events';

// helper that creates a flux store connected to a redux reducer
import createFluxStore from './createFluxStore';

// the new reducer
import clientReducer from './clientsReducer';

// Flux dispatcher
import Dispatcher from 'flux/lib/Dispatcher';

// Constant used by the dispatcher to notify when data changed
const CHANGE_EVENT = 'change';

// Instance of flux dispatcher
const AppDispatcher = new Dispatcher();

// Redux store compatible with flux
const clientsReduxStore = createFluxStore(clientsReducer);

// Initial state will come from redux
let _state = clientsReduxStore.getState();

// modified store, instance of EventEmitter
const ClientStore = Object.assign({}, EventEmitter.prototype, {

    getState() {
        return _state;
    },
    emitChange() {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener(callback) {
        this.on(CHANGE_EVENT, callback);
    },
    removeChangeListener(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },
    dispatcherIndex: AppDispatcher.register(function(payload) {
        const action = {
            ...payload,
            type: payload.actionType
        };
        adminReduxStore.dispatch(action);
    })
}

// remove instance of the store: const clientStoreInstance = new ClientStore();

// Async function that makes a server request to get all clients
// returns a Promise
const getClients = () =>
    http.get('/clients').then(clients => {
        // Update the state with the successful server response
        _state.clients = clients;
    });

// Toggles the direction of the results
const toggleSorting = () => {
    _state.clients = _state.clients.reverse();
};

// listen for flux actions with the redux-flux store
clientsReduxStore.subscribe(async function(action) {
    switch (action.actionType) {
        case 'GET_CLIENTS':
            await getClients();
            break;
        case 'TOGGLE_SORTING':
            await toggleSorting();
            break;
    }

    // Notify of the redux-flux store change
    ClientStore.emitChange();
});

// Export the the redux-flux store
export default AdminStore;
Enter fullscreen mode Exit fullscreen mode

With this store, we can get the state from the Redux reducer, start to move each function from flux to redux, and have both stores working without having to stop one or the other.

This might seem a bit overkill for our simple app, where we can take the risk of having both our actions broken, while we make the switch to Redux, but on an application with ten or more methods and stores, you would want all Flux methods working while migrating the others.

You can play around with this setup to go further and have the store update when Redux updates. I haven't found that necessary because I usually work on a single piece of the store or method and migrate it to Redux on all the components that use it.

Migrating the first action

The first action we will migrate is the one that reverses the order of the results. This one is easy because there are no side effects, everything happens synchronously.

Our ClientActions file looks like this before migrating to Redux:

// ClientActions.js

// Flux dispatcher
import Dispatcher from 'flux/lib/Dispatcher';

// Instance of flux dispatcher
const AppDispatcher = new Dispatcher();

// Flux actions
const ClientActions = {
    getClients() {
        AppDispatcher.dispatch({
            actionType: 'GET_CLIENTS'
        });
    },
    toggleSorting() {
        AppDispatcher.dispatch({
            actionType: 'TOGGLE_SORTING'
        });
    }
};

// Export the actions
export default AdminActions;
Enter fullscreen mode Exit fullscreen mode

Let's add the equivalent action creator for Redux, at the bottom of the file,:

export function toggleSorting() {
    return {
        type: 'TOGGLE_SORTING'
    };
}
Enter fullscreen mode Exit fullscreen mode

If another section of the app needs to consume the Flux actions, they can be imported like:

// Flux actions
import ClientActions from 'ClientActions';
ClientActions.toggleSorting();
Enter fullscreen mode Exit fullscreen mode

And the Redux actions can be imported without interfering with Flux:

// Redux actions
import * as clientActions from 'ClientActions';
clientActions.toggleSorting();
Enter fullscreen mode Exit fullscreen mode

After all your components start using the new reducer, the old Flux actions can be either removed or commented.

Migrating an async action

To perform async actions with Redux, we will need to use the redux-thunk middleware. We will see how to connect Redux to our app in the next section, but first, let's add the server request to get the list of clients, by adding this action creator to ClientActions.js:

// First import our http helper to the top of the file, you can use whatever you want, maybe just a simple fetch call
import * as http from './helpers/http';

// ...

// action that will pass the clients from the server request to the reducer
// will be 'dispatched' after the async request is successful
function saveClientsToStore(clients) {
    return {
        type: 'GET_CLIENTS',
        clients
    };
}

// Async action that will make a server request to get the list of clients
export function getClients() {
    // redux-thunk not only helps redux perform async actions, but it also makes the
    // redux dispatch available for any action this in turn let's us return another
    // action instead of an action creator object
    return dispatch =>
        http
            .get('/clients')
            // Call the dispatcher to pass the received data to the reducer
            .then(clients => dispatch(saveClientsToStore(saveClientsToStore)));
}
Enter fullscreen mode Exit fullscreen mode

Now our Flux store and actions have their counterpart in Redux!

Unfortunately, our components still don't know anything about Redux or the reducer yet, so on the next section we will connect it to the app.

Connecting the stores

First, let's connect Redux to the app's entry point:

// index.js

// hot reloading for development env
import { AppContainer } from 'react-hot-loader';

// react dependencies
import React from 'react';
import { render } from 'react-dom';

// redux tools
import {
    createStore, // turn the reducers into stores
    combineReducers, // combineReducers to merge all different reducer's states into one object
    applyMiddleware, // incorporate redux helpers into the store pipeline
    compose // helps combine different functions into one
} from 'redux';

// helps redux handle async actions
import thunkMiddleware from 'redux-thunk';

// Component that makes the reducers and actions accessible to our application
import { Provider } from 'react-redux';

// react-router's browser history, this is different in v4
import { browserHistory } from 'react-router';

// react-router and redux helpers
import {
    syncHistoryWithStore, // keeps the browser history and synchronized
    routerReducer // provides the router as a redux reducer
} from 'react-router-redux';

// Reducers
import clientsReducer from 'reducers/clientsReducer';

// App wrapper, we will connecte it to redux next
import App from './App';

// Make the redux-dev-tools browser extension work with the app if available
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

// The app store with all middlewares and reducers available
const store = createStore(
    combineReducers({
        clientsReducer,
        routing: routerReducer
    }),
    composeEnhancers(applyMiddleware(thunkMiddleware))
);

// Browser's history synchronized with Redux
const history = syncHistoryWithStore(browserHistory, store);

// App rendering using the Provider component to enable redux
// We pass the store to the Provider and the history to the App wrapper
render(
    <Provider store={store}>
        <App history={history} />
    </Provider>,
    document.getElementById('content')
);
Enter fullscreen mode Exit fullscreen mode

Connecting the component

Now that the application is aware of Redux, we need the app to handle the new store and actions:

// App.jsx

import React from 'react';
// We use react-router v3, migrating to v4 will be done in the future
import { Router, Route, Redirect, IndexRoute, browserHistory } from 'react-router';

// all our new redux actions
import * as clientActions from 'actions/clientActions';

// redux helper that connects the actions to the dispatcher
import { bindActionCreators } from 'redux';

// redux helper that connects redux actions to the dispatcher
import { connect } from 'react-redux';

// all the app components
import Clients from '/Clients';
import Shipments from '/Shipments';
import Products from '/Products';

// other flux actions that have not been migrated
import AuthActions from 'actions/AuthActions';

// the app container
const App = ({ actions, clients }) => (
    <Router history={browserHistory}>
        {/* Other flux stores still used */}
        <Route path="/" component={Home} onEnter={AuthActions.isAuthenticated}>
            {/* Untouched routes using Flux */}
            <Route path="products" component={Products} />
            <Route path="shipments" component={Shipments} />

            {/* Modified route using Redux state and actions */}
            <Route
                path="clients"
                component={() => (
                    <Clients
                        clients={clients}
                        getClients={actions.getClients}
                        toggleSorting={actions.toggleSorting}
                    />
                )}
            />
        </Route>
    </Router>
);

// pass the redux store(s) to the component as props
const mapStateToProps = state => ({
    clients: state.clients
    // These can be done in a future pull request with our new setup:
    // TBD: products: state.products
    // TBD: shipments: state.shipments
});

// pass redux actions to the component as props
const mapDispatchToProps = dispatch => ({
    actions: bindActionCreators(clientActions, dispatch)
});

// pass both redux state and actions to your component
export default connect(mapStateToProps, mapDispatchToProps)(App);

// export just the unplugged component, this is helpful for testing
export { App };
Enter fullscreen mode Exit fullscreen mode

By setting our app this way, we can pass the specific state and actions that each route will need. In some cases you will even find that your components can become stateless as they always receive the new state from the store.

Another thing to note is that we export our component twice, the default export requires a Redux store and its actions, while the other export is not connected, this helps us test the component as it lets us pass the state and props we need to instead of having
to mock the whole Redux store. Testing is a topic best left for a different post.

Be aware that how you connect it might change depending on the react-router version your app uses.

Look ma! No Flux!

Now that we are almost done migrating the Clients section, the last step is to use the Redux actions in our components instead of the old Flux actions.

Currently our component stores the clients in the state, and listens for Flux store changes, but it's now using the reducer function from props to toggle the sorting.

// Clients.jsx

import React from 'react';

// import flux actions
import ClientActions from 'ClientActions';

// import flux store
import ClientStore from 'ClientStore';

class Clients extends React.Component {
    // Set the initial state
    constructor(props) {
        super(props);
        const { clients } = ClientStore.getState();
        this.state = { clients };
    }

    // Set flux listener
    componentDidMount() {
        ClientStore.addChangeListener(this._onChange);
        // Request clients from server
        ClientActions.getClients();
    }

    // remove flux listener on unmount
    componentWillUnmount() {
        ClientStore.removeChangeListener(this._onChange);
    }

    // update the state when flux emits a change event
    _onChange = () => {
        const { clients } = ClientStore.getState();
        this.setState({ clients });
    };

    _reverseOrder = () => {
        // previously, using Flux:
        // ClientActions.toggleSorting();
        // now with Redux:
        this.props.toggleSorting();
    };

    render() {
        return (
            <div>
                <button type="button" onClick={this._reverseOrder}>
                    Reverse order
                </button>
                <ul>{this.state.clients.map(client => <li key={client.id}>{client.name}</li>)}</ul>
            </div>
        );
    }
}

export default Clients;
Enter fullscreen mode Exit fullscreen mode

Now that the component works with both Redux and Flux actions, let's add the next one and remove all Flux related stuff, by using the props that we previously passed on the parent component:

// Clients.jsx

import React from 'react';

class Clients extends React.Component {
    // Request the server data
    componentDidMount() {
        this.props.getClients();
    }

    _reverseOrder = () => this.props.toggleSorting();

    render() {
        return (
            <div>
                <button type="button" onClick={this._reverseOrder}>
                    Reverse order
                </button>
                <ul>
                    {/* We now use the clients array that comes from the props */}
                    {this.props.clients.map(client => <li key={client.id}>{client.name}</li>)}
                </ul>
            </div>
        );
    }
}

export default Clients;
Enter fullscreen mode Exit fullscreen mode

As you can see, our component is simpler now that it gets everything from the props, and it only gets the specific data needed instead of having to call the whole store.

And that's it, our first section has been migrated. We can now clean it up and remove all references to the old Flux methods (if no other component is still using them), and submit this for a pull request and work on the next section for the next sprint!

Conclusion

  • Migrating a large react store is no easy task, but it can be done with just a few changes in gradual steps without breaking the whole functionality of an application.

  • A variety of 3rd party libraries can help us integrate Redux and React, and by using a modified copy of Redux's createStore we can create a Flux store that handles both Redux and Flux actions.


Thanks to GitHub user vivek3003 for the createFluxStore function and initial approach.

Oldest comments (0)