For years, writing front-end code for Odoo meant writing jQuery. That era is over. In current Odoo, jQuery is not part of the core asset bundle (web._assets_core). It is lazy-loaded, on demand, via a separate web._assets_jquery bundle. The practical consequence is blunt: on any given page, the global $ may simply not be defined.
Why your $ isn't there
Custom JavaScript that opens with $(".something").hide() is making a bet that jQuery has loaded. On a backend page that never pulled in the jQuery bundle, that bet loses. You get $ is not defined, and your script halts at that line, taking everything after it down too. The failure is environmental: it depends on what else loaded on that particular page, which makes it intermittent and miserable to reproduce.
If you genuinely need jQuery
Sometimes you are integrating a third-party jQuery plugin and rewriting it isn't on the table. Then load the bundle explicitly and wait for it:
import { loadBundle } from "@web/core/assets";
await loadBundle("web._assets_jquery");
After the await, $ is guaranteed available. This is the legitimate escape hatch, but it is an escape hatch, not a foundation. For your own code, the answer is one of three native patterns.
Pattern 1: backend interactive UI is an OWL component
Anything stateful inside the Odoo web client is an OWL component. You describe the UI as a function of state, mutate the state, and the framework re-renders. You never reach into the DOM to "update" it. The whole jQuery instinct of find an element, change it is replaced by change the state, let it render.
Pattern 2: public pages use the Interaction framework
OWL is the right tool for the backend and overkill for a mostly-static public page that needs a little behaviour. For that, Odoo provides the Interaction framework. You write a small class with a CSS selector and a dynamicContent map that declares event handlers and dynamic attributes, and the framework wires server-rendered markup to behaviour without shipping the full component runtime. This is the correct home for the kind of light DOM glue that used to be a jQuery snippet at the bottom of a template.
Pattern 3: a genuine one-off uses native DOM
If it really is a single line, the browser's own API is the answer: document.querySelector(".x"), element.classList, element.addEventListener. Modern DOM APIs cover almost everything jQuery was originally adopted to paper over. No bundle, no dependency, no lazy-load.
The OWL discipline that trips people up
Engineers coming from jQuery hit the same few walls. They are worth naming:
- Hooks belong in
setup().useService,useState,useRefmust be called insidesetup(), not as class fields and not inside event handlers. Calling them elsewhere fails in ways the error message won't explain. - Let the framework clean up. Don't pair a manual
addEventListenerwith a forgotten removal. UseuseExternalListenerfor DOM events anduseBusfor bus events; both detach automatically when the component unmounts. - Don't manipulate the DOM inside a component. Setting
innerHTMLfromonMountedmeans OWL and you are now fighting over the same nodes. Render from state. - Use
browser.setTimeout, notwindow.setTimeout. The wrapped version is mockable in tests and not the source of leaks across unmounts.
And patch() changed too
If you extend core components, note that the patch() function no longer accepts a string name as its second argument. Passing one now throws. The current signature takes the target and the extension object directly and returns an unpatch function. Old patch calls are a hard error, not a warning.
The death of jQuery in Odoo isn't a single migration with a single replacement. It is a move from one library that did everything to three patterns that each do one thing well. Picking the right one (component, interaction, or native) is the whole skill, and it is worth learning deliberately rather than discovering by console error.