Tech Stack 2019: Frontend Global State

John Tucker
codeburst
Published in
5 min readNov 20, 2018

--

Adding frontend global state to the core of a technology stack for 2019.

This is one of a number of articles that add incremental functionality to the frontend and backend applications developed in the article Tech Stack 2019: Core.

The final result of this article is available from the frontend-global-state branch of the tech-stack-2019-frontend (TODO) repository. The backend remains unchanged from the Core article tech-stack-2019-backend.

What and Why?

While we can use local (component) state for a large number of situations, e.g., user-interface (UI) state, one will encounter situations where state needs to be accessed across a number of components across the application (a cross-cutting concern). In this case, we need a solid strategy for using global state; in the React ecosystem, Redux has become the defacto-standard for this.

In general, use Redux when you have reasonable amounts of data changing over time, you need a single source of truth, and you find that approaches like keeping everything in a top-level React component’s state are no longer sufficient.

However, it’s also important to understand that using Redux comes with tradeoffs. It’s not designed to be the shortest or fastest way to write code. It’s intended to help answer the question “When did a certain slice of state change, and where did the data come from?”, with predictable behavior. It does so by asking you to follow specific constraints in your application: store your application’s state as plain data, describe changes as plain objects, and handle those changes with pure functions that apply updates immutably. This is often the source of complaints about “boilerplate”. These constraints require effort on the part of a developer, but also open up a number of additional possibilities (such as store persistence and synchronization).

Redux — FAQ

At the same time, there are technologies (GraphQL APIs and Apollo Client) that will greatly reduce the need for Redux for handling persisted (stored in a backend) global state. Additionally, one can even use Apollo Client for local global state and thus not even use Redux. We will explore this approach in a separate article.

In the situations, however, were we need global state and are not using GraphQL APIs, Redux is a solid choice.

note: This is not a tutorial on Redux, but rather it is a walk-through of a basic example (emphasis on its use with TypeScript). There are a number of separate tutorials on Redux, one of which I have written: Redux By Example: Part 1.

Sidebar into Aliases

One common problem with React is that importing local modules using relative paths becomes tedious (is it dot or dot dot?). This is especially problematic when using Redux as you need to refer back to top-level modules from deeply nested components.

The solution is to use webpack aliases, e.g., we can use imports like:

import counter from 'DUCKS/counter';

instead of:

import counter from '../../counter';

The changes in webpack.config.js are:

...
module.exports = (env) => ({
...
resolve: {
alias: {
COMPONENTS: path.resolve(__dirname, 'src/components/'),
DUCKS: path.resolve(__dirname, 'src/ducks/'),
STORE: path.resolve(__dirname, 'src/store/'),
},
...
}
...

note: The three folders, components, ducks, and store, all share this problem.

Likewise we need to update tsconfig.json to support these aliases with TypeScript.

{
"compilerOptions": {
...
"baseUrl": ".",
"paths": {
"COMPONENTS/*": [
"src/components/*"
],
"DUCKS/*": [
"src/ducks/*"
],
"STORE/*": [
"src/store/*"
]
}
}
}

Basic Example

In this example, we add a counter that we can increment and decrement.

We begin by adding the dependencies:

npm install redux
npm install react-redux
npm install @types/react-redux
npm install redux-thunk

note: We will use redux-thunk, later, in an asynchronous example.

The code (src / store / index.ts) to create the Redux store is pretty standard; includes support for Chrome Redux Devtools:

With TypeScript, we need to define a type (src / store / AppAction.ts) that represents any action:

Likewise we need to define the type for the state (src / store / AppState.ts):

Using the Redux Duck pattern, we define the actions, reducers, and selectors for a slice of the state in a singular file (src / ducks / counter.ts):

We use the React Redux library to inject (using Provider) the state into the application (src / components / App.tsx):

In (src / components / Counter / index.ts), we use the React Redux library (using the connect higher-order component) to supply state (using selectors and action creators) to a wrapped component.

The wrapped component (src / components / Counter / CounterView.tsx):

Finally, we simply rename the previous App component that consumes the Hello API to the Hello component (src / components / Hello.tsx).

Asynchronous Example

In this example, we refactor our application to consume the Hello API through Redux instead of in the Hello component.

This example is a simplified version of what is provided in the Redux Async Actions documentation. The most complex piece is creating the Redux duck (src / ducks/ hello.ts):

We have to update the top-level reducer to support this new slice of the state (src / store / index.ts):

...
import hello from 'DUCKS/hello';
...
const reducers = combineReducers({
...
hello,
});
...

Because we are using TypeScript, we update (src / store / AppState.ts):

...
import { HelloState } from 'DUCKS/hello';
export default interface AppState {
...
hello: HelloState;
}

Likewise for (src / store / AppAction.ts):

...
import { HelloFetchRequestAction, HelloFetchResponseAction } from 'DUCKS/hello';
...
type AppAction =
...
| HelloFetchRequestAction
| HelloFetchResponseAction
...
...

In (src / components / Hello / index.ts), we use the React Redux library (using the connect higher-order component) to supply state (using selectors and action creators) to a wrapped component.

The wrapped component (src / component / Hello / HelloView.tsx):

Observation:

  • In addition to the Hello state being globally defined, we have moved all the logic (albeit simple) out of the components and into Redux

Wrap Up

This is one of a number of incremental features we will add to the core backend and frontend solutions. If you are interested in getting updates, would recommend that you follow me.

--

--