I can hear you crying out now: “Why on Earth would you want to use jQuery when there are much better tools available? Madness! What sort of maniac are you?” These are reasonable questions, and I’ll answer them with a little bit of context.
In my current job, I am responsible for the care and feeding of a legacy website. It’s old. The front-end relies on jQuery, and like most old legacy systems, it’s not in the best shape. That alone isn’t the worst, but I’m working with additional constraints. For example, we’re working on a full rewrite of the system, so massive refactoring work isn’t being approved, and I’m also not permitted to add new dependencies to the existing system without a full security review, which historically can take up to a year. Effectively, jQuery is the only JavaScript library I can use, since it’s already there.
My company has only recently come to realize that front-end developers might have important skills to contribute, so the entire front end of the app was written by developers unaware of best practices, and often contemptuous of their assignment. As a result, the code quality is wildly uneven and quite poor and unidiomatic overall.
Yeah, I work in that legacy codebase: quintessential jQuery spaghetti.
Someone has to do it, and since there will always be more legacy code in the world than greenfield projects, there will always be lots of us. I don’t want your sympathy, either. Dealing with this stuff, learning to cope with front-end spaghetti on such a massive scale has made me a better, if crankier, developer.
So how do you know if you’ve got spaghetti jQuery on your hands? One reliable code smell I’ve found is a lack of the venerable old .toggle()
. If you’ve managed to successfully not think about jQuery for a while, it is a library that smooths cross-browser compatibility issues while also making DOM queries and mutations incredibly easy. There’s nothing inherently wrong with that, but direct DOM manipulation can be very hard to scale if you’re not careful. The more DOM-manipulation you write, the more defensive against DOM mutation you become. Eventually, you can find yourself with an entire codebase written that way and, combined with less-than-ideal scope management, you are essentially working in an app where all of the state is in the DOM and you can never trust what state the DOM will be in when you need to make changes; changes could swoop in from anywhere in your app whether you like it or not. Your code gets more procedural, bloating things up with more explicit instructions, trying to pull all the data you need from the DOM itself and force it into the state you need it to be in.
This is why .toggle()
is often the first thing to go: if you can’t be sure whether an element is visible or not, you have to use .show()
and .hide()
instead. I’m not saying .show()
and .hide()
should be Considered Harmful™, but I’ve found they’re a good indication that there might be bigger problems afoot.
What can you do to combat this? One solution my coworkers and I have found takes a hint directly from the reactive frameworks we’d rather be using: observables and state management. We’ve all found that hand-rolling state objects and event-driven update functions while treating our DOM like a one-way dataflow template leads to more predictable results that are easier to change over time.
We each approach the problem a little differently. My take on reactive jQuery is distinctly flavored like Vue drop-in and takes advantage of some “advanced” CSS.
If you check out the script, you’ll see there are two different things happening. First, we have a State
object that holds all of the values for our page, and we have a big mess of events.
var State = {
num: 0,
firstName: "",
lastName: "",
titleColor: "black",
updateState: function(key, value){
this[key] = value;
$("[data-text]").each(function(index, elem){
var tag = $(elem).attr("data-tag");
$(elem).text(State[tag]);
});
$("[data-color]").each(function(index, elem){
var tag = $(elem).attr("data-tag");
$(elem).attr("data-color", State[tag]);
});
}
};
I’ll admit it, I love custom HTML attributes, and I’ve applied them liberally throughout my solution. I’ve never liked how HTML classes often do double-duty as CSS hooks and JavaScript hooks, and how if you use a class for both purposes at once, you’ve introduced brittleness into your script. This problem goes away completely with HTML attributes. Classes become classes again, and the attributes become whatever metadata or styling hook I need.
If you look at the HTML, you’ll find that every element in the DOM that needs to display data has a data-tag
attribute with a value that corresponds to a property in the State
object that contains the data to be displayed, and an attribute with no value that describes the sort of transformation that needs to happen to the element it’s applied to. This example has two different sorts of transformations, text and color.
<h1 data-tag="titleColor" data-color>jDux is super cool!</h1>
On to the events. Every change we want to make to our data is fired by an event. In the script, you’ll find every event we’re concerned about listed with its own .on()
method. Every event triggers an update method and sends two pieces of information: which property in the State
object that needs to be updated, and the new value it should be set to.
$("#inc").on("click", function(){
State.updateState("num", State.num + 1)
});
$("#dec").on("click", function(){
State.updateState("num", State.num - 1)
});
$("#firstNameInput").on("input", function(){
State.updateState("firstName", $(this).val() )
});
$("#lastNameInput").on("input", function(){
State.updateState("lastName", $(this).val() )
});
$('[class^=button]').on("click", function(e) {
State.updateState('titleColor', e.target.innerText);
});
This brings us to State.updateState()
, the update function that keeps your page in sync with your state object. Every time it runs, it updates all the tagged values on the page. It’s not the most efficient thing to redo everything on the page every time, but it’s a lot simpler, and as I hope I’ve already made clear, this is an imperfect solution for an imperfect codebase.
$(document).ready(function(){
State.updateState();
});
The first thing the update function does is update the value according to the property it receives. Then it runs the two transformations I mentioned. For text elements, it makes a list of all data-text
nodes, grabs their data-tag
value, and sets the text to whatever is in the tagged property. Color works a little differently, setting the data-color
attribute to the value of the tagged property, and then relies on the CSS, which styles the data-color
properties to show the correct style.
I’ve also added a document.ready
, so we can run the update function on load and display our default values. You can pull default values from the DOM, or an AJAX call, or just load the State object with them already entered as I’ve done here.
And that’s it! All we do is keep the state in the JavaScript, observe our events, and react to changes as they happen. Simple, right?
What’s the benefit here? Working with a pattern like this maintains a single source of truth in your state object that you control, you can trust and you can enforce. If you ever lose trust that your DOM is correct, all you need to do is re-run the update function with no arguments and your values become consistent with the state object again.
Is this kind of hokey and primitive? Absolutely. Would you want to build an entire system out of this? Certainly not. If you have better tools available to you, you should use them. But if you’re in a highly restrictive legacy codebase like I am, try writing your next feature with Reactive jQuery and see if it makes your code, and your life, simpler.