|
1498 | 1498 | <nav class="md-nav md-nav--secondary" aria-label="Table of contents"> |
1499 | 1499 |
|
1500 | 1500 |
|
| 1501 | + |
| 1502 | + |
1501 | 1503 |
|
1502 | 1504 | <label class="md-nav__title" for="__toc"> |
1503 | 1505 | <span class="md-nav__icon md-icon"> |
|
1508 | 1510 | <ul class="md-nav__list" data-md-scrollfix> |
1509 | 1511 |
|
1510 | 1512 | <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"> |
1519 | 1513 | <a href="#abstract" class="md-nav__link"> |
1520 | 1514 | Abstract |
1521 | 1515 | </a> |
1522 | 1516 |
|
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 | | - |
1604 | 1517 | </li> |
1605 | 1518 |
|
1606 | 1519 | </ul> |
|
1623 | 1536 |
|
1624 | 1537 |
|
1625 | 1538 |
|
1626 | | - <h1>002 ReframeInstances</h1> |
1627 | | - |
1628 | 1539 | <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">¶</a></h2> |
1629 | 1540 | <blockquote> |
1630 | 1541 | <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 |
1761 | 1672 | looking it up.</p> |
1762 | 1673 | <h3 id="problem">Problem<a class="headerlink" href="#problem" title="Permanent link">¶</a></h3> |
1763 | 1674 | <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">¶</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">¶</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">¶</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">¶</a></h2> |
| 1696 | +<pre><code class="clojure">;; lifecycle |
| 1697 | +(rf/make-frame opts?) ;; => frame |
| 1698 | +(rf/destroy-frame frame) |
| 1699 | + |
| 1700 | +;; view ergonomics |
| 1701 | +(rf/frame-provider {:frame f} & children) |
| 1702 | +(rf/use-dispatch) ;; => (fn [event]) |
| 1703 | +(rf/use-subscribe query-v) ;; optional convenience hook |
| 1704 | + |
| 1705 | +;; dynamic binding helper (tests/REPL/setup) |
| 1706 | +(rf/with-frame frame & 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">¶</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">¶</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 "Left"]] |
| 1729 | + [rf/frame-provider {:frame right-frame} |
| 1730 | + [counter-widget "Right"]]]) |
| 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 "Count: " 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">¶</a></h2> |
| 1744 | +<pre><code class="clojure">(defonce todo-frame |
| 1745 | + (rf/make-frame {:id :todo |
| 1746 | + :handler-scope {:mode :namespaces |
| 1747 | + :allow #{"todo" "shared"}}})) |
| 1748 | + |
| 1749 | +(defonce meme-frame |
| 1750 | + (rf/make-frame {:id :meme |
| 1751 | + :handler-scope {:mode :namespaces |
| 1752 | + :allow #{"meme" "shared"}}})) |
| 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">¶</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 | + [:<>])) |
| 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 "Count: " count] |
| 1783 | + [:button {:on-click #(dispatch! [:inc])} "+"]])) |
| 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">¶</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">¶</a></h2> |
| 1798 | +<h2 id="1-frame-runtime-object">1) Frame runtime object<a class="headerlink" href="#1-frame-runtime-object" title="Permanent link">¶</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">¶</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">¶</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">¶</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">¶</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">¶</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]} & children] |
| 1849 | + [:> (.-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 [:<>] 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">¶</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">¶</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">¶</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">¶</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> |
1764 | 1897 |
|
1765 | 1898 |
|
1766 | 1899 |
|
|
0 commit comments