Code as Data in JavaScript — Data Structure Driven UI
This is in a similar vein to an older post I had written: Code as Data — encoding business rules in F#. With modern JavaScript, you can apply a very similar technique on the frontend and structure your UI as data.
Problem Statement
On ClearTax’s GST platform, users can submit their GST data to the government’s backend. In response, they might get an error report in case there were any validation errors — this error report is basically a JSON response.
Here’s a sample of a response we could receive from calling the government’s API:
{
"b2b" : [
{
"error_code" : "ERR1",
"invoices" : [{payload data...}]
},
{
"error_code" : "ERR2",
"invoices" : [{payload data...}]
}
]
}
The possible number of errors is huge and continuously changing, and the level of help the user needs will depend on the type of error, the type of GST return being filed, etc.
Our goal was to show detailed, context-sensitive help to our users in case they receive an error report. Some requirements were:
- Highlight the exact error: translate opaque error codes to human-readable messages, and highlight fields in the source data which caused it.
- Make it easy to maintain this, and make it especially easy to add messaging for new types of errors that can show up in the future. (Since these are evolving systems.)
- Have a structured, consistent approach instead of writing a lot of custom logic.
- Be able to support different levels of detail in the help we give to the user. Common types of errors may have a detailed treatment.
Solution
This is where I turned to a trusty ‘code as data’ approach. We first defined a data structure as follows in a constants file:
{
ERR1: [
"Short description of the error",
<ul>
<li>Help text</li>
<li>Additional help text</li>
</ul>,
(item) => {
return <span>Counter Party GSTIN (<code>{item.ctin}</code>) in invoice <code>{item.inum} does not exist, please check if there was a typo in entering it.</span>;
},
],
ERR2: ...
}
It’s a dictionary indexed by unique error code. Each error is described as a list of 3 items:
- A short string description.
- A (static) piece of JSX markup showing additional help, which can be as long or as short as you need it to be.
- A function which returns markup depending on the current context (eg: invoice on which error was received). This function can use the current context to highlight the exact field on which which the error occurred, highlight the incorrect value, etc.
This was paired with a render function like this:
function renderError(errorCode, items) {
const [description, genericHelp, contextualHelpFn] = errors[errorCode];
return (
<div>
<h3>{errorCode} — {description}</h3>
{genericHelp}
<table>
{items.map(item => (
<tr>
<td>{item.inum}</td>
<td>{contextualHelpFn(item)}</td>
</tr>
)}
</table>
</div>
);
}
This example has been stripped down and simplified, but I hope you get what this does:
- We provide a high level description of each error message type.
2. We can dynamically populate the UI with deeply contextual error messages for particular transactions (invoices, etc) present in your tax return.
3. Since the function that generates contextual help is just writing code, we can be as detailed as we want. Some functions deal with 10s of corner cases for common types of error messages, while some rare errors have a more generic message.
4. You can unit test this very easily!
5. We can separate the ‘configuration’ or the ‘data’ driving the help screen (which might be large) from the ‘code’ driving it (which can fit on a single screen).
- Adding new error messages is easy. If you spot a new error, all you have to do to handle it is add a new key to the dictionary with a helpful description, the rest of the system will handle the rest.
- This can scale up to a large number of error types. If you have a large number of errors, you can easily split this declaration.
The main limitation of this approach is that due to the contextual help function, we’ve mixed code & data in our constants file and cannot treat this as a dumb JSON file loaded at runtime.
The simplest thing to do is to just it include it as a constants file in your frontend code and make it part of your bundle. It’s possible to split this as a separate JavaScript bundle, but that will require you to fiddle with your webpack config, etc — just keeping it as a constant will be the easiest solution.
Conclusion
While this is a very simple approach, but I wanted to share it because it’s something that can give you a lot of leverage for a surprisingly small amount of effort.
Treating more and more of your application as ‘data’ makes it possible to simplify your application, and deal with larger use-cases in a very generic manner.
Questions, comments? Please reach out to me via email or twitter.
PS: ClearTax is hiring. Please send an email to ankit@cleartax.in if you’re interested in joining us!