Separation of concerns in a React App

Rajesh Naroth
3 min readMay 22, 2020

--

Image by bluebudgie from Pixabay

Bootstrapping an application with several global dependencies can be tricky. Internationalization, Authentication, Component library, Theme, Routing, State management, Session management, Error Boundaries etc are some of the pieces you will need to provide throughout your React App.

In this article I’ll try to present a pattern to separate concerns in a clean way. There are no original ideas here. The essence of it is derived from blogs by Eric Elliott and Kent Dodds.

The end result would be something like this:

export const unAuthenticatedPage = compose(
withI18n,
withStrictMode,
withRouter,
withAuthentication,
withLoginShell
);
export const authenticatedPage = compose(
withI18n,
withStrictMode,
withRouter,
withRedux,
withAuthentication,
withAxiosHeaders,
withAppShell,
withErrorBoundary
);
export const App = () => {
const loginInfo = getUser();
return isUserAuthenticated(loginInfo.username)
? authenticatedPage(() => <AppContainer />)
: unAuthenticatedPage(() => <LoginContainer />);
};

What I want to demonstrate is the how simply and nicely it is expressed. App has two personas. One that is authenticated and one that is not. Both require a different combination of global bootstrapping. The secret to this is how you write your HOC wrappers.

The HOC Wrappers

Here are some examples of Higher Order Components. These are not copy-paste ready but just a general guideline.

withRedux

const store = createStore(combineReducers(allAsyncReducers));const withRedux = (Component) => (props) => (
<Provider store={store}>
<Component {...props} />
</Provider>
);

withi18n

Set up i18n

import { initReactI18next } from "react-i18next";
import translations from "../locales";
const withI18n = (Component) => (props) => {
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: translations
});
return <Component {...props} />;
};

withRouter

Set up react-router

const withRouter = (Component) => (props) => (
<Router basename={"/atlas"}>
<Component {...props} />
</Router>
);

withStrictMode

export const withStrictMode = (Component) => (props) => (
<StrictMode>
<Component {...props} />
</StrictMode>
);

withAppShell

The common UI pieces that envelops the App features

export const withAppShell = (Component) => (props) => (
<div>
<Header>
<Navigation />
</Header>
<AppContent>
<Component {...props} />
</AppContent>
<Footer />
</div>
);

withErrorBoundary

Protect your feature from crashing the full app. There is a nice example available here:

export const withErrorBoundary = (Component) => (props) => (
<ErrorBoundary>
<Component {...props} />
</ErrorBoundary>
);

Authentication

For example.

export const withAuthentication = (Component) => (props) => {
// reducer manages the authenticated user info
const [state, dispatch] = useReducer(reducer, initialState);
const { setUser } = actions(dispatch);
const context = {
state,
actions: { setUser, setToken }
};
React.useEffect(() => {
// get use authentication details.
if (loginInfo?.username) {
setUser(loginInfo.username);
} else {
logout();
}
}, []);
return (
<>
<AuthenticationContext.Provider value={loginInfo.username}>
<Component {...props} />
</AuthenticationContext.Provider>
</>
);
};

Putting it all together

If you have noticed, all these HOC wrappers have the same function signature:

withXXX = (Component) => (props) => { ... }

This means that you can compose them very nicely. An authenticated page will have different set of concerns than an unauthenticated page, such as Redux. You can compose two different Page behaviors:

import { compose } from "ramda";export const unAuthenticatedPage = compose(
withI18n,
withStrictMode,
withRouter,
withAuthentication,
withLoginShell
);
export const authenticatedPage = compose(
withI18n,
withStrictMode,
withRouter,
withRedux,
withAuthentication,
withAxiosHeaders,
withAppShell,
withErrorBoundary
);

Now it becomes very straightforward to setup your App:

export const App = () => {
const loginInfo = getUser();
return userIsAuthenticated(loginInfo.username)
? authenticatedPage(() => <AppContainer />)
: unAuthenticatedPage(() => <LoginContainer />);
};

This pattern provides a clean way to compose your app features inside a certain Context. You can augment some of these contexts by providing custom useContext() hooks. Say you want to use the logged user name in other parts of the app. For libraries such as Redux, the context is already provided and the hooks provided by the library is enough.

export const useAuthentication = () => {
return React.useContext(AuthenticationContext);
};

As simple as that. Hooks in libraries such as Redux will readily work once you’ve enclose the app in its Context Provider.

Summary

What is presented in this article is a simple yet powerful pattern that can bootstrap a ReactApp with very isolated, loosely coupled implementations. Extending them or adding new ones become very easy. All you need to do it is to create a HOC and compose it as shown above.

You can also use this pattern to create multiple global contexts and make them available to features in the app via hooks. More on that in my next article on State Management.

--

--

Rajesh Naroth
Rajesh Naroth

Written by Rajesh Naroth

Frontend Architect, San Jose, CA

No responses yet