Tasteful JQuery
Published over 1 year ago
You should know something: you don't hate JQuery.
You might think you hate JQuery, but what you really hate is unreadable, unmaintainable, unpredictable, untestable code.
You hate:
- When a piece of application state doesn't have a single source of truth. It might live on the server, and in a JS variable, and in a DOM element's data attribute or CSS class, all at the same time. The source of truth might even change at some point based on other application state.
- When you can't track or debug app state mutations
- When you don't know where a block of HTML is coming from. Is it coming from the server? Did someone insert it with plain JS? Did JQuery parse an HTML string to create it?
- When it takes longer to mock app state than it does to test the functionality you're actually interested in
This is distasteful JQuery. There's a better way!
People write unreadable, unmaintainable, unpredictable, untestable JQuery. There's a better way!
I've written mountains of content about Vue best practices, and applied a lot of the same ideas to React apps, but in reality, I've spent a ton of professional time working with JQuery apps.
Based on what I've learned about Vue and React, and all my experience reading difficult JQuery code, here's what tasteful JQuery looks like.
The Better Way
The trick is to identify the goals that modern frontend frameworks are designed to achieve, then try to achieve those goals within the constraints of a typical legacy JQuery app.
Goals:
- There must be one source of truth for any given piece of app state.
- UI must be a function of state. The UI is never the source of truth for app state.
- We must be able to track and debug state changes.
- We'd like a reasonable amount of type safety.
- The road to hell is paved with side effect code, but side effects are unavoidable. The UI itself is a side effect of state change. So, side effect code must follow consistent patterns to be more predictable.
Constraints:
- No modules
- No dependencies (except JQuery)
- No compilation or transpilation
Reactivity
A reactivity system is the key to almost all of our goals. As soon as you can track and react to state changes, you gain the ability to schedule all of your side effect code.
Scheduled side effects run on any state change, so they force you to consider all possible application states, and to write code that handles all edge cases.
Since we're not using modules or dependencies in our JQuery app, we're gonna have to roll our own reactivity system. My solution to this problem is createSyncable
, a copy/paste-able amount of plain JS with surpisingly great type safety, courtesy of JSDoc comments.
Here's a live demo with documentation:
Rendering
createSyncable
is as minimalist as reactivity gets, but it's hilariously powerful, as long as you follow smart patterns for rendering your UI.
To explore that, I wrote TodoMVC with createSyncable
and JQuery.
The live demo is below! But here are the patterns I want to call out that actually make this code nice to work with:
I fully committed to UI as a function of state. The only place I allowed myself to programmatically update the DOM was inside of
createSyncable.sync
callbacks, which schedule code to run when app state changes.If app state doesn't change, I don't update the DOM.
I confined all side effects (i.e. event listeners,
createSyncable.sync
code, and HTML insertion/deletion) to dedicated functions, named for their logical concern.I also prefixed the function names with
setup
, drawing inspiration from Vue'ssetup
function, which is similarly responsible for scheduling side effects and rendering based on state change.I used the
template
element to scaffold reusable markup that I needed to dynamically clone and/or insert, becausetemplate
is great.I used JQuery for DOM patching only. JQuery, compared to the
template
tag, is not a good tool for creating, cloning, and/or inserting new HTML elements, but it's still a nice, concise layer on top of clunky DOM APIs when you need to patch DOM elements in place.I could have done more in this area. For example, I could have written reusable JQuery-based utility functions to bind reactive state to attributes, quickly set up two-way binding, or conditionally display certain elements.
If I wanted to go all in on
createSyncable
+ JQuery, I'd write these functions, taking inspiration from the affordances I wrote for Baleada Vue Features.That actually sounds kinda fun. I might do it later 😅
Most importantly, I followed functional programming principles. Functions should be pure—no side effects—until it's impossible to do so. When writing UI code, you have to cross that line pretty quickly, so you need to stay consistent and organized when dealing with mutable values and side effects.
Mutable values that affect the way you render the UI should be reactive state. Side effects that commit changes to the DOM should be clearly and explicitly scheduled to run on state change, not at some random synchronous point in the program.
Code is organized by logical concern. Side effects that should run when a given piece of state changes are all co-located. You will absolutely never see a random click handler that calls ten different functions to affect distant, unrelated parts of the UI.
Enjoy!