<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Arik Wald</title>
    <description>The latest articles on DEV Community by Arik Wald (@arik_wald_87b25f4b2939e11).</description>
    <link>https://dev.to/arik_wald_87b25f4b2939e11</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4175864%2F30f681b8-1c51-4b96-8d6a-87fb456d4e3a.png</url>
      <title>DEV Community: Arik Wald</title>
      <link>https://dev.to/arik_wald_87b25f4b2939e11</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arik_wald_87b25f4b2939e11"/>
    <language>en</language>
    <item>
      <title>JOPS Comes to WordPress: Class-Based SPAs Inside Your Pages</title>
      <dc:creator>Arik Wald</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:06:38 +0000</pubDate>
      <link>https://dev.to/arik_wald_87b25f4b2939e11/jops-comes-to-wordpress-class-based-spas-inside-your-pages-44na</link>
      <guid>https://dev.to/arik_wald_87b25f4b2939e11/jops-comes-to-wordpress-class-based-spas-inside-your-pages-44na</guid>
      <description>&lt;p&gt;We made a small detour before finishing our Electron integration and made a short stop for WordPress integration 🙂 A JOPS SPA can now run inside a WordPress page, next to the theme and the page's other content.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;jops-core 1.1.2&lt;/strong&gt;, a new JOPS WordPress plugin adds a &lt;code&gt;[jops]&lt;/code&gt; shortcode. Drop it into a page built with Elementor or the block editor, and a complete class-based single-page app renders there, below the theme's header and next to the rest of the page. WordPress stays the CMS and the backend. JOPS adds the parts of the page that behave like an application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why put an app inside a WordPress page
&lt;/h2&gt;

&lt;p&gt;WordPress runs a large share of the web, and many of those sites eventually need more than content: a quote calculator, a customer portal, a booking flow, an internal dashboard behind the login. The usual options are a heavy plugin that almost fits, or a separate app on its own subdomain that loses the site's theme and menu.&lt;/p&gt;

&lt;p&gt;JOPS offers a third option. The app lives in a folder of the site, keeps the site's look, talks to WordPress over its REST API, and needs no build step. Views are classes with an Android-style lifecycle, so the same structure that works in the browser, in hybrid mobile apps and inside Next.js now works inside WordPress too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to run JOPS on WordPress
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Embedded in a page&lt;/th&gt;
&lt;th&gt;Independent SPA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;The app renders inside a WordPress page, next to the page's content&lt;/td&gt;
&lt;td&gt;A complete JOPS app with its own &lt;code&gt;index.html&lt;/code&gt;, opened at &lt;code&gt;https://&amp;lt;site&amp;gt;/&amp;lt;folder&amp;gt;/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How&lt;/td&gt;
&lt;td&gt;The JOPS plugin and the &lt;code&gt;[jops]&lt;/code&gt; shortcode, or an Elementor HTML widget&lt;/td&gt;
&lt;td&gt;Upload the app folder; no plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shares the theme, menu and page&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both work because of how WordPress handles requests: it only processes URLs that are not existing files or folders. A JOPS app in a folder of the WordPress root is served as static files, and WordPress never gets in the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the plugin, three ways
&lt;/h2&gt;

&lt;p&gt;Most WordPress sites don't use npm, so the plugin doesn't require it. Every GitHub release now carries a ready-to-upload &lt;code&gt;jops-wordpress.zip&lt;/code&gt; with the jops-core library included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No npm&lt;/strong&gt; (hosting with wp-admin only, XAMPP, MAMP, LocalWP): download &lt;a href="https://github.com/uidb-dev/jops-core/releases/latest/download/jops-wordpress.zip" rel="noopener noreferrer"&gt;jops-wordpress.zip&lt;/a&gt;, then &lt;strong&gt;Plugins → Add New Plugin → Upload Plugin&lt;/strong&gt; → &lt;strong&gt;Install Now&lt;/strong&gt; → &lt;strong&gt;Activate&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An npm project run by &lt;code&gt;wp-env&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;npx jops-core init --wp&lt;/code&gt; writes the plugin to &lt;code&gt;plugins/jops/&lt;/code&gt; and adds it to &lt;code&gt;.wp-env.json&lt;/code&gt;. &lt;code&gt;npx wp-env start&lt;/code&gt; installs and activates it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An npm project inside a WordPress folder&lt;/strong&gt; (a theme, a plugin, a Bedrock project): &lt;code&gt;npx jops-core init --wp&lt;/code&gt; searches upward for WordPress and writes the plugin to its plugins folder. &lt;code&gt;--wp-root &amp;lt;path&amp;gt;&lt;/code&gt; points to WordPress directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three install the same plugin into &lt;code&gt;wp-content/plugins/jops/&lt;/code&gt;. To update without npm, upload the newer zip and choose &lt;strong&gt;Replace current with uploaded&lt;/strong&gt;; with npm, &lt;code&gt;npx jops-core update&lt;/code&gt; refreshes the plugin and its library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the app to a page
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Put the app folder in the WordPress root&lt;/strong&gt;, the folder with &lt;code&gt;wp-config.php&lt;/code&gt;, for example &lt;code&gt;&amp;lt;root&amp;gt;/jops-spa/&lt;/code&gt;. Upload it, copy it over SSH, or deploy it with your pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use root-based paths&lt;/strong&gt; for everything JOPS loads: the app module, layouts, stylesheets and images.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// /jops-spa/src/App.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jops-core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/jops-spa/src/layout/main.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Add the shortcode.&lt;/strong&gt; In Elementor, add a &lt;strong&gt;Shortcode&lt;/strong&gt; widget (General → Shortcode); in the block editor, use the Shortcode block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[jops app="/jops-spa/src/App.js" id="jops-spa-root" class="jops-spa"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. On the published page, the shortcode outputs the import map, the library in hosted mode, the app's root &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; and a small script that mounts the app once the library has loaded. In the Elementor editor, the widget shows a clear placeholder instead, so the app never fights the editor for the page.&lt;/p&gt;

&lt;p&gt;Prefer no plugin? An Elementor &lt;strong&gt;HTML&lt;/strong&gt; widget can hold the same code by hand, with the library vendored in the app folder. The README has the snippet.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the two systems share a page
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WordPress owns the path, JOPS owns the hash.&lt;/strong&gt; &lt;code&gt;/quotes/&lt;/code&gt; is a WordPress page; &lt;code&gt;/quotes/#details?id=7&lt;/code&gt; is a screen inside the JOPS app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The backend is WordPress.&lt;/strong&gt; The app calls the REST API with &lt;code&gt;fetch("/wp-json/…")&lt;/code&gt; on the same origin, so the login cookie is sent. Requests that act as the logged-in user also need a REST nonce, provided by your theme or plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The theme owns the page's CSS.&lt;/strong&gt; The app's stylesheet applies to the whole page, so scope every rule under the app's root class (&lt;code&gt;.jops-spa .card { … }&lt;/code&gt;) and leave &lt;code&gt;body&lt;/code&gt; and &lt;code&gt;:root&lt;/code&gt; to the theme.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we tested it on
&lt;/h2&gt;

&lt;p&gt;We built and tested each install path on its own WordPress site before the release:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Install path&lt;/th&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zip upload, no npm&lt;/td&gt;
&lt;td&gt;Docker, &lt;code&gt;wordpress:php8.3-apache&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;WordPress 7.1.3, Elementor 4.3.5, Hello Elementor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;init --wp&lt;/code&gt; in a wp-env project&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@wordpress/env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WordPress 7.1.3, PHP 8.3, Elementor 4.4.0 beta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;init --wp&lt;/code&gt; from a theme folder&lt;/td&gt;
&lt;td&gt;Docker, root folder mounted&lt;/td&gt;
&lt;td&gt;WordPress 7.1.3, Elementor 4.3.5, Hello Elementor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On each site the app rendered on the published page, every route switched, images loaded, and the page made exactly one library request with no console errors. We also checked the edge cases: a second &lt;code&gt;[jops]&lt;/code&gt; on the same page, a wrong library URL, the block inserted later by JavaScript, and the Elementor editor's live re-render.&lt;/p&gt;

&lt;p&gt;Testing on real WordPress also found a bug in jops-core itself: the &lt;code&gt;jops:ready&lt;/code&gt; event could fire before the library had finished initialising. It's fixed in 1.1.2, which helps every hosted JOPS app, not only WordPress ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One JOPS app per page.&lt;/strong&gt; A second &lt;code&gt;[jops]&lt;/code&gt; on the same page logs a warning and doesn't start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app runs on the published page&lt;/strong&gt;, not inside the Elementor editor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a folder name that isn't a page slug&lt;/strong&gt; and doesn't start with &lt;code&gt;wp-&lt;/code&gt;: a real folder always wins over a WordPress page at the same address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPress in a subfolder&lt;/strong&gt; (&lt;code&gt;example.com/blog/&lt;/code&gt;) means every path includes it: &lt;code&gt;/blog/jops-spa/…&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch your caches.&lt;/strong&gt; Caching plugins and CDNs may keep serving old copies of the app's static files after an update.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Also new in 1.1.2
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;View.removeSubViews(condition?)&lt;/code&gt;&lt;/strong&gt; removes and destroys every subview, or only those that match a condition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jops:ready&lt;/code&gt;&lt;/strong&gt; now fires after jops-core has fully initialised, so calling &lt;code&gt;mount()&lt;/code&gt; from the listener is safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/uidb-dev/jops-core/releases/latest/download/jops-wordpress.zip" rel="noopener noreferrer"&gt;Download the JOPS WordPress plugin&lt;/a&gt; (always the newest release)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/jops-core" rel="noopener noreferrer"&gt;jops-core on npm&lt;/a&gt;: the full guide is in the README section "WordPress Integration"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/uidb-dev/jops-core" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for issues and discussions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ui-db.com/articles/jops-class-based-spa-framework/" rel="noopener noreferrer"&gt;Introducing JOPS&lt;/a&gt;, if you're new to the framework&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ui-db.com/articles/jops-nextjs-integration/" rel="noopener noreferrer"&gt;JOPS Meets Next.js&lt;/a&gt;, the same idea inside React apps&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next: back to Electron
&lt;/h2&gt;

&lt;p&gt;The detour is over, and the JOPS team is back on the road to the desktop. The Electron integration is in progress, so the same class-based views can power standalone applications on Windows, macOS and Linux. We'll share what we learn, including what doesn't fit.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;JOPS is built and maintained at UIDB, a boutique team of senior engineers who build custom systems with our clients, side by side. If your WordPress site needs an application inside it, or a system that needs to hold up for years, &lt;a href="https://ui-db.com" rel="noopener noreferrer"&gt;we'd be glad to talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>wordpress</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>JOPS Meets Next.js: Class-Based SPAs Inside Server-Rendered Apps</title>
      <dc:creator>Arik Wald</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:18:54 +0000</pubDate>
      <link>https://dev.to/arik_wald_87b25f4b2939e11/jops-meets-nextjs-class-based-spas-inside-server-rendered-apps-28e</link>
      <guid>https://dev.to/arik_wald_87b25f4b2939e11/jops-meets-nextjs-class-based-spas-inside-server-rendered-apps-28e</guid>
      <description>&lt;p&gt;When we shipped hybrid mobile support in JOPS 1.0.13, we said the next step was to examine how JOPS and Next.js could work together. With &lt;strong&gt;jops-core 1.1.0&lt;/strong&gt;, that work is done: a complete JOPS single-page app can now run inside a page of a Next.js app, next to React, with Next.js Route Handlers as its backend.&lt;/p&gt;

&lt;p&gt;This isn't JOPS competing with React. Next.js keeps everything it does best: server rendering, data fetching, routing and the React ecosystem. JOPS adds class-based views with an Android-style lifecycle on the pages that need them. One codebase, two models, each where it fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bring classes into a Next.js app
&lt;/h2&gt;

&lt;p&gt;Object-oriented design is how most backend and mobile developers think: Java, C#, Kotlin, Swift, Android and UIKit are all built around classes. Modern React has moved the other way. Server Components are async functions, and React's newest features are built for function components. Classes still work in client components, but a class-based architecture no longer fits naturally in a modern React and Next.js codebase.&lt;/p&gt;

&lt;p&gt;Functions and hooks are a proven model, and many teams are productive with them. JOPS gives teams that think in classes a first-class place inside the same app, without asking anyone to switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The split is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js owns the path, JOPS owns the hash.&lt;/strong&gt; The page &lt;code&gt;/quotes&lt;/code&gt; is a Next.js route; &lt;code&gt;/quotes#details?id=q2&lt;/code&gt; is a screen inside the JOPS app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The JOPS app lives in &lt;code&gt;public/&lt;/code&gt;&lt;/strong&gt; and is served as-is. Turbopack and webpack never bundle it, so JOPS keeps its no-build design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The backend is Next.js.&lt;/strong&gt; The JOPS app calls Route Handlers with &lt;code&gt;fetch()&lt;/code&gt;, on the same origin, with the session cookie sent automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bridge is a single React component, &lt;code&gt;JopsApp&lt;/code&gt;, imported from &lt;code&gt;jops-core/nextjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app/quotes/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;JopsApp&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jops-core/nextjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;QuotesPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JopsApp&lt;/span&gt; &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/quotes/App.js"&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"quotes-root"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JopsApp&lt;/code&gt; is a Client Component, so a Server Component page can render it. On the server it renders an empty &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. In the browser it loads the JOPS library once per page, adds the import map and starts the app inside that &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. The app's entry module is the same &lt;code&gt;App.js&lt;/code&gt; a standalone JOPS app uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// public/quotes/App.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jops-core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/quotes/layout/main.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Proven on a real application
&lt;/h2&gt;

&lt;p&gt;We didn't design the bridge on a whiteboard. We built a typical Next.js CRM first, on Next.js 16, React 19, the App Router and Tailwind CSS 4, and then rebuilt its entire Quote Builder as a JOPS app inside it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A quote list with status filters and loading skeletons&lt;/li&gt;
&lt;li&gt;Quote details with status actions and a printable quote document&lt;/li&gt;
&lt;li&gt;A two-step quote editor with a product picker, live totals and server-side validation&lt;/li&gt;
&lt;li&gt;A new-quote flow started from the CRM's own sales pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it runs on the CRM's REST Route Handlers, styled with the same Tailwind classes as the rest of the app. Building a real feature end to end exposed issues no demo would have. We fixed them in the framework before the release, and the few that are better handled by convention are listed below.&lt;/p&gt;

&lt;h2&gt;
  
  
  An app lifecycle that respects the host
&lt;/h2&gt;

&lt;p&gt;A JOPS app inside Next.js behaves like an app on a phone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leaving the page ends the app completely.&lt;/strong&gt; Follow a Next.js link elsewhere, or press Back past the app's first screen, and the router, every view, the app's &lt;code&gt;Store&lt;/code&gt; and &lt;code&gt;EventBus&lt;/code&gt;, and JOPS's history hooks are all removed. Nothing keeps running in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coming back launches it again&lt;/strong&gt; and restores the screen from the hash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js navigation keeps working.&lt;/strong&gt; Next.js history state is preserved in every entry JOPS writes, so Back and Forward behave across both frameworks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;The quickest start is the new project template. In an empty folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx jops-core init
&lt;span class="c"&gt;# choose: 3. Next.js JOPS Embedded SPA&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It creates a Next.js project with a React-only page, a page hosting a JOPS SPA (a mock login and a quotes list), and a Route Handler as its backend. To add JOPS to an existing Next.js app instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i jops-core
npx jops-core update   &lt;span class="c"&gt;# copies the library to public/lib/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then put your JOPS app in &lt;code&gt;public/&lt;/code&gt; and render &lt;code&gt;&amp;lt;JopsApp app="/my-app/App.js" /&amp;gt;&lt;/code&gt; on the page that hosts it.&lt;/p&gt;

&lt;p&gt;Not using React? The same mechanism works on any page: load the library with &lt;code&gt;data-host&lt;/code&gt;, and start or stop the app yourself with &lt;code&gt;window.JOPS.mount()&lt;/code&gt; and &lt;code&gt;unmount()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also new in 1.1.0
&lt;/h2&gt;

&lt;p&gt;Building a full application inside Next.js shaped several additions that every JOPS app can use, standalone or hosted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onPause()&lt;/code&gt;&lt;/strong&gt;, the counterpart of &lt;code&gt;onResume()&lt;/code&gt;, called when another view comes to the front while this one stays alive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag props:&lt;/strong&gt; attributes on a declared view, such as &lt;code&gt;&amp;lt;Card jops title="Hi" item-id="7"&amp;gt;&lt;/code&gt;, arrive as read-only &lt;code&gt;this.props&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layouts with several top-level elements&lt;/strong&gt;, and &lt;code&gt;find(id)&lt;/code&gt; for every element with a given id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owned event subscriptions:&lt;/strong&gt; &lt;code&gt;EventBus.subscribe(owner, event, callback)&lt;/code&gt; removes itself when its view is destroyed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A round of router and view fixes&lt;/strong&gt; found while building the Quote Builder, from reliable default routes to views that resume exactly once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Know before you start
&lt;/h2&gt;

&lt;p&gt;A few conventions keep the two frameworks out of each other's way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One JOPS app per page.&lt;/strong&gt; The router, &lt;code&gt;Store&lt;/code&gt; and &lt;code&gt;EventBus&lt;/code&gt; are page-wide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root-absolute paths&lt;/strong&gt; for everything JOPS loads, and the library served from &lt;code&gt;public/lib/&lt;/code&gt; rather than a CDN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js owns the CSS.&lt;/strong&gt; Inside Next.js, &lt;code&gt;setStyle()&lt;/code&gt; is ignored; Tailwind picks up classes used in your JOPS files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST, not Server Actions.&lt;/strong&gt; A JOPS app talks to Next.js through Route Handlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy &lt;code&gt;public/&lt;/code&gt; with the app,&lt;/strong&gt; and use short cache headers for the JOPS files there, since they aren't content-hashed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/jops-core" rel="noopener noreferrer"&gt;jops-core on npm&lt;/a&gt;: the full guide is in the README section "Next.js Integration"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/uidb-dev/jops-core" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for issues and discussions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ui-db.com/articles/jops-class-based-spa-framework/" rel="noopener noreferrer"&gt;Introducing JOPS&lt;/a&gt;, if you're new to the framework&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ui-db.com/articles/jops-hybrid-mobile-cordova-capacitor/" rel="noopener noreferrer"&gt;JOPS Goes Mobile&lt;/a&gt;, the hybrid iOS and Android release&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next: JOPS and Electron for standalone applications
&lt;/h2&gt;

&lt;p&gt;JOPS now runs in the browser, in hybrid mobile apps and inside Next.js. The next environment on our list is the desktop. The JOPS team will examine integration with Electron, so the same class-based views can power standalone applications on Windows, macOS and Linux, with access to the file system and native menus through Electron's main process. Many of the internal tools JOPS was built for live on the desktop, and we'll share what we learn as the work progresses.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;JOPS is built and maintained at UIDB, a boutique team of senior engineers who build custom systems with our clients, side by side. If you're extending a Next.js system or planning one that needs to hold up for years, &lt;a href="https://ui-db.com" rel="noopener noreferrer"&gt;we'd be glad to talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>JOPS Goes Mobile: Hybrid iOS and Android Apps with Cordova and Capacitor</title>
      <dc:creator>Arik Wald</dc:creator>
      <pubDate>Tue, 13 Oct 2026 08:41:50 +0000</pubDate>
      <link>https://dev.to/arik_wald_87b25f4b2939e11/jops-goes-mobile-hybrid-ios-and-android-apps-with-cordova-and-capacitor-27fo</link>
      <guid>https://dev.to/arik_wald_87b25f4b2939e11/jops-goes-mobile-hybrid-ios-and-android-apps-with-cordova-and-capacitor-27fo</guid>
      <description>&lt;p&gt;JOPS started from a simple idea: bring the structure of an Android app to the browser. Views are classes, every view has a lifecycle (&lt;code&gt;loadLayout()&lt;/code&gt; → &lt;code&gt;inflate()&lt;/code&gt; → &lt;code&gt;onLayout()&lt;/code&gt; → &lt;code&gt;onResume()&lt;/code&gt;), and navigation keeps a back stack. A framework shaped like a mobile app should also run as one, and with &lt;strong&gt;jops-core 1.0.13&lt;/strong&gt; it does.&lt;/p&gt;

&lt;p&gt;The same JOPS app now ships to iOS and Android inside &lt;strong&gt;Apache Cordova&lt;/strong&gt; or &lt;strong&gt;Capacitor&lt;/strong&gt;, with no bundler and no changes to your views. This article covers what we changed, what we tested it on, and how to set up a hybrid project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in 1.0.13
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Android back button drives the router.&lt;/strong&gt; The hardware key and the back gesture call &lt;code&gt;Router.back()&lt;/code&gt;, and the first screen closes the app, as Android users expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A setup command that knows your project.&lt;/strong&gt; &lt;code&gt;npx jops-core init&lt;/code&gt; detects Cordova and Capacitor projects and writes the app into the folder the native shell actually serves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notch-safe layouts.&lt;/strong&gt; Hybrid projects get &lt;code&gt;viewport-fit=cover&lt;/code&gt;, and the Hello World template pads its header and footer for notches, the Dynamic Island and the home indicator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A more precise router.&lt;/strong&gt; A new &lt;code&gt;canGoBack()&lt;/code&gt; method, and &lt;code&gt;onBackPressed()&lt;/code&gt; now runs only on real back navigation. Before 1.0.13 it also fired when tapping a forward link, in every browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing changes in a regular browser.&lt;/strong&gt; Every mobile adjustment is either feature-detected or a CSS value that resolves to &lt;code&gt;0&lt;/code&gt; outside a native shell. A plain web project gets exactly the same &lt;code&gt;init&lt;/code&gt; output as before.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we tested it on
&lt;/h2&gt;

&lt;p&gt;Before the release, we ran JOPS test apps in both shells, on both platforms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shell&lt;/th&gt;
&lt;th&gt;Versions&lt;/th&gt;
&lt;th&gt;Platforms tested&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apache Cordova&lt;/td&gt;
&lt;td&gt;Cordova 12, cordova-ios 8.1.1, cordova-android 15.1.0&lt;/td&gt;
&lt;td&gt;iPhone 17 Pro Max simulator (iOS 26.4), Pixel 2 emulator (Android 16, API 36)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacitor&lt;/td&gt;
&lt;td&gt;Capacitor 8.5, &lt;code&gt;@capacitor/app&lt;/code&gt; 8.1&lt;/td&gt;
&lt;td&gt;iPhone 17 Pro Max simulator (iOS 26.4), Pixel 2 emulator (Android 16, API 36)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Beyond the simulator and emulator, we also tested the apps on physical iPhone and Android devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ionic&lt;/strong&gt; works through its framework-free UI components (&lt;code&gt;@ionic/core&lt;/code&gt;) on top of Cordova or Capacitor. &lt;code&gt;ionic start&lt;/code&gt; always scaffolds an Angular, React or Vue app, so start from a plain Cordova or Capacitor project instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hybrid app in four steps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Create the hybrid project and its platforms.&lt;/strong&gt; With Cordova:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cordova create my-app com.example.myapp MyApp
&lt;span class="nb"&gt;cd &lt;/span&gt;my-app
cordova platform add ios android
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with Capacitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-app
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm i @capacitor/core @capacitor/ios @capacitor/android @capacitor/app
npm i &lt;span class="nt"&gt;-D&lt;/span&gt; @capacitor/cli
npx cap init MyApp com.example.myapp &lt;span class="nt"&gt;--web-dir&lt;/span&gt; www
npx cap add ios
npx cap add android
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Install JOPS in the project root&lt;/strong&gt;, the folder with &lt;code&gt;package.json&lt;/code&gt;, not inside &lt;code&gt;www/&lt;/code&gt;. The shell copies the whole web folder into the app, so a &lt;code&gt;node_modules/&lt;/code&gt; there would be bundled too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i jops-core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Run &lt;code&gt;init&lt;/code&gt;.&lt;/strong&gt; It detects the project type and writes the app where the shell will find it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx jops-core init
&lt;span class="c"&gt;# [jops-core] Detected cordova project → web folder: www/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Build and run&lt;/strong&gt; with the shell's own tools: &lt;code&gt;cordova run ios&lt;/code&gt; and &lt;code&gt;cordova run android&lt;/code&gt;, or &lt;code&gt;npx cap sync&lt;/code&gt; followed by &lt;code&gt;npx cap run ios&lt;/code&gt; and &lt;code&gt;npx cap run android&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Behind step 3, &lt;code&gt;init&lt;/code&gt; handles the details that usually cost an afternoon. For Cordova it allows the inline import map and &lt;code&gt;Thread&lt;/code&gt; workers in the Content-Security-Policy, adds &lt;code&gt;cordova.js&lt;/code&gt;, keeps Cordova's sample page as &lt;code&gt;index.cordova.html&lt;/code&gt;, and sets the iOS scheme to &lt;code&gt;app://localhost&lt;/code&gt;. That last one matters: JOPS loads views and layouts with native &lt;code&gt;import()&lt;/code&gt; and &lt;code&gt;fetch()&lt;/code&gt;, which WebViews block on &lt;code&gt;file://&lt;/code&gt; pages. &lt;code&gt;init&lt;/code&gt; never overwrites values you already set, and running it twice changes nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Android back button
&lt;/h2&gt;

&lt;p&gt;This is the change that matters most on Android. On Android 16 (target SDK 36, with predictive back), an app that doesn't register a back handler closes on the first press of the back button, wherever the user is. For a single-page app, that means one accidental swipe ends the session.&lt;/p&gt;

&lt;p&gt;JOPS now listens for the back button once the native shell reports it's ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;Router.singleton().canGoBack()&lt;/code&gt; is true, it calls &lt;code&gt;Router.back()&lt;/code&gt;, and the current view receives &lt;code&gt;onBackPressed()&lt;/code&gt; exactly as it does for browser back.&lt;/li&gt;
&lt;li&gt;On the first screen, it closes the app, the standard Android behaviour.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also smooths over a cordova-android 15 issue where a single key press fires the back event twice. In Capacitor, install the official &lt;code&gt;@capacitor/app&lt;/code&gt; plugin and the same behaviour applies with no code; without the plugin, JOPS logs a console warning explaining what to add. If your app handles back itself, switch the built-in handler off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/lib/jops-core.min.js"&lt;/span&gt;
        &lt;span class="na"&gt;data-app=&lt;/span&gt;&lt;span class="s"&gt;"./src/App.js"&lt;/span&gt; &lt;span class="na"&gt;data-native-back=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Notches, the Dynamic Island and the home indicator
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;viewport-fit=cover&lt;/code&gt;, the page extends under the status bar and the home indicator. Pad fixed bars with the safe-area insets. They are &lt;code&gt;0&lt;/code&gt; in a regular browser, so the web layout doesn't change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe-area-inset-top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.footer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2rem&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe-area-inset-bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What isn't covered yet
&lt;/h2&gt;

&lt;p&gt;Two cases are still open, and we'd rather say so up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The iOS edge-swipe back gesture in Cordova hasn't been tested yet.&lt;/li&gt;
&lt;li&gt;Navigating &lt;em&gt;forward&lt;/em&gt; in the browser into a stacked view that was already closed does nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One note for apps that are already published with Cordova on iOS: moving from &lt;code&gt;file://&lt;/code&gt; to the &lt;code&gt;app://localhost&lt;/code&gt; origin means the app can't see data previously stored in &lt;code&gt;localStorage&lt;/code&gt; or IndexedDB under &lt;code&gt;file://&lt;/code&gt;. If your app stores data locally, plan a migration before switching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/jops-core" rel="noopener noreferrer"&gt;jops-core on npm&lt;/a&gt;: the full guide is in the README section "Hybrid Mobile Apps (Cordova, Ionic &amp;amp; Capacitor)"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cdn.ui-db.com/hello-world/index.html" rel="noopener noreferrer"&gt;Live demo: JOPS Hello World&lt;/a&gt;, the template behind the mobile setup&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/uidb-dev/jops-core/issues" rel="noopener noreferrer"&gt;GitHub issues&lt;/a&gt; for bugs and device reports&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ui-db.com/articles/jops-class-based-spa-framework/" rel="noopener noreferrer"&gt;Introducing JOPS&lt;/a&gt;, if you're new to the framework&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next: JOPS and Next.js
&lt;/h2&gt;

&lt;p&gt;Next, the JOPS team will examine integration with Next.js and its server-side rendering. Next.js brings server rendering, routing and a large ecosystem; JOPS brings class-based views, an explicit lifecycle and no build step of its own. We want to find out how the two can work side by side, for example JOPS views running on the client inside server-rendered Next.js pages, so that teams can draw on the strengths of both and gain flexibility in each environment. We'll share what we learn, including what doesn't fit.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;JOPS is built and maintained at UIDB, a boutique team of senior engineers who build custom systems with our clients, side by side. If you're planning a hybrid mobile app or a system that needs to hold up for years, &lt;a href="https://ui-db.com" rel="noopener noreferrer"&gt;we'd be glad to talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4drakpriyp0t0iaezg98.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4drakpriyp0t0iaezg98.jpg" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Building Web Apps the Android Way: Introducing JOPS</title>
      <dc:creator>Arik Wald</dc:creator>
      <pubDate>Sat, 10 Oct 2026 19:27:14 +0000</pubDate>
      <link>https://dev.to/arik_wald_87b25f4b2939e11/building-web-apps-the-android-way-introducing-jops-8p7</link>
      <guid>https://dev.to/arik_wald_87b25f4b2939e11/building-web-apps-the-android-way-introducing-jops-8p7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://ui-db.com/articles/jops-class-based-spa-framework/" rel="noopener noreferrer"&gt;ui-db.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I've spent most of my career building native apps: Android, iOS, the WebView bridges that connect them to JavaScript, and C# desktop systems. Every time I came back to modern web development for an internal tool or a hybrid app, I found the same trade-off. Either I accepted a full build chain (bundler, transpiler, JSX, a hooks model) for a screen with five forms, or I wrote loose scripts with no structure at all.&lt;/p&gt;

&lt;p&gt;I wanted something in between: the structure of an Android app, with a real view lifecycle and real navigation, running directly in the browser as plain JavaScript. That became &lt;strong&gt;JOPS&lt;/strong&gt; (JavaScript Object-Oriented Programming), now on npm as &lt;code&gt;jops-core&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What JOPS is
&lt;/h2&gt;

&lt;p&gt;JOPS is a small single-page application framework built on native ES modules. There is no compiler and no bundler: you add an import map and a module script, and the browser runs your app.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Views are classes.&lt;/strong&gt; Every screen and component extends &lt;code&gt;View&lt;/code&gt;, and its state lives on the instance, where a debugger and a subclass can find it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real lifecycle.&lt;/strong&gt; &lt;code&gt;loadLayout()&lt;/code&gt; → &lt;code&gt;inflate()&lt;/code&gt; → &lt;code&gt;onLayout()&lt;/code&gt; → &lt;code&gt;onResume()&lt;/code&gt;, plus &lt;code&gt;onBackPressed()&lt;/code&gt;, modeled on Android.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Navigation with a back stack.&lt;/strong&gt; A hash router with tab-style routes, a stack of screens, slide and fade animations, and deep links with query parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layouts in plain HTML.&lt;/strong&gt; Write layouts as &lt;code&gt;.html&lt;/code&gt; files or as escaped tagged template literals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small helpers, not a platform.&lt;/strong&gt; A dot-path &lt;code&gt;Store&lt;/code&gt;, a static &lt;code&gt;EventBus&lt;/code&gt;, and a &lt;code&gt;Thread&lt;/code&gt; class for background work in a Web Worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiny and typed.&lt;/strong&gt; About 10 KB minified, 3.6 KB gzipped, with no dependencies, and TypeScript declarations so editors autocomplete the whole API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The lifecycle, if you've written Android
&lt;/h2&gt;

&lt;p&gt;The framework calls each view's methods in a fixed order, so you always know where a piece of code belongs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;When it runs&lt;/th&gt;
&lt;th&gt;What it's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loadLayout(path)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;First&lt;/td&gt;
&lt;td&gt;Load the view's HTML layout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inflate()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After the layout loads&lt;/td&gt;
&lt;td&gt;Turn the layout into live DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onLayout()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Once the DOM is live&lt;/td&gt;
&lt;td&gt;Add subviews, read initial data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onResume()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every time the view becomes visible&lt;/td&gt;
&lt;td&gt;Refresh data, including on back navigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onBackPressed()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When the user navigates back&lt;/td&gt;
&lt;td&gt;Save state or confirm leaving&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is a complete view. Event handlers are ordinary methods, wired to the markup with a single attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jops-core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProfileView&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;loadLayout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/layout/profile.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;onLayout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSubView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderList&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;onResume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user.name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;greeting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// escaped&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;onSave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user.name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- profile.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"greeting"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;jops-event-bind&lt;/span&gt; &lt;span class="na"&gt;onchange=&lt;/span&gt;&lt;span class="s"&gt;"onSave"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Layouts are just HTML
&lt;/h2&gt;

&lt;p&gt;Since version 1.0.12, a layout can be a plain &lt;code&gt;.html&lt;/code&gt; fragment: no import, no export, no template syntax. Views nest declaratively, and the router is declared in the same markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;View&lt;/span&gt; &lt;span class="na"&gt;jops&lt;/span&gt; &lt;span class="na"&gt;layout=&lt;/span&gt;&lt;span class="s"&gt;"./src/layout/header.html"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/View&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;Router&lt;/span&gt; &lt;span class="na"&gt;jops&lt;/span&gt; &lt;span class="na"&gt;animation=&lt;/span&gt;&lt;span class="s"&gt;"slide"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;View&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"/home"&lt;/span&gt; &lt;span class="na"&gt;jops&lt;/span&gt; &lt;span class="na"&gt;layout=&lt;/span&gt;&lt;span class="s"&gt;"./src/layout/home.html"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/View&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Orders&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt; &lt;span class="na"&gt;jops&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./src/Orders.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/Orders&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Router&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layout file is fetched once and cached for the life of the page. When you need dynamic markup in JavaScript, the &lt;code&gt;layout&lt;/code&gt; tagged template escapes interpolated values by default, so user input can't inject HTML unless you explicitly mark it with &lt;code&gt;raw()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navigation that remembers where you were
&lt;/h2&gt;

&lt;p&gt;The router supports the two navigation styles mobile developers already know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flat routes&lt;/strong&gt; (&lt;code&gt;href="#orders"&lt;/code&gt;) switch between existing screens, like tabs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stacked navigation&lt;/strong&gt; (&lt;code&gt;Router.singleton().navigate("/detail?id=42", DetailView)&lt;/code&gt;) creates a new screen on top of the current one, like &lt;code&gt;startActivity()&lt;/code&gt;. &lt;code&gt;back()&lt;/code&gt; pops it with an animation, and the previous screen's &lt;code&gt;onResume()&lt;/code&gt; runs again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deep links work out of the box: opening &lt;code&gt;index.html#orders?tab=open&lt;/code&gt; lands directly on that route, and &lt;code&gt;getParams()&lt;/code&gt; returns &lt;code&gt;{ tab: "open" }&lt;/code&gt; on first load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heavy work off the main thread
&lt;/h2&gt;

&lt;p&gt;For CPU-bound tasks such as parsing, hashing or crunching a large dataset, extend &lt;code&gt;Thread&lt;/code&gt;. The &lt;code&gt;run()&lt;/code&gt; method executes in a Web Worker, and results come back to &lt;code&gt;onMessage()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Thread&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jops-core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReportThread&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Thread&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// runs in a Web Worker, with no access to the page&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Total:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReportThread&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting started in two minutes
&lt;/h2&gt;

&lt;p&gt;Load JOPS from a CDN with two tags. No install required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"importmap"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;imports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jops-core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.jsdelivr.net/npm/jops-core@latest/lib/jops-core.min.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;
        &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/jops-core@latest/lib/jops-core.min.js"&lt;/span&gt;
        &lt;span class="na"&gt;data-app=&lt;/span&gt;&lt;span class="s"&gt;"./src/App.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or scaffold a project with npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;jops-core
npx jops-core init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One practical note: ES modules don't load from &lt;code&gt;file://&lt;/code&gt;, so serve the folder with any static server (for example &lt;code&gt;npx serve&lt;/code&gt;) rather than double-clicking &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When JOPS fits, and when it doesn't
&lt;/h2&gt;

&lt;p&gt;JOPS is a good fit for internal tools, admin panels, dashboards, and hybrid apps running inside a WebView, where a build chain is overhead you don't need. It also suits teams coming from Android, iOS or C#, who think in classes and lifecycles rather than hooks.&lt;/p&gt;

&lt;p&gt;It's worth being clear about the limits, too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No reactive state.&lt;/strong&gt; The &lt;code&gt;Store&lt;/code&gt; is a plain key-value container on purpose. Views read it in &lt;code&gt;onResume()&lt;/code&gt; and update the DOM explicitly. If you want automatic re-rendering on every state change, a reactive framework is a better match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's young.&lt;/strong&gt; JOPS reached 1.0 in October 2026. The API is stable within the major version, but the ecosystem around it is small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The source is private.&lt;/strong&gt; JOPS ships as a minified module with full TypeScript declarations and is MIT-licensed for any use, including commercial projects. Feedback and bug reports go through GitHub issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cdn.ui-db.com/hello-world/index.html" rel="noopener noreferrer"&gt;Live demo: JOPS Hello World&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/jops-core" rel="noopener noreferrer"&gt;Documentation and install: jops-core on npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/uidb-dev/jops-core" rel="noopener noreferrer"&gt;Issues and discussions: GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd especially like to hear whether the lifecycle model feels natural to developers who don't come from mobile, and what you'd need before using JOPS in a production internal tool.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;JOPS is built and maintained at UIDB, a boutique team of senior engineers who build custom systems with our clients, side by side. If you're planning an internal tool, a hybrid app or a system that needs to hold up for years, &lt;a href="https://ui-db.com" rel="noopener noreferrer"&gt;we'd be glad to talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
