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.
Fantastic article and something I can put to use right away. I inherited a spaghetti Shopify site code base and all we have to work with is jQuery.
After working with React for a few years, going back to jQuery seems like magic but you run into the state and source of truth problems all too often as you explain so elegantly.
Will definitely try these methods in the next refactor.
I appreciate that this brings the benefits of state, but that updateState function will massively increase the amount of times jQuery has to search the DOM compared to just writing old fashioned ninja code.
The reason why I say this is because I work with some large legacy projects and if all the document.ready code was refactored and run every time the user triggers an event, the site would grind to a Holt.
Great article. I love that you were able to react-ify ye olde pasta.
Nah, man.
What you need to do is phase out jQuery – if you create more dependencies on it, you will never get rid of it.
“No dependencies”, okay, but you’re allowed to add new code, obviously?
So don’t add “dependencies”, just use an engine that’s small enough to own and copy/paste it.
For example, dot-dom is basically React in 512 bytes – stateful components with life-cycle events and virtual DOM:
https://github.com/wavesoft/dot-dom
It does have quite modern browser requirements, but it’s easy to downgrade with Babel and a few quick changes.
Alternatively there’s Superfine, which is just a simple virtual DOM in about 300 LOC:
https://github.com/jorgebucaran/superfine
Neither of these have any potential for security issues, but they enable you to move towards modern UI patterns incrementally, and they don’t have to be “dependencies”.
Rasmus, what you’re not seeing is that when I say “no dependencies”, I really mean it. Node is a dependency, and we are not permitted to install it. It’s not about size, it’s about freedom.
@merid if you really mean “no dependencies” then you can’t write any code, because the site will be dependent on the code you have written just like you are dependent on the code that already exists that you have to work around. For example if you rewrite React character by character, is that a dependency or your code?
Dude, if I could do that, and make it backwards compatible to IE9, I wouldn’t be working here :)
I don’t get the ire against jQuery. Its historical importance to the web as a unifying API across web browsers that were incapable (and still are incapable) of following standards and allowing a seamless cross platform experience is not to be understated. Why, it probably single-handled help usher in ‘Web 2.0’
And honestly, the modern gripe about its size given the swelling and bloated nature of JS bundles? Pots and kettles come to mind.
For small projects, chucking in a CDN-based JQuery and writing away is perfectly fine. The only thing that is now starting to make this almost unnecessary is svelte.
I love everything about this. Creative and pragmatic. Here’s to all the devs out there keeping the older parts of the web running smoothly for us. :)
In recent versions of jQuery you can just use
data()
to access the data attributes directly. No need to use getAttribute() for them anymore.You don’t want to do that in this example, and in my opinion, ever. .data() only loads values when the page loads, and caches them. If you mutate any data-* attributes in your code, your javascript won’t see the updates.
https://api.jquery.com/data/#data-html5
Using data() and [data] are really two different things with a small connection. I can see why you are limited from using data() in this project, but for people lucky enough to have full control over the code and don’t already use another library, using data() definitely has it’s place.
..Building a prototype webgl app where the state is the DOM ..no shadow or mirror DOM ..just get & set directly from the DOM. Mainly because im mixing several libs plus jquery & vanilla js fairly willy nilly ..sometimes in the same function!! ..nice to know I can look you up when it comes time to clean it all out :)
Great stuff.
I’m quite confused why this is so controversial.
I used to do some stuff on a SharePoint 2010 intranet site (which they’re still using) and this pattern would have come in really handy then.
Only thing I might do differently is only updating an element when the value has actually changed. e.g.
if ($(elem).text() != State[tag]) { $(elem).text(State[tag]) }
Hey bro, I loved this article! I even got inspired to extend your example and create a little bit more general solution! :D
https://jsfiddle.net/jeyssonguevara/L7gmqsty/2/
The name ‘unspaguetti’ is just there for fun, it should be changed for something shorter XD
Here is a small sample if what that experiment can do:
I know you only provide examples, but it might be worth caching the selectors you use within the updateState function. I can’t imagine searching the Dom for everything every time the user triggers any event is a good idea.
Nicely done! I’ll admit I’m almost excited to refactor some jQuery.
Amazing how a shift in perspective can improve undesirable circumstances (broader life lesson for all? Perhaps)
Looks like a fairly controversial topic. I don’t quite understand the justification for going this route. It sounded like it was because you can’t trust the DOM state… but then you wrote code that reads from the DOM. So either you can trust it or you can’t. It seems like you’re just used to doing things the React way and want to continue working that way. There’s nothing inherently wrong about that. Just remember that whoever gets this project next is going to have to deal with the original “spaghetti-fied” code plus your “react-ified” code and they will probably be in an even worse situation than you.
If the legacy code is really as bad as you make it sound, there is a potential issue with how this is setup. You are only updating your State object when events are fired. There is a chance other code can change the values without firing events. So you’re right back in the boat of having something you can’t trust as gospel. If you run the updateState() function right before you need to work with value you might as well just read directly from the DOM at that point and don’t need any event listeners that do updateState().
Or maybe there is something I am completely missing here?
The approach I demoed here assumes that your DOM is untrustworthy, but your state object is trustworthy. The only thing this code pulls in from the dom is templating tags, because you need something to hook onto, but otherwise all the data lives in the state object and gets pushed out to the view. If your attributes or state object is being remotely mutated, this approach obviously isn’t going to work, and you have bigger problems than a pattern like this can address.
Why not using web components?
They’re pretty well supported, so no more dependancy and your code will be well organized.
Well everything is fine wanting to replace a jQuery build application with React,Vue, Angular etc. I’m all in. But! Don’t do it on existing jQuery projects. Most of the time it’s not only jQuery, There are always jQuery plugins used. Plugins like datatables.js for enabling table lookup/filtering/editing, select dropdown plugins, date picker plugins graph plugins, ,dom manipulation plugins etc etc. This adds to the workload of converting an existing jQuery project. Not the mention data coming from backend which is another ‘issue’ and, also depending on what backend is used, will make converting easy or hard. Before you know it, you are 10 months further on what should be an ‘easy’ conversion end is a conversion that costs hell of a lot more than estimated.
There is really no shame in maintaining a current project that is based on jQuery. if you need to ‘convert’ existing jQuery project, the easiest method is to start project ‘MK2’ and use the desired framework from scratch.
six years from now there is a new better framework called ‘FizleJuice’ and then you should should replace ‘now framework’ for FizleJuice and… hey Dejavu!
;)
Hello,
Best one…Thanks for sharing this content, as a developer it is really helpful for me