I have selected a family of related technologies that make building modern web applications significantly easier than it ever has been.
- React
- Create React App
- Redux
- Redux Toolkit
- Hooks
- Typescript
- ESLint
- Prettier
- Visual Studio Code
This is a curated selection of technologies representing nothing more than my own opinions as a long time UI Developer. This selection is valid for applications that uphold a particular set of assumptions, including a preference for React over Angular, and a back-end API using REST instead of GraphQL.
I would also like to point out that apps built with these technologies can now be integrated into Liferay DXP easier than ever before. Further articles will likely explore this.
A GitHub repo is available to demo this selection of technologies in action: https://github.com/bkinseyx/cra-redux-ts/tree/best-practices-blogpost
The rest of this article assumes some knowledge of React and Redux.
I would like to give a shout out to Mark Erikson, the maintainer of Redux, who has really helped push Redux and evolve it to where it’s now in a better position to compete with a growing number of competing state libraries. I have to say, I like it!
Here are some competing solutions used by Angular and React to help maintain state circa 2020:
- Redux. The most well-known state library, widely used with both React and Angular.
- MobX: Offers some new ideas, but is still much less used
- Apollo: A great solution for GraphQL APIs.
- Ngrx: An Angular library inspired by Redux but implemented with RxJS (reactive programming)
- Custom/hooks: With hooks, it can be easier than ever to roll your own React solution for state management.
Typescript
I learned to fall in love with Typescript thanks to Angular.
The father of Typescript is Anders Hejlsberg, a veteran computer language genius. It’s so incredibly fortunate that the creator of Turbo Pascal, Delphi, and C# turned his attention to fixing Javascript, and thus Typescript was born.
Typescript improves the Javascript dev experience significantly, bringing badly needed type sanity to Javascript, but not just that. Once you adopt Typescript, you get so many benefits:
- A robust and expressive typing system
- Real-time linting of type errors as you are coding
- Incredibly powerful code completion
- Self-documenting code
- Code that needs fewer tests
- Often better perf, considering most run-time testing of types is no longer necessary
- Advanced language features not yet available in the browser
The downside of Typescript is that the React ecosystem has been slower to adopt it. But it’s coming along, and there is now a built-in template with Create React App: https://create-react-app.dev/docs/adding-typescript/
Create React App
I’m a huge fan of Create React App because it helps bring the React community much closer together when it comes to structuring a React app. It provides a default selection of technologies, a starting point for exploration. You can extend it using templates, in this case we are using Typescript:
npx create-react-app cra-redux-template --typescript && cd cra-redux-template
Additional Dependencies
We need both dependencies proper and devDependencies:
npm i -S @reduxjs/toolkit @types/react-redux @types/webpack-env react-redux serialize-javascript
npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier eslint-config-prettier eslint-config-react eslint-plugin-prettier
For a better dev experience, there are a number of files that need to be added and config tweaks that need to be created. Follow along from this article: https://medium.com/@xfor/setting-up-your-create-react-app-project-with-typescript-vscode-d83a3728b45e
Redux Toolkit
At the heart of Redux we have the store. With Redux Toolkit we can implement it like so:
store.ts
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import rootReducer from "./rootReducer";
import createLogger from "middleware/logger";
const store = configureStore({
reducer: rootReducer,
middleware: [
...getDefaultMiddleware(),
createLogger(process.env.NODE_ENV === "development")
]
});
if (process.env.NODE_ENV === "development" && module.hot) {
module.hot.accept("./rootReducer", () => {
const newRootReducer = require("./rootReducer").default;
store.replaceReducer(newRootReducer);
});
}
export type AppDispatch = typeof store.dispatch;
export default store;
This example adds one bit of custom middleware on top of what redux-toolkit provides out of the box, the logger. I do not recommend using an API service middleware. The redux-toolkit example repo does not use it, and I don’t think it’s necessary or adds any benefit, but I concede this topic is being actively debated. See this bit of code to see how the redux-toolkit example handles API success and failure. It’s all just standard promises, and if systematic changes (such as logging all API errors) are needed, it could be composed from the API code itself.
Note that there is some special functionality to handle “hot reloading,” sometimes known as HMR or Hot Module Replacement. As you are developing it, the app can update itself in the browser as you are writing it in your editor (literally) without affecting the store. This is amazing when you think about it — this exact feature was a historic holy grail for web development. Because of this feature you can code and test simultaneously, without having to start over again in your app each time you make a code update.
You’ll note a dependency on rootReducer:
rootReducer.ts
import { combineReducers } from "@reduxjs/toolkit";
import counterReducer from "features/counter/counterSlice";
const rootReducer = combineReducers({
counter: counterReducer
});
export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;
The purpose of rootReducer is to combine all your Redux “slices” (sometimes known as “ducks”) into a single reducer. In this case, I have just a single slice called counterSlice, but in a larger app we could compose together a vast number of slices and their own individual reducers.
How to Organize Slices
The convention I’ve adopted in this project is that each Redux slice file goes in its corresponding feature directory. A feature directory contains a chunk of functionality, often a component or components that do something related, and the Redux slice file needed to manage the state. In this case we have the Counter.ts component combined with the Counter.css file and the counterSlice.ts file.
$ tree
.
|-- App.css
|-- App.test.tsx
|-- App.tsx
|-- features
| `-- counter
| |-- Counter.css
| |-- Counter.tsx
| `-- counterSlice.ts
|-- index.css
|-- index.tsx
|-- react-app-env.d.ts
|-- rootReducer.ts
|-- serviceWorker.ts
|-- setupTests.ts
`-- store.ts
Super Simple Slice Example
This is our example slice. It features a single bit of state called “theCounter,” which we will access via a selector, and three actions can be dispatched: increment, decrement, and reset.
I would like to point out the “nullish coalescing operator,” (??) which is a future Javascript feature and now a standard modern Typescript language feature that gets transpiled to browser safe Javascript when it’s built. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator
counterSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface CounterState {
theCounter: number;
}
const initialState: CounterState = {
theCounter: 0
};
const counterSlice = createSlice({
name: "counterSlice",
initialState,
reducers: {
incrementCounter(state, action: PayloadAction<number | undefined>) {
const incrementAcount = action.payload ?? 1;
state.theCounter += incrementAcount;
},
decrementCounter(state, action: PayloadAction<number | undefined>) {
const decrementAcount = action.payload ?? 1;
state.theCounter -= decrementAcount;
},
resetCounter(state) {
state.theCounter = 0;
}
}
});
export const {
incrementCounter,
decrementCounter,
resetCounter
} = counterSlice.actions;
export default counterSlice.reducer;
A Reduxified Component using Hooks
Our counter component is pretty simple. A lot of the boilerplate from previous Redux versions has gone away. No longer do we need to make a HOC (“higher-order component”), or a class component, or container components. Say goodbye to mapStateToProps, mapDispatchToProps, and connect.
In their place we just need to use a couple of powerful hooks.
Counter.tsx
import React from "react";
import "./Counter.css";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "rootReducer";
import {
incrementCounter,
decrementCounter,
resetCounter
} from "features/counter/counterSlice";
const Counter: React.FC = () => {
const dispatch = useDispatch();
const { theCounter } = useSelector((state: RootState) => state.counter);
return (
<div className="Counter">
the counter: {theCounter}
<button onClick={() => dispatch(incrementCounter())}>
Increment
</button>
<button onClick={() => dispatch(decrementCounter())}>
Decrement
</button>
<button onClick={() => dispatch(resetCounter())}>
Reset
</button>
</div>
);
};
export default Counter;
Prettier
Prettier is an advanced code formatter, that works better than any other code formatter I’ve ever tried. The secret to its success is that it literally compiles the code into an AST (abstract syntax tree) and then uncompiles it back into code following your standard coding conventions. And this happens virtually instantaneously. The same code always gets formatted exactly the same way no matter how it’s originally written, which is a godsend for developers who work on teams, and it makes your git diffs far more readable. No more bikeshedding over code formatting ever again. My favorite way to configure prettier is to have it “Format on Save.” In my personal coding style, I save frequently, and I keep an eye out for how Prettier fixes my code as I save, which can sometimes make a structural mistake in the code more obvious.
ESLint
While Prettier is great for fixing code formatting (white spaces, brace placement, etc.), this does not completely eliminate the need for linting. Linting is great for catching more subtle issues beyond formatting. Linting with Typescript is especially powerful because it can detect and identify typing mismatches, and put a red squiggle under the offending code in real-time. Advanced linting can do so much more, above and beyond low-level coding issues like syntax and typing. Linting can detect potential problems with your code and identify many kinds of “code smells.” One high-level example is that linting can detect so-called “cyclomatic complexity,” which is basically just code with too much nesting that should be broken up into smaller functions. If you set up your linting rules carefully, it can help your team write better, clearer code. Linting is indispensable for enforcing a team’s coding standards, and automating some aspects of code review.
Note that ESLint is probably going to completely replace TSLint in the future. See https://eslint.org/blog/2019/01/future-typescript-eslint
Visual Studio Code
For Javascript and Typescript, I strongly recommend Visual Studio Code as a code editor/IDE. I was an emacs user for longer than I care to admit, but VSCode finally won me over with its amazing features that make it great for Javascript/Typescript development. VSCode completion is called Intellisense and it works phenomenally well. VSCode even has the ability to use Typescript types from the internet to help type check non-Typescript Javascript code. This is called Automatic Type Acquisition. I find this mind-blowing.
VSCode itself is written in Typescript, so it’s no surprise that the dev experience for Typescript is really top-notch.
Summary
I hope this blog post has helped inspire you to explore many of these technologies and form your own opinions about best practices.
If you have any questions at all, please engage with us via comments on this blog post, or reach out to us here.