Top 20 gotchas, widget lifecycle quick-reference, AMD module directory, dojox index, Dojo 1.x→2 migration guide, and recommended tools
These are the bugs that cost senior developers the most hours on real Dojo projects. Each one has a short explanation and the fix.
BorderContainer, TabContainer, dgrid — all require startup() after insertion into the DOM. Missing it causes zero-height containers. Always call myWidget.startup() and then myContainer.resize().
dgrid 1.x expects a dstore collection, not a dojo/store object. Passing dojo/store/Memory silently produces an empty grid. Use dstore/Memory with dstore/Trackable.
All subscribers execute immediately in the publish call stack. If a subscriber throws, the remaining subscribers are skipped. Wrap subscriber bodies in try/catch for production code; never rely on async behaviour from publish.
Handles returned by topic.subscribe(), dojo/on(), and dojo/aspect must be registered with this.own() inside widgets. Otherwise they survive widget destruction and fire against null DOM nodes.
Calling dialog.show() a second time on a form Dialog can leave stale validation state and broken focus management. Create a new Dialog instance per open; destroy it in onHide.
widget.isValid() returns a boolean without showing errors. widget.validate(true) also shows the red border and tooltip. Use isValid() to aggregate, then validate(true) to make errors visible.
parseOnLoad: true scans the entire DOM on load for data-dojo-type attributes. If you also create widgets programmatically, double-parsing causes widget ID conflicts. Set parseOnLoad: false and call parser.parse() only on specific nodes.
If you override postCreate, startup, or destroy, you must call this.inherited(arguments). Omitting it breaks the MRO chain — mixins up the chain never run their own hooks.
Returning a value from a .then() callback wraps it in a resolved Deferred for the next handler. Returning nothing passes undefined. Always explicitly return the next promise in chains, or you'll lose the resolved value silently.
Dojo 1.x runs in ES5 environments. Arrow functions are unavailable. Always use lang.hitch(this, fn) or lang.hitch(this, "methodName") to bind this inside callbacks and event handlers.
declare([A, B, C], {...}) — C3 MRO processes left to right. If A and B both define postCreate, B's runs first in this.inherited() chain. Draw your MRO graph before mixing more than two classes.
Without extraLocale: ["fr"] in dojoConfig, the French NLS bundle is never fetched at startup. A runtime switch will fail in production (built app) because the bundle was never included in the build layers.
renderCell must return a DOM Node, not an HTML string. Returning a string causes it to be rendered as text (escaped). Use document.createElement() or domConstruct.create().
Attach points (e.g., data-dojo-attach-point="myNode") are resolved in buildRendering(). They are available in postCreate(). But widget children placed via attach points are not started — call their startup() explicitly.
destroy() cleans up the widget itself but leaves child widgets in memory. destroyRecursive() destroys the widget and all dijit descendants. Use destroyRecursive() on container widgets; destroy() on leaf widgets.
xhr.post(url, { data: JSON.stringify(obj) }) does NOT set Content-Type: application/json automatically. Add headers: { "Content-Type": "application/json" } explicitly, or the server will receive the body as form data.
dojo/domReady! fires when the DOM is parsed, before images and stylesheets finish loading — same as DOMContentLoaded. window.onload waits for everything. Use domReady! for widget setup; use window.onload only if you need fully-loaded resources (e.g., image dimensions).
Using customBase: true without staticHasFeatures can produce a larger dojo.js than expected because dead-code elimination doesn't fire. Always pair customBase with the full staticHasFeatures table (see Phase 9).
Setting href on a ContentPane fetches HTML via XHR and parses widgets inside. Setting content sets innerHTML and does NOT parse widgets. If your pane content has data-dojo-type attributes, use href or call parser.parse(pane.containerNode) after setting content.
A dijit/form/Select created programmatically with an empty options array will show a blank dropdown. Always provide at least one option at construction time, even a placeholder { value:"", label:"Select…" }, or call select.startup() after adding options.
The eight lifecycle hooks in creation order. The "when to use" column is the single most-referenced piece of information for Dojo widget development.
this.domNode. Template is parsed, attach points resolved. Don't add event listeners here.this.inherited(arguments).own() handles auto-removed). Does NOT destroy child widgets.| Question | postCreate | startup |
|---|---|---|
| Is widget in live DOM? | No | Yes |
| Can read layout dimensions? | No (all zero) | Yes |
| Create child widgets? | Yes | No (too late for most) |
| Attach event listeners? | Yes | Rarely |
| Call resize()? | No | Yes (layout containers) |
The 50 most-used Dojo modules organized by category. Memorize these and you'll write Dojo fluently.
dojox contains experimental and domain-specific extensions. They are stable enough for production use but have less polish than core dojo/dijit. Always check the specific version for your app — APIs changed significantly between 1.x minor versions.
at(model, "prop") for declarative two-way binding in templatesOnly include dojox modules you actually use in your build layers. The full dojox package is large (~4 MB unminified). Selectively adding charting or mvc to one build layer keeps your production bundles lean.
Dojo 2 (released as "dojo/framework" in 2019) was a complete rewrite with TypeScript and a React-like VDOM. It is NOT backward-compatible with Dojo 1.x. Most Dojo 1.x shops migrated to React, Vue, or Angular rather than Dojo 2. This table maps the Dojo 1.x concept to its modern equivalent.
Most Dojo 1.x applications are internal enterprise tools with 10+ year lifespans. The pragmatic migration path is: wrap Dojo widgets as web components first (using customElements.define), then incrementally replace sections with React/Vue at the page level, without a full rewrite. The pub/sub architecture you built throughout this tutorial makes incremental migration tractable — React components can publish and subscribe to the same topic bus during the transition period.