Migrating a React application from Reflux to Redux
Cristina Radulescu
Posted on April 24, 2020
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.
The state management in Redux is very similar to Reflux, the main difference being the use of reducers.
I identified the following steps during the migration process.
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.
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();
},
});
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(),
]);
}
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: {},
});
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);
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 aLOADED
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,
});
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();
}
},
…
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');
}
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);
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."}
]
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}/>
);
}
});
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>)
};
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.
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.
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 🙏
Posted on April 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.