Skip to content

Commit 54c8bc3

Browse files
committed
Update docs
1 parent dc07ee8 commit 54c8bc3

File tree

6 files changed

+2685
-2550
lines changed

6 files changed

+2685
-2550
lines changed

EPs/002-ReframeInstances/index.html

Lines changed: 224 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,6 +1498,8 @@
14981498
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
14991499

15001500

1501+
1502+
15011503

15021504
<label class="md-nav__title" for="__toc">
15031505
<span class="md-nav__icon md-icon">
@@ -1508,99 +1510,10 @@
15081510
<ul class="md-nav__list" data-md-scrollfix>
15091511

15101512
<li class="md-nav__item">
1511-
<a href="#ep-002-multiple-re-frame-instances" class="md-nav__link">
1512-
EP 002 - Multiple re-frame Instances
1513-
</a>
1514-
1515-
<nav class="md-nav" aria-label="EP 002 - Multiple re-frame Instances">
1516-
<ul class="md-nav__list">
1517-
1518-
<li class="md-nav__item">
15191513
<a href="#abstract" class="md-nav__link">
15201514
Abstract
15211515
</a>
15221516

1523-
</li>
1524-
1525-
</ul>
1526-
</nav>
1527-
1528-
</li>
1529-
1530-
<li class="md-nav__item">
1531-
<a href="#introduction" class="md-nav__link">
1532-
Introduction
1533-
</a>
1534-
1535-
<nav class="md-nav" aria-label="Introduction">
1536-
<ul class="md-nav__list">
1537-
1538-
<li class="md-nav__item">
1539-
<a href="#global-state-as-a-frame" class="md-nav__link">
1540-
Global State As A Frame
1541-
</a>
1542-
1543-
</li>
1544-
1545-
<li class="md-nav__item">
1546-
<a href="#the-two-problems" class="md-nav__link">
1547-
The Two Problems
1548-
</a>
1549-
1550-
</li>
1551-
1552-
<li class="md-nav__item">
1553-
<a href="#on-mutating-registrars" class="md-nav__link">
1554-
On Mutating registrars
1555-
</a>
1556-
1557-
</li>
1558-
1559-
<li class="md-nav__item">
1560-
<a href="#problem-1-frames-and-registrars" class="md-nav__link">
1561-
Problem 1: Frames And Registrars
1562-
</a>
1563-
1564-
</li>
1565-
1566-
<li class="md-nav__item">
1567-
<a href="#problem-1-solutions" class="md-nav__link">
1568-
Problem 1 - Solutions
1569-
</a>
1570-
1571-
</li>
1572-
1573-
<li class="md-nav__item">
1574-
<a href="#problem-2-views-dispatch-and-subscription" class="md-nav__link">
1575-
Problem 2: Views, dispatch and subscription
1576-
</a>
1577-
1578-
</li>
1579-
1580-
<li class="md-nav__item">
1581-
<a href="#problem-2-minimal-design-solution" class="md-nav__link">
1582-
Problem 2: Minimal Design Solution
1583-
</a>
1584-
1585-
</li>
1586-
1587-
<li class="md-nav__item">
1588-
<a href="#problem-2-better-design-solution" class="md-nav__link">
1589-
Problem 2: Better Design Solution
1590-
</a>
1591-
1592-
</li>
1593-
1594-
<li class="md-nav__item">
1595-
<a href="#problem" class="md-nav__link">
1596-
Problem
1597-
</a>
1598-
1599-
</li>
1600-
1601-
</ul>
1602-
</nav>
1603-
16041517
</li>
16051518

16061519
</ul>
@@ -1623,8 +1536,6 @@
16231536

16241537

16251538

1626-
<h1>002 ReframeInstances</h1>
1627-
16281539
<h2 id="ep-002-multiple-re-frame-instances">EP 002 - Multiple re-frame Instances<a class="headerlink" href="#ep-002-multiple-re-frame-instances" title="Permanent link">&para;</a></h2>
16291540
<blockquote>
16301541
<p>Status: Drafting. May be incoherent and/or wrong. Probably don't read.</p>
@@ -1761,6 +1672,228 @@ <h3 id="problem-2-better-design-solution">Problem 2: Better Design Solution<a cl
17611672
looking it up.</p>
17621673
<h3 id="problem">Problem<a class="headerlink" href="#problem" title="Permanent link">&para;</a></h3>
17631674
<p>if we <code>subscribe</code> in a view, and that subscription needs to causes other subscriptions to be created, how to get at the associated frame at the point when we want to create the further subscriptions?</p>
1675+
<hr />
1676+
<h2 id="appendix-multi-frame-ergonomic-model-and-implementation-sketch-2026-update">Appendix: Multi-frame ergonomic model and implementation sketch (2026 update)<a class="headerlink" href="#appendix-multi-frame-ergonomic-model-and-implementation-sketch-2026-update" title="Permanent link">&para;</a></h2>
1677+
<h1 id="multi-frame-re-frame-ergonomic-programmer-model-implementation-sketch">Multi-frame re-frame: ergonomic programmer model + implementation sketch<a class="headerlink" href="#multi-frame-re-frame-ergonomic-programmer-model-implementation-sketch" title="Permanent link">&para;</a></h1>
1678+
<p>This document answers two questions:</p>
1679+
<ol>
1680+
<li>What should multi-frame re-frame feel like to a programmer?</li>
1681+
<li>How can that be implemented without violating React/Reagent constraints?</li>
1682+
</ol>
1683+
<hr />
1684+
<h2 id="programmer-model-target-ux">Programmer model (target UX)<a class="headerlink" href="#programmer-model-target-ux" title="Permanent link">&para;</a></h2>
1685+
<p><strong>Mental model:</strong> each <code>[rf/frame-provider {:frame f} ...]</code> subtree runs against its own runtime world.</p>
1686+
<p>Inside that subtree:</p>
1687+
<ul>
1688+
<li>plain <code>rf/subscribe</code> should just work,</li>
1689+
<li>plain <code>rf/dispatch</code> should work during render-time flows,</li>
1690+
<li><code>rf/use-dispatch</code> is the ergonomic default for event handlers (<code>:on-click</code>, async callbacks),</li>
1691+
<li>explicit <code>*-to</code> APIs are available for tests/integration/non-UI code.</li>
1692+
</ul>
1693+
<p>No prop drilling of frame values.</p>
1694+
<hr />
1695+
<h2 id="public-api-surface-small-memorable">Public API surface (small + memorable)<a class="headerlink" href="#public-api-surface-small-memorable" title="Permanent link">&para;</a></h2>
1696+
<pre><code class="clojure">;; lifecycle
1697+
(rf/make-frame opts?) ;; =&gt; frame
1698+
(rf/destroy-frame frame)
1699+
1700+
;; view ergonomics
1701+
(rf/frame-provider {:frame f} &amp; children)
1702+
(rf/use-dispatch) ;; =&gt; (fn [event])
1703+
(rf/use-subscribe query-v) ;; optional convenience hook
1704+
1705+
;; dynamic binding helper (tests/REPL/setup)
1706+
(rf/with-frame frame &amp; body) ;; macro
1707+
1708+
;; explicit integration path
1709+
(rf/dispatch-to frame event)
1710+
(rf/dispatch-sync-to frame event)
1711+
(rf/subscribe-to frame query-v)
1712+
1713+
;; ergonomic plain API (frame resolved from current context/binding)
1714+
(rf/dispatch event)
1715+
(rf/dispatch-sync event)
1716+
(rf/subscribe query-v)
1717+
</code></pre>
1718+
1719+
<hr />
1720+
<h2 id="usage-examples">Usage examples<a class="headerlink" href="#usage-examples" title="Permanent link">&para;</a></h2>
1721+
<h2 id="1-two-isolated-widgets-on-one-page">1) Two isolated widgets on one page<a class="headerlink" href="#1-two-isolated-widgets-on-one-page" title="Permanent link">&para;</a></h2>
1722+
<pre><code class="clojure">(defonce left-frame (rf/make-frame {:id :left}))
1723+
(defonce right-frame (rf/make-frame {:id :right}))
1724+
1725+
(defn page []
1726+
[:div
1727+
[rf/frame-provider {:frame left-frame}
1728+
[counter-widget &quot;Left&quot;]]
1729+
[rf/frame-provider {:frame right-frame}
1730+
[counter-widget &quot;Right&quot;]]])
1731+
</code></pre>
1732+
1733+
<p>Child components stay close to normal re-frame style:</p>
1734+
<pre><code class="clojure">(defn counter-widget [label]
1735+
(let [dispatch! (rf/use-dispatch) ;; one hook for callbacks
1736+
count @(rf/subscribe [:counter/value])] ;; plain subscribe remains
1737+
[:div
1738+
[:h3 label]
1739+
[:button {:on-click #(dispatch! [:counter/inc])}
1740+
(str &quot;Count: &quot; count)]]))
1741+
</code></pre>
1742+
1743+
<h2 id="2-two-different-apps-embedded-in-one-host-page">2) Two different apps embedded in one host page<a class="headerlink" href="#2-two-different-apps-embedded-in-one-host-page" title="Permanent link">&para;</a></h2>
1744+
<pre><code class="clojure">(defonce todo-frame
1745+
(rf/make-frame {:id :todo
1746+
:handler-scope {:mode :namespaces
1747+
:allow #{&quot;todo&quot; &quot;shared&quot;}}}))
1748+
1749+
(defonce meme-frame
1750+
(rf/make-frame {:id :meme
1751+
:handler-scope {:mode :namespaces
1752+
:allow #{&quot;meme&quot; &quot;shared&quot;}}}))
1753+
1754+
(defn host-page []
1755+
[:main
1756+
[rf/frame-provider {:frame todo-frame} [todo/root]]
1757+
[rf/frame-provider {:frame meme-frame} [meme/root]]])
1758+
</code></pre>
1759+
1760+
<h2 id="3-reusable-isolated-widget-pattern">3) Reusable isolated widget pattern<a class="headerlink" href="#3-reusable-isolated-widget-pattern" title="Permanent link">&para;</a></h2>
1761+
<pre><code class="clojure">(defn make-counter-frame []
1762+
(rf/make-frame {:db {:count 0}}))
1763+
1764+
(defn counter-widget [label]
1765+
(let [frame (make-counter-frame)]
1766+
[rf/frame-provider {:frame frame}
1767+
[counter-widget-init frame]
1768+
[counter-widget-ui label]]))
1769+
1770+
(defn counter-widget-init [frame]
1771+
;; register once for this frame (not on every render)
1772+
(r/with-let [_ (rf/with-frame frame
1773+
(rf/reg-event-db :inc (fn [db _] (update db :count inc)))
1774+
(rf/reg-sub :count (fn [db _] (:count db))))]
1775+
[:&lt;&gt;]))
1776+
1777+
(defn counter-widget-ui [label]
1778+
(let [dispatch! (rf/use-dispatch)
1779+
count @(rf/subscribe [:count])]
1780+
[:div.widget
1781+
[:h3 label]
1782+
[:p &quot;Count: &quot; count]
1783+
[:button {:on-click #(dispatch! [:inc])} &quot;+&quot;]]))
1784+
</code></pre>
1785+
1786+
<h2 id="4-explicit-path-for-tests-and-non-ui-code">4) Explicit path for tests and non-UI code<a class="headerlink" href="#4-explicit-path-for-tests-and-non-ui-code" title="Permanent link">&para;</a></h2>
1787+
<pre><code class="clojure">(let [frame (rf/make-frame {:id :batch})]
1788+
(rf/dispatch-sync-to frame [:init])
1789+
@(rf/subscribe-to frame [:status]))
1790+
1791+
(rf/with-frame (rf/make-frame {:db {:count 0}})
1792+
(rf/dispatch-sync [:counter/inc])
1793+
@(rf/subscribe [:counter/value]))
1794+
</code></pre>
1795+
1796+
<hr />
1797+
<h2 id="internal-architecture">Internal architecture<a class="headerlink" href="#internal-architecture" title="Permanent link">&para;</a></h2>
1798+
<h2 id="1-frame-runtime-object">1) Frame runtime object<a class="headerlink" href="#1-frame-runtime-object" title="Permanent link">&para;</a></h2>
1799+
<p>Each frame owns mutable runtime state:</p>
1800+
<pre><code class="clojure">{:id :todo
1801+
:app-db (reagent/atom {})
1802+
:registrar ... ;; visible handlers for this frame
1803+
:router ... ;; queue/scheduler state
1804+
:sub-cache ... ;; reactions/sub graph
1805+
:lifecycle {:destroyed? false}
1806+
:config {...}}
1807+
</code></pre>
1808+
1809+
<p>Anything mutable that can affect behavior is frame-scoped.</p>
1810+
<h2 id="2-parameterize-internals-by-frame">2) Parameterize internals by frame<a class="headerlink" href="#2-parameterize-internals-by-frame" title="Permanent link">&para;</a></h2>
1811+
<p>Core internals become explicit:</p>
1812+
<pre><code class="clojure">(dispatch* frame event)
1813+
(dispatch-sync* frame event)
1814+
(subscribe* frame query-v)
1815+
(invoke-handler* frame event)
1816+
(cache-lookup* frame query-v)
1817+
</code></pre>
1818+
1819+
<p>Public APIs are wrappers around frame resolution + these internals.</p>
1820+
<h2 id="3-registration-strategy">3) Registration strategy<a class="headerlink" href="#3-registration-strategy" title="Permanent link">&para;</a></h2>
1821+
<p>Pragmatic approach:</p>
1822+
<ul>
1823+
<li>keep handler definitions globally registered (good hot reload + ecosystem compatibility),</li>
1824+
<li>derive frame-local resolver/filter at <code>make-frame</code>,</li>
1825+
<li>enforce scope via <code>:handler-scope</code> (<code>:all</code>, namespace allow-list, package allow-list).</li>
1826+
</ul>
1827+
<hr />
1828+
<h2 id="frame-resolution-design-hook-safe-ergonomic">Frame resolution design (hook-safe + ergonomic)<a class="headerlink" href="#frame-resolution-design-hook-safe-ergonomic" title="Permanent link">&para;</a></h2>
1829+
<p>The critical design point: <strong>never call React hooks from general utility functions</strong>.</p>
1830+
<h3 id="1-core-primitives">1) Core primitives<a class="headerlink" href="#1-core-primitives" title="Permanent link">&para;</a></h3>
1831+
<pre><code class="clojure">(def ^:dynamic *current-frame* nil)
1832+
(defonce frame-context (js/React.createContext nil))
1833+
(def default-frame (make-frame {:id :default}))
1834+
1835+
(defn current-frame* []
1836+
;; non-hook path, safe everywhere
1837+
(or *current-frame* default-frame))
1838+
1839+
(defn use-frame []
1840+
;; hook path, valid only in component/hook call sites
1841+
(or (js/React.useContext frame-context)
1842+
*current-frame*
1843+
default-frame))
1844+
</code></pre>
1845+
1846+
<h3 id="2-provider-implementation">2) Provider implementation<a class="headerlink" href="#2-provider-implementation" title="Permanent link">&para;</a></h3>
1847+
<p>Provider sets React context and also dynamic binding for render-time code paths.</p>
1848+
<pre><code class="clojure">(defn frame-provider [{:keys [frame]} &amp; children]
1849+
[:&gt; (.-Provider frame-context) {:value frame}
1850+
[frame-render-binding frame children]])
1851+
1852+
(defn- frame-render-binding [frame children]
1853+
(binding [*current-frame* frame]
1854+
(into [:&lt;&gt;] children)))
1855+
</code></pre>
1856+
1857+
<p>This is what allows plain <code>rf/subscribe</code> to work unchanged inside provider subtrees.</p>
1858+
<h3 id="3-plain-apis">3) Plain APIs<a class="headerlink" href="#3-plain-apis" title="Permanent link">&para;</a></h3>
1859+
<pre><code class="clojure">(defn subscribe [query-v]
1860+
(subscribe* (current-frame*) query-v))
1861+
1862+
(defn dispatch [event]
1863+
(dispatch* (current-frame*) event))
1864+
</code></pre>
1865+
1866+
<h3 id="4-hook-convenience-apis">4) Hook convenience APIs<a class="headerlink" href="#4-hook-convenience-apis" title="Permanent link">&para;</a></h3>
1867+
<pre><code class="clojure">(defn use-dispatch []
1868+
(let [frame (use-frame)]
1869+
;; stable closure per mounted component instance
1870+
(r/with-let [f (fn [event] (dispatch* frame event))]
1871+
f)))
1872+
1873+
(defn use-subscribe [query-v]
1874+
(let [frame (use-frame)]
1875+
(subscribe* frame query-v)))
1876+
</code></pre>
1877+
1878+
<hr />
1879+
<h2 id="correctness-notes">Correctness notes<a class="headerlink" href="#correctness-notes" title="Permanent link">&para;</a></h2>
1880+
<ol>
1881+
<li><strong>Why plain <code>subscribe</code> works ergonomically:</strong> subscription creation happens during render; provider render binding supplies <code>*current-frame*</code>.</li>
1882+
<li><strong>Why <code>use-dispatch</code> is still recommended:</strong> event handlers/async callbacks run after render; dynamic binding no longer applies.</li>
1883+
<li><strong>Subscription chaining:</strong> nested <code>subscribe*</code> inherits the same frame context; cache keys include frame identity + query.</li>
1884+
<li><strong>Async effects:</strong> deferred callbacks should capture frame explicitly (<code>use-dispatch</code>, <code>dispatch-to</code>, or closure with frame).</li>
1885+
<li><strong>Destroyed frame behavior:</strong> dispatch/subscribe against destroyed frames should throw clear errors.</li>
1886+
</ol>
1887+
<hr />
1888+
<h2 id="minimal-implementation-slice">Minimal implementation slice<a class="headerlink" href="#minimal-implementation-slice" title="Permanent link">&para;</a></h2>
1889+
<ol>
1890+
<li><code>make-frame</code>, <code>dispatch-to</code>, <code>subscribe-to</code></li>
1891+
<li><code>frame-provider</code> with context + render-time binding</li>
1892+
<li>plain <code>dispatch</code>/<code>subscribe</code> via <code>current-frame*</code></li>
1893+
<li><code>use-dispatch</code></li>
1894+
<li>two counters on one page proving isolation and ergonomics</li>
1895+
</ol>
1896+
<p>This slice demonstrates the intended UX while staying technically sound.</p>
17641897

17651898

17661899

0 commit comments

Comments
 (0)