DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Declaring your business πŸ‘” logic like React βš›οΈ
Naman Malhotra
Naman Malhotra

Posted on • Updated on

Declaring your business πŸ‘” logic like React βš›οΈ

I published a npm library that allows you to write your conditional business logic in a declarative way like React does. It is currently being used at HackerRank in production and has made our code more maintainable and readable especially when it comes to complex rendering logic.

Before I dive into what this library does, let's understand the difference between the two approaches.

What is the difference between declarative and imperative programming?

According to wiki:

In Imperative Programming paradigm you manipulate the state of the program directly using statements to achieve the desired behavior.

In Declaraive Programming paradigm you focus on what the program should achieve rather than how to achieve it.

I don't get your gibber jabber, tell me in code.

Imagine a simple UI component, such as a "Submit" button which submits form data. While we wait for the request to return from the server we would want to disable the button.

if(isLoading) {
  button.disabled = true;
}
Enter fullscreen mode Exit fullscreen mode

To achieve the disabled state, manipulate the UI like this ^.

In contrast, the declarative approach would be:

return <Button disabled={isLoading} />;
Enter fullscreen mode Exit fullscreen mode

Because the declarative approach separates concerns, this part of it only needs to handle how the UI should look in a specific state, and is therefore much simpler to understand.

so coming back to the point:

So, how can you declare your conditional business logic like React?

A couple of months ago, I published a library on npm called match-rules that can turn your code from:

function isUserLocked(user: User) {
  // some messed up legacy locking logic data from backend
  if (
    user?.flagged === true &&
    user?.blocked === true &&
    (user?.is_locked === 0 || user?.is_locked === "LOCKED") && 
    user?.profile?.account_deleted === true
  ) {
    return true;
  }

  return false;
}

function showWarning(user: User) {
  return isUserLocked(user) && user?.show_warning;
}

function showAccountDisabled(user: User) {
  return isUserLocked(user) && user?.profile?.show_account_disabled;
}

if (isUserLocked(user)) {
  // render account locked UI
}

if (showWarning(user)) {
  // render warning UI or something else
}

if (showAccountDisabled(user)) {
  // render account disabled UI
}
Enter fullscreen mode Exit fullscreen mode

to

import matchRules from 'match-rules';

import {
  IS_USER_LOCKED_RULE,
  SHOW_WARNING_RULE,
  SHOW_ACCOUNT_DISABLED_RULE
} from './rules';

// user object can be served from the app state 
if (matchRules(user, IS_USER_LOCKED_RULE)) {
  // render user locked UI
}

if (matchRules(user, SHOW_WARNING)) {
  // show warning UI
}

if (matchRules(user, [IS_USER_LOCKED_RULE, SHOW_ACCOUNT_DISABLED_RULE])) {
  // render account disabled UI
}
Enter fullscreen mode Exit fullscreen mode

where your rules can reside in rules.js with an object like structure:

export const IS_USER_LOCKED_RULE = {
  flagged: true,
  blocked: true,
  is_locked: (value, sourceObject) => value === 0 || value === "LOCKED",
  profile: {
   account_deleted: true,
  },
};

export const SHOW_WARNING_RULE = {
  ...IS_USER_LOCKED_RULE,
  show_warning: true,
};

export const SHOW_ACCOUNT_DISABLED_RULE = {
  profile: {
    show_account_disabled: true,
  },
};
Enter fullscreen mode Exit fullscreen mode

Let's have a look at a couple of advantages of declaring the conditional logic in a declarative way:

  • It reduces cognitive complexity considerably: if you observe IS_USER_LOCKED_RULE it vividly describes what all conditions need to be met as compared to isUserLocked function. The object structure is more readable.

  • You can compose and pass multiple rules: compose/extend multiple rules to form new rules, which avoids repetition. Also, you can pass multiple rules object as an Array of rules.
    Multiple rules are by default are compared with and operator, you can also compare them using or operator by passing { operator: 'or' } prop in options. You can read more about this on docs.
    We composed a new rule by extending IS_USER_LOCKED_RULE

export const SHOW_WARNING_RULE = {
  ...IS_USER_LOCKED_RULE,
  show_warning: true,
};
Enter fullscreen mode Exit fullscreen mode

In an Object-based structure, you can easily extend rules without introducing complexity.

  • Save time with unit tests: you don't have to write specific unit tests for RULES object, at max you can do snapshot testing if you wish to. match-rules handle rule matching logic for you, so you don't have to write specs.

  • Write your logic in its true form: since the data structure in a JavaScript of the source is mostly an Object. It makes sense to define your conditions in an Object as well, this way you don't have to destructure the Object. It especially helps if your object is deeply nested.
    In our example, the status key was nested inside the profile object. The RULE we wrote had the same structure and with the expected value.

profile: {
  account_deleted: true,
},
Enter fullscreen mode Exit fullscreen mode
  • Handle complex conditions using functions: so far it is capable to handle any condition since you can write your own functions in the rule. when it encounters a function it passes the value (as the first parameter) and original source object (as the second parameter) from the source to that function matching the corresponding key of that level. The same happened in the above example when it encountered the is_locked key.
is_locked: (value, sourceObject) => value === 0 || value === "LOCKED"
Enter fullscreen mode Exit fullscreen mode

Using a combination of key's value and original source object you can handle complex conditions. You have to write spec just for this function.

So, I consolidated my thoughts into a library and called it match-rules

Add match-rules

Think of it more of a practice as it works on the principles we just discussed.

If I have to give a precise definition, it would be:

match-rules is a tiny (1kB GZipped) zero dependency JavaScript utility that lets you write your conditional business logic in a declarative way.

It can be used with feature flags, complex conditions, conditional rendering, and the rest is your imagination.

How it works?

The way match-rules work is, it checks for each key in the RULES object for the corresponding key in the source object. It does so by treating the RULES object like a tree and recursively going through each key until there are no nodes left. Rules generally contain a small subset of keys from the source object, it can be an exact replica of the complete object as well, with expected values.

How to use it and detailed documentation:

yarn add match-rules or npm install --save match-rules

API of matchRules looks like this:

import matchRules from 'match-rules';

// returns a boolean value.
matchRules(
  sourceObject, // can be any object with data.
  RULES_OBJECT, // you can also pass multiple rules in an array [RULE_ONE, RULE_TWO],
  options, // (optional)
);

const options = {
  operator: 'and', // (optional, default: 'and') in case of multiple rules you can pass 'and' or 'or'. In the case of 'or,' your rules will be compared with 'or' operator. Default is 'and'
  debug: true, // (optional, default: false) when debug is true, it logs a trace object which will tell you which rule failed and with what values of source and rules object.
};

// NOTE: all the rules inside a single rule are concatenated by 'and' operator by default.
Enter fullscreen mode Exit fullscreen mode

For examples and detailed documentation please visit the Github repo.

People Involved

A big thanks to

Sudhanshu Yadav for code review, design discussion, feedback, and coming up with the name match-rules :p
Aditya for reviewing this article thoroughly, constructive feedback, and recommending this blog site.
Vikas for reporting a critical bug and feedback for this article.

Current Status

It is stable with 100% code coverage and is currently being used at HackerRank in Production.

match-rules does not have any dependency and it is only 1kB (GZipped) in size.

Feel free to send across a Pull Request if it doesn't fit your use-case.

So next time when you are about to write conditional rendering logic. Try this library. You'll thank me later xD.

Show some support, leave a star if you find it useful.
GitHub: https://github.com/naman03malhotra/match-rules
npm: https://www.npmjs.com/package/match-rules
Live Example: https://stackblitz.com/edit/match-rules

Also checkout my other open-source project, a simple binge-watching chrome extension, for auto skipping intro for Netflix and Prime.

If you want to discuss match-rules, comment below or reach out at Twitter or LinkedIn.

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.