Guide to Write Better React Apps
Best practices to follow when using Redux and writing React apps.
When writing any application, it’s important to discover the architectural problems early on. This helps you and your team to avoid unresolvable technical debt. Most of the time we as a developer tend to postpone some of the required activity(like writing comments, refactoring code for better readability or improving the codebase to handle upcoming complex requirements) on a later date for keeping our focus on getting main things done. While it’s important to balance the activities you’re doing but it’s also important to foresee future problems because of your today’s technical choices and decisions.
Redux is a tiny library for maintaining the predictable state in JavaScript applications. It can be used with any view library but in this post, we’re going to focus on React. This post contains most of the best practices to consider when writing the React app or using Redux.
1. Differentiate Presentational Components and Container Components
Container components are the React component which is responsible to subscribe to the state changes and pass it down to the pure components. Generally, these containers are concerned with how things work. It may contain both presentational and container components inside but usually don’t have any DOM markup of their own except for some wrapping divs.
Presentation components(also referred to as pure component) are also the React component which is concerned with how things look. These components are not aware of Redux and do not specify how the data is loaded or mutated. The data and ability to update them using callbacks are provided as props. Generally, pure components do not have local state, if they do it’s mostly UI state rather than the data state.
When writing pure components, you can see the improved performance if stateless functional components are used. These components avoid unnecessary checks and memory allocations and are therefore more performant.
// bad
class User extends Component {
render() {
const { name, email } = this.props;
return <p><b>{name}</b> - {email}</p>;
}
}
// good
const User = ({ name, email }) => (
<p><b>{name}</b> - {email}</p>
);
Local state is not “bad”; and there are instances where it can be useful(like UI specific minor states, temporary form validation error messages). If the state in consideration is useful to other components in the application or should be modified by other components, move it from local state to the Redux store.
2. Follow SOLID Principles
SOLID principles are coding standards that all developers should have a clear knowledge to avoid writing bad design code. It has five design principles intended to make software designs more understandable, flexible and maintainable. These are-
- Single responsibility principle - A class or function should only have a single responsibility, that is, only changes to one part of the software’s specification should be able to affect the specification of the class.
- Open–closed principle - Software entities(classes, modules, functions, etc.) should be open for extension but closed for modification.
- Liskov substitution principle - Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. See also design by contract.
- Interface segregation principle - Many client-specific interfaces are better than one general-purpose interface.
- Dependency inversion principle - One should “depend upon abstractions, [not] concretions.”
For example in the previous section when we defined the boundary for presentation or container components, it was all about the single responsibility principle. Bulky tightly coupled components with many responsibilities only increase technical debt. As our application grows it becomes harder to add new functionality or update existing ones.
If you detect more complexity in component, try to breakdown the problem and do something that makes that simpler. There is always some pattern to simplify things in the React apps.
3. Abstract Redux Complexity from Container
Although it’s the responsibility of the containers to connect redux with pure components, most of the details of dispatching actions and calling service methods (methods using which it interacts with backend resources asynchronously) should be abstracted from containers. For example-
// bad
class MyContainer extends Component {
...
onUserAction = () => {
const {dispatch} = this.props;
dispatch(dataRequestAction());
fetchData()
.then((data) => {
dispatch(dataSuccessAction(data));
})
.catch(err => {
dispatch(dataFailureAction(err));
});
};
...
}
// good
class MyContainer extends Component {
...
onUserAction = () => {
const {loadData} = this.props;
loadData();
};
...
}
const loadData = () => {
return dispatch => {
dispatch({ type: LOAD_DATA_REQUEST });
fetchData()
.then((data) => {
dispatch({
type: LOAD_DATA_SUCCESS,
payload: data
});
})
.catch(err => {
dispatch({
type: LOAD_DATA_FAILURE,
payload: err.message
});
});
};
};
Here loadData()
is an action creator and that is passed to MyContainer
as
props using react-redux
’s connect()
higher order component and container
simply subscribes to the data from the global state.
Below is the directory structure that I prefer, however you can make the necessary changes as per your requirements but remember to stick to the one you follow.
.
├── components # Pure components
├── containers # Containers
├── modules # Redux related parts goes in this dir
├── index.js # Exports combined reducers
└── app # app reducer
├── actionCreators # Keeps action creators for app reducer
├── actions.js # Exports action constants for app reducer
├── initialState.js # Keeps initial state of app reducer
├── reducerHandlers # Keeps app reducers
4. Minimize Assumptions and Dependencies
Components that may be used across projects should make no assumptions about the application architecture, and minimize any dependencies. If we keep components agnostic about the application environment they are running in, we gain a more solid, flexible component or library that will be useful for projects over a longer period.
5. Write Unit Tests
When your application starts growing and multiple contributors are working on the same codebase. Chances are that few of them will not be aware of breaking changes that they are making. It becomes really important for everyone to know if they have broken something in the app. Being a dynamic language, sometime you’ll not get the errors immediately but in runtime. Consider your react-native app which you’ve shipped and it has some breaking changes, is it not the worst situation to be in?
Writing unit tests will not only save you from these blunders but also it’ll help you to ship the changes with more confidence. If you have not yet started writing test cases, you should first start with the reducers.
- Cover reducers tests first, it’s important to cover them because the build will silently pass even if there are bugs when it’s setting state. You never want to miss if the state is being mutated or being set incorrectly.
- Export both the connected and non-connected component, because while writing
unit tests, we don’t need to test the functionality of
react-redux
’s<Provider/>
or theconnect()
function itself. You can safely assume those have been tested and do their job. What you want to test is the actual container component itself. By exporting the un-connected container, you can easily mock plain objects as props to test the container across different scenarios.
6. Provide More Control to Redux Consumers
When you define action constants, define it in such a way that it gives a more granular level of control on any process. For example, when you are making an API call there are three possible actions you can have-
- REQUEST - Before sending the request
- SUCCESS - After the request is served successfully
- FAILURE - After something went wrong
If you define multiple stages, it helps consumers(containers and components) to cover complex requirements easily and accurately.
7. Not Using Redux
The whole idea of using redux is to standardized things so that when your requirements grow you don’t need to think about refactoring later for the base architecture of the application. While making API calls, in some cases when you don’t need to keep data in the centralized state, you don’t need to give up on using redux, rather you can just ignore setting global state in reducer and still maintain the pattern.
If you have any comments or questions, leave them below — I would love to hear what you think.