Migrating a React application from Reflux to Redux

kittyradulescu

Cristina Radulescu

Posted on April 24, 2020

Migrating a React application from Reflux 
to Redux

In this article I will describe how you can migrate a React application from Reflux to Redux with two example projects. In the first part of the article I will describe how state management looks in Reflux and compare it to the state management in Redux. This kind of migration is useful in larger projects where working with Reflux becomes complicated to maintain, because of React components which start listening to changes coming from multiple stores, which makes the code difficult to understand and to extend 💥. Therefore, migrating to Redux might be a better choice 💪. In the last part of the article I will show the Redux actions and state in the Redux DevTools plugin. For the calls to the server I will be using mock server. In order to better understand this article, some basic understanding of React, Reflux and Redux is needed, because the ideas that are presented are relying on basic information about React components, Reflux and Redux state management.

In Reflux we have three important components: stores, actions and view components. The view components trigger actions, the stores listen to actions and the stores update the view components with new data.

Alt Text

The state management in Redux is very similar to Reflux, the main difference being the use of reducers.

Alt Text

I identified the following steps during the migration process.

Alt Text

Store creation

Reflux
The EventsStore listens to the loadEventsDetails action, which is being called by the view component. When the action is called the store is calling an async method which loads the event list from the server and then sets the event information on the state. In order to notify the view components that listen to the store the trigger method must be called.

Alt Text

let state = "EMPTY";
let eventsInformation = [];

const EventsStore = Reflux.createStore({

    init() {
        this.listenTo(EventsActions.loadEventsDetails, this.onLoadEventsDetails);
    },

    async onLoadEventsDetails() {
        try {
            const response = await axios.get('http://localhost:8080/allevents');
            this.updateEventsInformation(response.data);
        } catch (error) {
            this.setError();
        }
    },

    startLoading() {
        state = "LOADING";
        this.trigger();
    },

    updateEventsInformation(events) {
        state = "LOADED";
        eventsInformation = events;
        this.trigger();
    },

    getEventsInformation() {
        return eventsInformation;
    },

    getState() {
        return state;
    },

    setError() {
        state = "ERROR";
        this.trigger();
    },

});
Enter fullscreen mode Exit fullscreen mode

Redux
Store creation in Redux is very similar to the store creation in Reflux. We will create the store in the EventsConnector and since we will use sagas for async calls to the server, what we need to do is to create a saga middleware and connect it to the store and enhance it with the Redux DevTools so that we can see in the Google Chrome plugin the evolution of the state. By using the Provider from the react-redux module we are sending the store to the AllEvents container where we are mapping the state to props and the dispatch to props and connect it to the AllEvents view component which will be rendered. We need to add all the sagas in the rootSaga so that our saga middleware knows about our sagas. Sagas will not be run though, but will run only when we will dispatch an action which will trigger the saga.

export const sagaMiddleware = createSagaMiddleware();
const appliedMiddleware = applyMiddleware(sagaMiddleware);

const enhanceWithDevTools = composeWithDevTools(appliedMiddleware);

const store = createStore(rootReducer, undefined, enhanceWithDevTools);

sagaMiddleware.run(rootSaga);

export const EventsConnector = (props) => (
    <Provider store={store}>
        <AllEvents {...props}/>
    </Provider>
);

export default function* rootSaga(): * {
    yield all([
        fetchAllEvents(),
    ]);
}
Enter fullscreen mode Exit fullscreen mode

Action creation

Reflux
In the actions file I defined one action on which the events store is listening to. The action needs to be created with Reflux.createActions. In this file, multiple actions could be added.

const EventsActions: Object = Reflux.createActions({
    loadEventsDetails: {},
});
Enter fullscreen mode Exit fullscreen mode

Redux
By looking at the actions in Reflux I will create actions in Redux. We need to load the events from the server and for that we will create three actions to do the following:

  • loading events
  • finish loading events
  • setting an error
export const FETCH_EVENTS = "FETCH_EVENTS";
export const FINISH_FETCHING_EVENTS = "FINISH_FETCHING_EVENTS";
export const SET_ERROR = "FINISH_FETCHING_EVENTS";

const createAction = (type, data) => ({ type, data });

export const startFetchingEvents = () => createAction(FETCH_EVENTS);

export const finishFetchingEvents = (events) => createAction(FINISH_FETCHING_EVENTS, events);

export const setError = (error) => createAction(SET_ERROR, error);
Enter fullscreen mode Exit fullscreen mode

After each action is dispatched the reducer updates the state in Redux. In the reducer we are defining the initial state.

  • variable which identifies if we have a loading LOADING state while waiting for the server response and or a LOADED state
  • variable which holds information about events
  • error variable

Everytime the state is updated the components which depend on that state are rerendered.

export const initialState = {
    state: null,
    events: [],
    error: null,
};

export const eventReducer = (state = initialState, action) => {

    switch (action.type) {
        case FETCH_EVENTS:
            return {...state, state: "LOADING"};
        case FINISH_FETCHING_EVENTS:
            return {...state, events: action.data, state: "LOADED"};
        case SET_ERROR:
            return {...state, error: action.data, state: "ERROR"};
    }
    return state;
};

export const rootReducer = combineReducers({
    eventDetails,
});
Enter fullscreen mode Exit fullscreen mode

The root reducer is combining multiple reducers, in our case we only have one reducer. The structure of the root reducer is determining the path to the state variables which is used in the container to retrieve the state from Redux.

API Calls

Reflux
Server calls are done directly from the store.


    async onLoadEventsDetails() {
        try {
            const response = await axios.get('http://localhost:8080/allevents');
            this.updateEventsInformation(response.data);
        } catch (error) {
            console.error(error);
            this.setError();
        }
    },

Enter fullscreen mode Exit fullscreen mode

Redux
We will create a saga for each method which does a server call in the Reflux EventsConnector. The asynchronous calls will be done with axios.

export function* fetchAllEventsEffect(action): * {
    try {
        const response = yield call(fetchEvents);
        yield put(finishFetchingEvents(response.data));
    } catch (e) {
        yield put(setError(e));
    }
}

export default function* fetchAllEvents(): * {
    yield takeLatest(FETCH_EVENTS, fetchAllEventsEffect);

}

export function fetchEvents() {
    return axios.get('http://localhost:8080/allevents');
}
Enter fullscreen mode Exit fullscreen mode

The saga will be called when the FETCH_EVENTS event is called. The AllEventContainer maps the state in Redux to props and the dispatching of the actions to props and connects it to the React component which gets rendered.

export const mapStateToProps = (state) => ({
    events: state.eventDetails.events,
});

export const mapDispatchToProps = (dispatch) => ({
    fetchAllEvents: () => dispatch(startFetchingEvents()),
});

export default connect(mapStateToProps, mapDispatchToProps)(AllEvents);
Enter fullscreen mode Exit fullscreen mode

The response received from our mock server looks like this:

[
    {"id": 1, "name": "React Beginner Workshop", "description": "Curious about React? Join us for React Essentials."},
    {"id": 2, "name": "Javascript Workshop", "description": "Learning Javascript is always challenging. Challenge yourself at this workshop."},
    {"id": 3, "name": "Java 8 Workshop", "description": "Find out more about Java 8 features."},
    {"id": 3, "name": "Learn Scala", "description": "Introduction in Scala."},
    {"id": 3, "name": "AI Advanced Algorithms", "description": "Let's learn AI together at this fabulous workshop."}
]
Enter fullscreen mode Exit fullscreen mode

Creating the view component

Reflux
The AllEventsConnector view component is rendered. The mixins property is used so that the component listens to the data which is sent from the store and then updates the state of the component with the new data. The connection to the store is done using Reflux.listenTo having as parameters the store and the method which is called when new data from the store is sent to the component. In the lifecycle method componentDidMount the Reflux action loadEventsDetails is called. The events information is then sent via props to the AllEvents component to be displayed.

const AllEventsConnector = createClass({

    mixins: [
        Reflux.listenTo(EventsStore, "handleStoreUpdate"),
    ],

    getInitialState() {
        return this.getCurrentState();
    },

    handleStoreUpdate() {
        this.setState(this.getCurrentState());
    },

    getCurrentState() {
        return {
            eventsInformation: EventsStore.getEventsInformation(),
        };
    },

    componentDidMount() {
        EventsActions.loadEventsDetails();
    },

    render() {
        return (
            <AllEvents eventsInformation={this.state.eventsInformation}/>
        );
    }

});
Enter fullscreen mode Exit fullscreen mode

Redux
The AllEvents component is using hooks to call fetchAllEvents and dispatch the action from the container and then when the props.events is updated the component will be re-rendered and the information about the events will get displayed.

const AllEvents = (props) => {

    useEffect(() => {props.fetchAllEvents()}, [props.fetchAllEvents]);

    return props.events.map(event => <div key={event.id}> {event.name} </div>)
};
Enter fullscreen mode Exit fullscreen mode

Redux Dev Tools

An advantage of using Redux is the fact that you can use the Redux DevTools plugin in Google Chrome and see the state and the actions triggered.

Alt Text

Cloning the projects

If you want to see the code for both projects you can clone the repositories from the links below. In order to start the projects please check the steps in the Readme files 🖥.

Sample Reflux project can be found here

Sample Redux project can be found here

After starting the project on http://localhost:3000/ you will get the events displayed.

Alt Text

I hope this article described how you can migrate an older project using Reflux to Redux and helps you migrate. Please let me know in the comments section below if you have other suggestions for migrating a Reflux store to Redux and which were the problems that you encountered. 🛠

Thanks 🙏

💖 💪 🙅 🚩
kittyradulescu
Cristina Radulescu

Posted on April 24, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024