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;
}
To achieve the disabled state, manipulate the UI like this ^.
In contrast, the declarative approach would be:
return <Button disabled={isLoading} />;
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
}
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
}
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,
},
};
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 toisUserLocked
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 withand
operator, you can also compare them usingor
operator by passing{ operator: 'or' }
prop in options. You can read more about this on docs.
We composed a new rule by extendingIS_USER_LOCKED_RULE
export const SHOW_WARNING_RULE = {
...IS_USER_LOCKED_RULE,
show_warning: true,
};
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, thestatus
key was nested inside the profile object. The RULE we wrote had the same structure and with the expected value.
profile: {
account_deleted: true,
},
- 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"
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
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.
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)