<?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>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>
