Building Scalable Applications using React — Part 1: Choosing the right mix of tools
November 10, 2020 -
In the series of building scalable applications using frontend frameworks / libraries we started with Angular in my last post. However when we speak about React we have to take a different approach and one post is enough. The reason being React is highly un-opinionated library unlike opinionated framework Angular.
This leads to obvious decision on how to pick the right mix of tools / libraries along with React to build scalable large applications. In ReactJS we always end up asking “What’s the right way to xyz?”, “What’s the right library for abc?”…so let’s try to address it in Part-1.
Key Considerations
- Scalable to dynamic requirements
- Modular in nature and open to multiple communication gateways
- Single Global State management which can be traceable & testable
- Faster first time load and consistent performance
- Reactive to environmental changes including communication, infrastructure, bandwidth & devices
Technology Stack
Let us first look at the Tech Stack for React
ReactJS — Technology Stack
You will see from above Tech Stack the flexibility ReactJS provides on the choices to make. This leads to important decision, which is very key for building ReactJS apps, is the choice of tools / libraries. This chapter is exclusively for arriving at that decision before we look at overall solution architecture.
User Interface (UI) — Components
Starter Kits
Productivity is key for developers and make sure to start with a Boilerplate,
- create-react-app — For building client side single page apps (Official)
- React Redux Boilerplate — For building client side apps with Redux state management
- Create React App + Redux + React Router — For mid sized application with Redux and Redux-Thunk for middleware management
- React + Redux + Saga Boilerplate — A more complex app using Redux & Redux Saga for middleware and side effects
- Next.js — For building server-side React apps
- Gatsby — For building sites with Static Site Generators
- More….
Base UI Library
Start off with a base UI library based on the UX design (bootstrap, material, sematic, flat….). The following UI libraries also provides customizable UI components & layouts to jump start youe app development.
- material-ui — Popular UI library for React based on Google material design
- reactstrap — UI components & layouts based on bootstrap 4
- PrimeReact — Rich UI components including charts, data grid, complex components and many more…
- More…
Styling
The reason this is important and needs special attention when building React applications is the fact that ReactJS core design principle is ‘HTML within JavaScript (JSX)’. Hence a normal loading of CSS/Sass within JavaScript components does not go well with the Architecture & Design of ReactJS and so the performance. Considering ReactJS is lightweight and very flexible, there are many 3rd party tools to style your application & components
- CSS Modules — Goes well with React as CSS class names, animation names are scoped locally by default and can be written as a separate CSS file.
- Styled Components — Highly customisable, easy to read, structured, goes well with components and JSX development, enable global, local and component themes
- TailwindCSS — Utility based CSS frameworks, very easy to create custom CSS, readable, goes well with components and JSX development, enable global, local and component themes
- React-JSS — Uses much popular JSS (CSS in JS) within React. Widely used by Static Site Generators, Server Side rendering, enable global andcomponent themes
If you are already using any UI base library mention above, then it provides its own methodology for styling. For instance, ‘Material-UI’ uses Styled Components.
Interface
Interface acts as a bridge between components and the middleware ensuring above principles / patterns
This is where our first test of choosing libraries gets in play as ReactJS is just a view library and for routing, models we need to rely on 3rd party libraries. However thankfully it's pretty straight forward
Routing
React-Router is the default choice and also recommended by ReactJS team. This is widely used in all React applications. It works well for all kinds of app
Another great alternative is Reach-Router which is fairly new and forked from React-Router. It’s very simple, config based and has most needed features built in including nested routes, relative links, animations, accessibility. It works well for simple-medium apps and where state management patterns like Redux is not used
You can add react-loadable on top to enable lazy-loading of routed components, dynamic imports, code-splitting.
You already see the flexibility of React and benefits it provides to choose what you need based on the requirements
State Management
Designing & maintaining state is critical in frontend and more so for large applications. State also plays a vital role in performance and ensure re-rendering only when needed.
React State
Ideal for small apps with no nested child components and there no need of passing data between from child to parent.
Event for medium-large apps we can use to persist local component state for interactions like pagination, sorting, highlighting…
Context API
This is great when we want to store in a global place and share it across components. This way we can avoid passing loads of props to nested components.
Use Context API for common data sharing across components like locale, theme, master data cache which does not change frequently. Keep this store to the minimum as you may except necessary re-rendering if the data changes frequently.
Redux / Mobx
Once your application grows neither React State nor Context API helps in ensuring maintainability & performance. Debugging becomes a nightmare as its difficult trace data flow and updates. Enter Redux / Mobx which provides a pattern to manage application store / state. Choosing between them is simple,
Use Mobx for small-medium application which is lightweight (don’t have complex transactions / logic). You want to quickly jump in and familiar with object oriented concepts. It is un-opinionated, simple and flexible
Use Redux for medium-large application having many data sets to manage and need a single source of truth which is testable and debuggable. It does have a steep learning curve but has great community support and tooling. It is opinionated and follows functional programming paradigm
Business Logic & Middleware
This is the most challenging and complex decision to make in ReactJS. The reason being its a view library and with multiple other libraries to manage state like Redux, middlewares like Redux Saga, Redux Thunk… it is easy to get confused on where to place your business logic. Again, there are multiple options depending on the usage of redux, redux saga, redux thunk or any other middleware.
Actions Creator
This is where you typically have your business logic, calling services as this is the first one triggered upon user action and calls reducers to manage state.
Pros
- Easy to identify by any developer
- Ideal for simple use cases and business logic
Cons
- Not possible to make Async calls for multiple requests
- No access to global state and need to rely on component props for accessing data from state
- Cannot have a workflow process to cater audit, error handling or even a workflow logic
- Testing components is a challenge and need to mock action creators
Use Action Creator for business logic for very simple and there is no need for multiple service requests, handling workflow logic, no dependency with global state.
Reducers
We tend to have our business logic here as this is where we decide on how to manage state. This sounds a logical place as we have have our logic prior to deciding on the new state.
Pros
- It's easy to write business logic in Reducer
- Business logic can decide the new state, so no complex data flow
Cons
- Reducers are synchronous, so async operations possible
- It negates the design patterns of Redux to just manage state (Pure functions)
- Cannot dispatch further actions for audit, logging, error handing
Ideally we should not be using Reducers for Business logic. Use it only for learning purpose or your logic is tightly coupled with state
Thunks (Middleware)
When our use cases becomes medium — complex we start using middlewares and one such commonly used middleware is Thunk. They are interceptors for Action creators and ideal to have your business logic
Pros
- Easy to identify by any developer and focussed
- Access to global state
Cons
- No easy way to cancel pending requests or only take the latest if multiple requests are made
- Cannot add further interception to augment your actions (multiple actions)
- Testing them needs to be mocked as there are functions are promise
Use Thunks for Business Logic for medium complexity and there is no need for augmenting multiple actions for logging, error handling, further requests / interceptors.
Sagas — Redux-Saga (Good Approach)
We start using Redux-Saga for more complex situations, triggering workflows, augmenting actions for multiple requests, error handling, logging. Considering Redux-Saga orchestrates actions and there by reducers, it is the ideal to have your business logic
Pros
- We can perform cancelling of requests or take latest if multiple requests through ES6 generators
- Async Orchestrations is possible through Sagas
- Testing is easy as action creators only dispatch objects and Saga does everything else
Cons
- Complex to understand and implement unless you are an expert
- Lots of code to setup and configure your Sagas
- No interceptions possible
Use Redux-Saga for business logic for medium — complex situations. You know what you are doing and are an expert with Redux
Redux-Logic (Alternate Approach)
Many of you wouldn’t have heard about this middleware and its fairly new and moreover whitewashed by usage Thunk and Saga. It brings in Thunk, Saga, Redux-Middleware all together and is the best place for your business logic
Benefits
- Ability to do Async processing and dispatching
- Ability to intercept actions for error handling, caching, transformation, authorization…
- Ability to cancel requests and take latest if multiple requests
- Easy to test and its declarative keeping actions, business logic, interceptors separate
Redux-Logic is a good approach / solution to have your business logicand provides all benefits of Thunk, Saga, Redux-Middleware, Observable all in one package
Communication Gateway
ReactJS is a view library, so we need to rely on 3rd party for API calls. The following libraries can be used based on the requirements,
axios — Promise based http client and is light weight. This is very popular as its root is based on Angular’s http service
apisauce —Light-weight wrapper for axios. It is better structured, flow and can be used in react-native (determine connection issues)
apollo — One and only client for GraphQL based requests
Interceptors / Wrapper (Add-On)
When you are dealing with large data sets which are nested you may want to intercept incoming data, normalize it so that it can be rendered with performance (or else you will have to deal with scanning through multiple lists to display a nested data on the screen, which is not a good design). Thankfully we have a library ‘normalizr’ which does exactly the same.
Moreover, you should not rely on same schema structure of data set received from API, as it may not be optimized / normalized the way UI component needs to be rendered.
References and Inspirations
Redux vs MobX without Confusion
Where do I put my business logic in a React-Redux application?