Software Engineering at Amity— Pandemic Edition Part II

More lessons learned during a busy few months.

Chris Vibert
6 min readJun 28, 2020

Part II in the series.

During the few months that I stood at my desk in the kitchen, I worked on 3 projects all with very different tech stacks. I wrote some good code and I wrote some bad code. I lost what feels like years of sleep, but more importantly, I gained what feels like years of experience.

I want to share some of these experiences and pass on some useful tips that I picked up along the way.

Build your own translation service

Before moving to Bangkok earlier this year, I’d never worked on an application that needed to support more than one language — hardcoding text in English was all I’d ever known.

At Amity, we need to support many languages, and we do so mainly using the react-intl library. It’s simple to use, but until my second ‘pandemic project’, I’d never given much thought to how it works.

This second project was an application for Doctors to manage potential COVID patients (built with NextJS which I talk about in Part I). We knew that the application would have to support the Thai language eventually, but to get moving quickly we started off with just hardcoded English.

When the time for translation came, I took the same approach always:

This led me to more bug reports that I was expecting — many people were having trouble translating their NextJS apps with the popular libraries like react-intl and react-i18next, mainly due to the server-side rendering (SSR) that comes with NextJS.

I found the next-i18next library which is specifically for NextJS and its SSR, but that required a custom Express server which we were intentionally avoiding in favour of the far simpler API routes solution.

And so I decided to build my own. Doing so was a last resort, but given the immense time pressure, this last resort was reached quite quickly. Even my Sunday morning Git commits are evidence of the rush:

Building a translation service that matched our needs was relatively straightforward:

  1. Detection of the user’s language — browser settings can provide an initial language with window.navigator.language, and localStorage can be used to save a preferred language.
  2. A React context provider that stores the user’s current language along with a function to update this language. The function to update language is needed only by the ‘language picker’ component, and the language value itself is needed to ensure that our ‘translate’ function is outputting a string in the correct language. This translate function comes from…
  3. A React hook to consume the current language from context and use it to provide a translate function to components. The function will take a translation key, and return the corresponding value from the relevant language object — if no value is found it’ll try the default language object, and if no luck here, it’ll simply return the key itself.

Any component rendering a string that needs translation can use this hook to access the translate function:

Tips for translating apps

A general piece of advice: don’t let translation/localisation become an afterthought. If your app will need to be translated eventually, then you should build for this right from the start.

Even without any translation system in place, you can wrap text strings in a ‘dummy’ translate function:

const translate = string => string<span>{translate('This will be translated one day')}</span>

This will allow you to later find the strings that require translation by searching for where this function is used. When the real translate function comes along — assuming it’s named the same — you can just replace these text strings with the corresponding key from the translations object.

Rediscovering higher-order components in React

Since the release of Hooks, React applications have started to look very different. One of the main differences is that ‘with’ has been replaced by ‘use’ — by this, I mean that higher-order components have been replaced with hooks.

Both hooks and higher-order components (HOCs) allow you to extract common logic for reuse across multiple components. Whether or not hooks can and should completely replace HOCs is debatable, but the reality of it is that hooks are replacing HOCs. Think of all your favourite libraries and you’ll find that they’re probably now advocating the use of hook alternatives like useSelector and useHistory.

Despite this trend, the first of my three projects was working on an application using React 15 with no hooks but lots of HOCs — the application was built from functional components wrapped by HOCs from the recompose library. This HOC-based architecture turned out to be a perfect solution to our requirement: to take the existing registration flow from Amity’s Eko App and modify it so that with the help of a few environment variables, it could also act as the registration flow for a completely new app.

We knew that this would mean lots of conditional logic throughout the app:

const variableToUse = config.isApplicationA ? variableA : variableB

where this variableToUse could be anything such as a page title, the current registration step number, or the URL of the next step in the registration flow.

Instead of having to write this line of code in each component, HOCs allowed us to:

  1. Write it only once in a withIsApplicationA HOC
  2. Keep all of the conditional logic outside of the components themselves — the components would receive these variables as props but without knowing how or where their values were derived

Point 2 was particularly important because we knew that eventually, the two registration flows may diverge enough to justify each having its own repository. If this time came, we could remove the conditional logic without having to interfere with the components themselves.

As the application progressed, these conditional variables became more complex. Soon, each of the registration flows was using a different set of backend APIs with very different response formats. This meant that the frontend needed a separate set of MobX store actions for each set of APIs. For example, the final registration request required a separate store action for each registration flow:

To handle this, we built the WithConditionalRequest HOC to inject the relevant store action as a prop into the wrapped component. It does this by:

  1. Consuming the app’s MobX store and the app’s config — inject('store', 'config')
  2. Using this config to determine the relevant app — withIsApplicationA(config)
  3. Taking the two possible MobX actions from the store based on the function names passed as arguments (and checking that they actually exist)
  4. Choosing the correct one of these two and passing it down as a prop named according to the propName argument

Using this conditional request HOC, along with many other HOCs both custom and from third-parties, we established a common shape for our components: small, functional, and injected with a few props dependent on which registration flow it was.

This pseudo example of the final registration step demonstrates shape. For the user, the final step looks nearly identical for each registration flow but has a different title and uses a different API for the registration request thanks to the withAppName and withConditionalRequest HOCs.

The component itself remains very simple or ‘dumb’ — it doesn’t even know that it’s acting as a final step for two separate registration flows. This kind of simplicity cannot even be achieved using hooks.

Using hooks, this final step component would probably look something like this:

The conditional logic lives inside the component itself — the withAppName and withConditionalRequest hooks. If the time came to refactor and remove this logic, it would mean doing so within the components themselves.

Overall, this project was a refreshing reminder that there is still a place for HOCs in React applications, no matter what version of React is being used.

Tips for using higher-order components

An undeniable downside of a HOC-based architecture is that you end up in ‘wrapper hell’. This is where components become so buried under layers of HOCs that your React DevTools and stack traces become meaningless.

Wrapper hell.

To make this more manageable, add a display name to your custom HOCs. All the recompose HOCs will have this built-in, and recompose even offers methods to set display names yourself: setDisplayName and wrapDisplayName. It won't solve the problem, but it’ll alleviate some of the pain when debugging a stack trace.

--

--