Eventually the time will come when your team wants to use React + Redux for their frontend stack. We made that commitment some time ago at Small Improvements – we never had to regret it. As we come from an Angular 1.x frontend application, we needed to decide between React (+ ecosystem) and Angular 2. Staying with Angular 1.x was no option for our three teams. We saw too many benefits in other solutions like React e.g. to embrace functional programming. In the end we decided to go all the way with React + Redux, since most of our developers used it already in their side projects.
The article should give other teams or even companies some learnings and insights to have a smooth onboarding to the React + Redux ecosystem. While some learnings are applicable to React + Redux, others might be general insights about migrating to another technology.
React before Redux
In the beginning not everyone is familiar with React and its ecosystem. Give people time to understand, to experiment and to exchange thoughts. Introduce React without a state management library. Make use of the React lifecycle methods and internal state management (setState) to teach React itself. After a while you will want to introduce a state management library. It’s easy to get around an external library in smaller side projects, but it isn’t when you are contributing to a larger code base. In general don’t introduce a state management library when you don’t know the problem it solves for you.
In Angular state management got messy for us. Sharing state between components, watching state changes of components and services, storing server data – it was pretty soon a chaos. We knew the flaws of state management in Angular when the Flux pattern evolved. Perhaps that’s the cause why so many developers at Small Improvements got hooked by React + Redux eventually.
Don’t overengineer Redux
Once you started using Redux as state management library, don’t overengineer it. In the beginning the Redux ecosystem itself can be overwhelming. Moreover the way you deal with state management in Redux is different than what a lot of people are used to from the past. Again, like in React, give your team a chance to understand Redux. Not everyone will be already familiar with the overarching functional programming principals behind it.
We love to use these little enhancements in Redux, but we made the mistake to introduce them too early. Nowadays it’s easy to npm install a package. Speak with your team about new packages. Don’t take from them the opportunity to learn it themselves.
Understand it, before you use it
Learn React before using Redux. In react-redux you use the connect functionality to literally connect the Redux store to your React components. The Provider component at the root level makes sure that the store is passed as context to the underlying components. Thus you can retrieve the store state in mapStateToProps and pass dispatchable actions on the store to your components in mapDispatchToProps.
But what is the connect doing there? It’s too easy to simply call it magic. When learning React before using Redux, you can make sure that everyone in your team understands the concept of higher order components (HOC). After that everyone has the chance to reproduce the underlying mechanics of connect in react-redux. Eventually everyone is aware of the hidden Redux store in the React context.
We decided to use redux-saga as we had to introduce asynchronous actions. In our case some people already used it before and felt that it is a great match. After some time using it, we don’t regret to handle our side effects with generators.
But what is the best approach to introduce asynchronous actions? Not everyone is aware of generators after all and it might add yet another level of complexity. Redux-thunk is a great way to begin with asynchronous actions. It allows you to dispatch delayed actions. When the whole team feels comfortable with asynchronous actions, you should decide whether you want to experiment with another solution. The way to deal with asynchronous actions should be a recurring discussion topic until you make the final decision. Otherwise you will delay the decision and end up with bigger refactorings of your asynchronous layer.
Normalize your data?
We don’t normalize our data, even if we were aware of the aspect to keep a flat state in Redux. It was no unconscious decision to keep a deep nested state. Since we already have a large Angular 1.x application, we are used to most of the data structures. In our case it would expand the gap between the two worlds, because we would have to get used to two different data structures. Once we introduce new data structures, we keep the state flat from the beginning. We are not sure yet whether it was a bad (unconscious) decision. Still we feel comfortable to keep our deep nested state immutable by using ES6 spread operators. Moreover reducer tests with deep-freeze help to ensure immutability.
In the beginning we had a technical folder separation. Everyone is used to it from React + Redux tutorials. We had folders for reducers, actions, components, constants etc. Very early we noticed that the approach would never scale with independent teams. We decided to have feature folders. Now we have packages with clear boundaries. Take for instance a Table component package:
--Table/ ----index.js ----components/ ------Table/ --------index.js --------container.js --------presenter.js --------style.less ------Cell/ --------index.js --------presenter.js --------spec.js ------Row/ --------index.js --------presenter.js --------spec.js ----ducks/ ------index.js ------filter --------index.js --------spec.js ------sort --------index.js --------spec.js ------select --------index.js --------spec.js
One index file gives an entry point to each package. The ducks index file still exposes all necessary action creators and reducers. When another package wants to dispatch a Table action, it has to import it from “Table” and not from “Table/ducks”. The package has clear boundaries.
“I saw you are using ducks?” Yes. We decided to use them when we introduced feature folders. The advantage is to have everything in one place. But once you introduce ducks, you should decide on best practices to keep the duck files tidy. Standardize your naming for reducer and action creator functions to distinguish them.
Moreover we noticed that ducks don’t scale very well for us. The lines of code grew very fast. That’s why we decided to split up the ducks responsibilities in smaller domains, like you can see in the ducks folder for the Table in the example above.
What about boundaries to legacy frameworks?
ReactDOM.render() is all you need to have a React component tree in Angular. Moreover you can simply use the react-redux Provider component to pass the imported Redux singleton store as context to the component tree. You can dispatch actions on the store and get the state from everywhere, since you only need to import the store in your non React world.
The other way around we use a helper to render Angular components in React. Once you have a large code base with complex non React components, you can’t easily rewrite them all at once. That approach ensures us a stable migration from Angular 1.x to React. We can still reuse Angular components. Once we refactor one component from Angular to React, we can easily exchange the component in one place.
What about a synced cache to the legacy framework?
In the beginning we experimented with Relay to facilitate caching of our backend data. Even more we had attempts to make Relay independent of React to use it in Angular as well. But very soon Relay felt like a foreign object in React to us. We stopped the experiment to use Relay + GraphQL and remained with using our RESTful solution.
Still we had to figure out how to cache the server side data in our single page React + Angular application. Since we already used an own store architecture in Angular, we synced the stores to the Redux store. Everything we implement in the future uses the Redux store, but our old Angular pages still get the cached data from our store architecture.
Hack & Tell
You read a lot about giving people time to understand the ecosystem properly. Your whole team is sitting in the same boat when introducing something novel. Everyone tries to accomplish a scalable and maintainable code base in the new ecosystem. At Small Improvements we are having weekly Hack & Tells to exchange our recent gatherings. We share learnings to get a mutual understanding of doing things in React + Redux. In general those Hack & Tells don’t apply necessarily to one technology.
Knowledge you could exchange in a weekly Hack & Tell:
- best practices
- decisions like naming, folder structure etc.
- reusable components / feature packages
- new npm modules which solve a real problem in your code base
- recent pull requests
Perhaps once a week isn’t even enough to exchange knowledge in a whole new ecosystem. Our code base is scaling well, even though we feel that we could refactor all the time. We don’t regret the step to migrate from Angular to React and its ecosystem.