Separation of concerns in a React App
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.