Traditionally the idea of scaling in development was mostly associated with server-side applications. However, as the web applications started to become more complex with frameworks like React, PWAs, native applications, and others, the need to write scalable code has crossed to the frontend. It is our obligation to think about our application as a living organism which can (and probably will) continuously grow. Speaking about scalability a lot of things comes up, but the two most important ones are handling data traffic increase and structuring your code so new features and changes can be done effortlessly. In this article, we will cover the second one and share with you which library stack and code structure we use here in Kraken so our new developers don’t get lost in our apps and can be productive as soon as possible.
Code structure
When it comes to code structure and pretty much everything outside its main API, React is very opinionated and gives you a lot of freedom. That is especially true when we need to define our code structure. There is no official way of doing things, and even the famous React developer and creator of Redux is very vague about that subject.
In general, I don’t agree with this approach, most importantly because we have to assume that in the future someone else will also work on this project. What feels right for us could be nonsense for someone else. Especially if we work on more projects and we use a different structure for every one of them. The second important thing is that our structure must not be brittle. Adding new features must be easy, and in most cases we should not have to rewrite big parts of the remaining project code to make it work. To achieve this we should plan ahead and not change things as we go. It is important to decide early on how to organize your code structure since this will prevent inconsistencies and will accelerate app development.
Create React App
In most cases, our main starting point here at KrakenSystems with any React application is Create React App (CRA) boilerplate. It is a great boilerplate for creating single-page applications built with React. We wrote about it in our last article here. Since it comes with everything we need like Babel, live development server, Autoprefixer for CSS and powerful testing suite built with Jest there is no point in spending hours tweaking Webpack and building configuration which is already prepared inside CRA. We can easily customize it with features we regularly use, and its documentation is great. I advise spending an hour or two going through it. It will save you from unnecessary ejecting your app because you thought that customization of the CRA could not be done without ejecting it first (happened to me a few months ago actually).
Node-saas
When writing CSS our weapon of choice is SASS, or specifically SCSS. We are aware of the rising popularity of CSS-in-JS, but we decided to stick to SCSS since it is easier to write and the learning curve is very small. The main problem which people have with CSS and all of its preprocessors is global namespace which can be the source of many bugs, but in most cases it is easily fixable with smart naming conventions. To add node-saas to CRA you just need to install it to the project and that’s pretty much it. You can find detailed instructions for this here.
Adding proxy to the development server
When developing some new app, we like to have frontend and backend served on different hosts. For example, the frontend can be accessed from localhost:4000 and backend server from localhost:8000. We like to have things separated and it is convenient for us, but it does require some additional setup to the frontend part of the application. To make sure our API requests land on the correct address we need to add a proxy to the development server which will handle them. You can’t access the development server configuration file in CRA, but you also don’t need to eject the whole app just to add a proxy to it. Add the proxy field with all the needed configuration to the package.json file and CRA will automatically use it after you restart the whole app. Detailed instructions can be found here.
File structure
As I have mentioned before, there is no official rule telling us how to structure our files inside the React application, and therefore, many different teams use different ways to structure their apps. Lately, our goto method for that is something which can be named a feature-based file structure. That means that every feature, or in some cases every page, has its own folder with its main container and components which are used in it. For example, It would look something like this:
With this approach it is very easy for a new developer to start working on a feature, the structure is easy to follow and it significantly lowers the time needed to understand how the app works. Components nested inside feature folders are used only there, everything else is added to the Shared folder which contains components like Button, TextInput, Modal, icons and error pages, but also some constants or shared helper methods. Containers are used to fetch data or use data from the redux store (which is also defined inside a special folder with all the reducers in one place) and handle routing. Data is then passed as props to components which are as simple as they can be and their main job is to display some UI based on passed props. They are mostly functional components and if they need some sort of local state, then we prefer to use hooks. To see how this can look in practice, here is a simple container handling the profile page:
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import get from 'lodash.get';
import ProfileDetails from '../components/ProfileDetails';
import { refreshUser } from 'redux/user/actions';
const ProfileContainer = ({
refreshUserAction,
user,
}) => {
return (
<>
<ProfileDetails user={user} refreshUser={refreshUserAction} />
</>
)
}
ProfileContainer.propTypes = {
refreshUserAction: PropTypes.func.isRequired,
user: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
user: get(state, 'user.details'),
});
const mapDispatchToProps = {
refreshUserAction: refreshUser,
};
export default connect(mapStateToProps, mapDispatchToProps)(ProfileContainer);
Library stack
Not every project is the same, but there are some libraries and tools which we use in almost every React app we build. Some of them like PropTypes or React Router are basically industry standard and some like Moment or Lodash are here to simplify our development process. They are also important for ensuring scalability of our apps. Using Proptypes gives us a sort of self-documenting layer for our components and using Axios enables us to easily define configuration file with all the methods, headers and error handling in one place so making API requests feels like a breeze.
PropTypes
PropTypes was actually a core React feature which is now since React v15.5 a standalone package used for type-checking props. Through its validators you can run checks on your props and they will return a warning in your console if any of the props have received a wrong type of data, for example, array instead of an object which can easily break your application. You can check for more than one type of data or even give it an array of options for that prop. Except for its obvious role in making your application more stable using PropTypes will help you a lot with catching bugs and it will also force you to think more about the app you are building. And as said before, it will add a great documentation layer for your app since now you can on a first glance see what every component expects to receive as props.
import React from 'react';
import PropTypes from 'prop-types';
const Button = ({
children,
disabled,
onClick,
size,
theme,
...rest
}) => (
<button
className={`Button Button-size-${size} Button-theme-${theme}`}
type="button"
disabled={disabled}
onClick={onClick}
{...rest}
>
{children}
</button>
);
Button.propTypes = {
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
disabled: PropTypes.bool,
onClick: PropTypes.func,
size: PropTypes.oneOf(['sm', 'md', 'lg']),
theme: PropTypes.oneOf(['default', 'info', 'warning', 'success', 'error']),
};
Button.defaultProps = {
disabled: false,
onClick: null,
size: 'md',
theme: 'default',
};
export default Button;
Lodash
Lodash is a versatile and very popular library which gives you a huge set of utility methods to easily work with objects, arrays, strings, etc. In React very often we have to handle all sorts of objects and arrays so Lodash will be a true lifesaver when fast development is a priority. Some of the most popular methods are _.get, _.pick, _.debounce, _.sortedUniq,… Although everything that Lodash does is pure Javascript and we can replicate every method, it will make your code look clean and you won’t need to think about polyfills for older browsers. In web development world there are a lot of loud voices preaching against using 3rd party libraries or frameworks at all ( just google about jQuery vs vanilla JavaScript ). While it is true that you should first become proficient in your native programming language syntax, when building an app one needs to be pragmatic and use the best tools at its disposal which will allow you both speed of development and app stability.
import { get } from 'lodash';
// Gets the value at path of object
// If the resolved value is undefined, the defaultValue (third argument) is returned in its place.
const dataObject = {
user: {
firstname: 'Hari',
lastname: 'Seldon',
occupation: 'psychohistorian',
locations: {
origin: 'Helicon',
lastKnown: 'Trantor',
}
},
};
const userOriginPlace = get(dataObject, 'user.locations.origin');
=> "Helicon"
const userCurrentPlace = get(dataObject, 'user.locations.current', 'unknown');
=> "unknown"
Axios
Similar to lodash vs. pure javascript, there is also a debate about using fetch or axios for making your http requests. In general, they are very similar, but axios has few advantages over fetch. Unlike fetch, axios lets us handle errors much better. For instance, promise returned from fetch won’t reject on HTTP error status ( 404, 500, etc. ) which is not the best solution, especially when our app behavior depends on network request’s response. Also, when using axios we don’t need to use json() method on a response, the data is already here, ready to be used immediately. Axios is all in all much more configurable than fetch, which is very useful when dealing with unique or more complicated situations.
import axios from 'axios';
axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.xsrfHeaderName = 'x-csrftoken';
axios.interceptors.response.use((response) => {
if (response.status === 401
|| response.status === 403) {
window.location.href = '/login'; // eslint-disable-line no-undef
}
return response;
}, error => Promise.reject(error));
const endpoint = '';
const getPath = url => `${endpoint}${url}`;
const api = {
get: (url, config = undefined) => axios.get(getPath(url), config),
post: (url, data = undefined, config = undefined) => axios.post(getPath(url), data, config),
patch: (url, data, config) => axios.patch(getPath(url), data, config),
delete: (url, config = undefined) => axios.delete(getPath(url), config),
};
export default api;
Moment.js
Almost every app out there uses datetime to some extent, so you should be familiar with date formats and manipulation. To make things easier we use Moment.js library. On its homepage, there is a quote saying it is made to ‘parse, validate, manipulate and display dates and times in Javascript’. Moment adds a wrapper for native JavaScript date object and extends its functionality. For instance, if you are going to work with timezones, Moment will save you a lot of time. The syntax for Moment is simple. It uses a number of methods on main moment wrapper object and if you want you can also add your own to it. It has great documentation with excellent examples so if you get stuck using it that should be, like with every other technology, your first place to look. Moment is not a small library, so if you want to trim down your bundle size, you can also think about date-fns library which has a smaller set of tools, but it will do just fine for most of your needs.
import React from 'react';
import moment from 'moment';
const defaultDateTimeFormat = 'DD/MM/YYYY HH:mm';
const TimeDisplay = ({ time }) => {
return (
<div className="TimeDisplay">
<p>
This was last updated on:
<span>
{
moment(time).format(defaultDateTimeFormat) // ex. 19/04/2019 14:48
}
</span>
</p>
</div>
);
}
export default TimeDisplay;
React Router
React Router is a dynamic routing library built on top of the React and if you are not building a small or fairly simple app, you are going to need some routing solution for easy navigation. React Router is the industry standard in React so you should definitely learn it at least.
It has three packages - react-router, react-router-dom and react-router-native. You should install only one of the last two since they both use react-router. The only difference is that react-router-dom is meant for apps that will be run inside browsers, and react-router-native is meant to be used alongside React Native. React-router-dom is built from components which you include in your app so it doesn’t feel ‘hacky’. Instead, it looks and behaves like a normal React code. Wrap your components inside React Router and you are good to go.
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
const App = () => {
return (
<div className="App">
<BrowserRouter>
<>
<Switch>
<Route path="/" component={MainContainer} />
<Route path="/account" component={AccountContainer} />
<Route path="/users" component={UsersContainer} />
<Route path="/users/:userId" component={UserContainer} />
</Switch>
</>
</BrowserRouter>
</div>
);
}
export default App;
React Redux
Redux is a predictable state container for Javascript apps and it is most useful when we have a lot of data traveling around the app and we need something more complex than native state management to control it and make sense of it all. React Redux is the official tool through which we can use all the functionality of Redux inside React app. It let us build a store, which will be our centralized source of truth and components will then use data directly from that store. React Redux will also enable us to dispatch actions which will control state updates and connect Redux store with the rest of the app. Using Redux inside React app will add an extra layer of complexity to the app. Some repetitive boilerplate is needed to prepare a Redux configuration, but when finished with the initial setup, there won’t be any need to tinker around it a lot afterward - it will just work. Its philosophy about a single source of truth resonates very well with React and in my opinion, it makes React apps more readable and understandable when using it correctly. There is no need in using it for smaller applications since it will be unnecessary overhead. A good rule of thumb for when we should include Redux is if at any point we need to use the same data in more than one feature or page.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { combineReducers, createStore } from 'redux';
import App from './components/App';
const userData = { firstname: 'Pham', lastname: 'Nuwem' };
// action
const getUser = () => ({
type: 'GET_USER',
payload: userData,
});
// reducer
const userReducer = (state = {}, action ) => {
switch(action.type) {
case: 'GET_USER':
return { ...state, user: action.payload };
default:
return state;
}
}
// combine reducers to use in single store
const reducers = combineReducers({
users: userReducer,
});
// create store
const store = createStore(reducers);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root'),
);
Redux Thunk
Redux actions from a previous snippet are synchronous, which means that they almost instantly return an object containing action type and some data inside the payload property. This is valid behavior, but most of the time we will want to do some action first, like fetch that data from some API and then return the object with action type and payload. This takes some time so to ensure that the store doesn’t get updated before the request is completed we use popular redux middleware package like redux-thunk. Middlewares are used to intercept the actions, do something with them and send them to the reducer or to the next middleware. There are many of them and we can easily build our own, but for this use case redux-thunk is basically industry standard. It will intercept the action, wait for the network request to be finished and then dispatched it with the fetched data as a payload.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { applyMiddleware, combineReducers, createStore } from 'redux';
import thunk from 'redux-thunk';
import App from './components/App';
import api from '../api';
// asynchronous action using async/await syntax
const getUser = () => async dispatch => {
const response = await api.get('/user');
dispatch({
type: 'GET_USER',
payload: response.data
});
};
// reducer
const userReducer = (state = {}, action ) => {
switch(action.type) {
case: 'GET_USER':
return { ...state, user: action.payload };
default:
return state;
}
}
// combine reducers to use in single store
const reducers = combineReducers({
users: userReducer,
});
// create store
const store = createStore(
reducers,
applyMiddleware(thunk)
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#root'),
);
What are your best practices when writing React apps? Do you disagree with something we wrote here? If yes, share your thoughts here in the comments section. React is constantly improving and evolving, giving us a lot of opportunities to be creative, which in the long run will make us overall better developers.