diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs
index 71ba532f3..8be0baa33 100644
--- a/src/nextjournal/clerk/render.cljs
+++ b/src/nextjournal/clerk/render.cljs
@@ -172,6 +172,14 @@
 (defn render-unreadable-edn [edn]
   [:span.inspected-value.whitespace-nowrap.cmt-default edn])
 
+(def !read-string-without-tag-table
+  (delay (eval 'nextjournal.clerk.viewer/read-string-without-tag-table)))
+
+(defn render-read+inspect
+  [x] (try [inspect @!read-string-without-tag-table]
+           (catch js/Error _e
+             (render-unreadable-edn x))))
+
 (defn error-badge [& content]
   [:div.bg-red-50.rounded-sm.text-xs.text-red-400.px-2.py-1.items-center.sans-serif.inline-flex
    [:svg.h-4.w-4.text-red-400 {:xmlns "http://www.w3.org/2000/svg" :viewBox "0 0 20 20" :fill "currentColor" :aria-hidden "true"}
@@ -333,6 +341,9 @@
                  (get-in x [:nextjournal/render-opts :id])
                  (with-meta {:key (str (get-in x [:nextjournal/render-opts :id]) "@" @!eval-counter)})))))
 
+(defn render-children [xs opts]
+  (into [:<>] (nextjournal.clerk.render/inspect-children opts) xs))
+
 (def expand-style
   ["cursor-pointer"
    "bg-indigo-50"
@@ -435,6 +446,18 @@
   [:span.cmt-number.inspected-value
    (if (js/Number.isNaN num) "NaN" (str num))])
 
+(defn render-hex-number [num] (render-number (str "0x" (.toString (js/Number. num) 16))))
+
+(defn render-map-entry [xs opts]
+  (into [:<>] (comp (nextjournal.clerk.render/inspect-children opts) (interpose " ")) xs))
+
+(defn render-symbol [x] [:span.cmt-keyword.inspected-value (str x)])
+(defn render-keyword [x] [:span.cmt-atom.inspected-value (str x)])
+(defn render-nil [_] [:span.cmt-default.inspected-value "nil"])
+(defn render-boolean [x] [:span.cmt-bool.inspected-value (str x)])
+(defn render-char [c] [:span.cmt-string.inspected-value "\\" c])
+(defn render-var [x] [:span.inspected-value [:span.cmt-meta "#'" (str x)]])
+
 (defn sort! [!sort i k]
   (let [{:keys [sort-key sort-order]} @!sort]
     (reset! !sort {:sort-index i
@@ -526,12 +549,30 @@
     [:div.rounded.border.border-red-200.border-t-0.overflow-hidden
      [throwable-view ex opts]]))
 
-(defn render-tagged-value
-  ([tag value] (render-tagged-value {:space? true} tag value))
+(defn render-tagged-value*
+  ([tag value] (render-tagged-value* {:space? true} tag value))
   ([{:keys [space?]} tag value]
    [:span.inspected-value.whitespace-nowrap
     [:span.cmt-meta tag] (when space? nbsp) value]))
 
+
+(defn render-tagged-value [{:keys [tag value space?]} opts]
+  [render-tagged-value
+   {:space? (:nextjournal/value space?)}
+   (str "#" (:nextjournal/value tag))
+   [nextjournal.clerk.render/inspect-presented value]])
+
+(defn render-js-object [v opts]
+  [render-tagged-value* {:space? true}
+   "#js"
+   [nextjournal.clerk.render/render-map v opts]])
+
+(defn render-js-array [v opts]
+  [render-tagged-value* {:space? true}
+   "#js"
+   [nextjournal.clerk.render/render-coll v opts]])
+
+
 (defn set-viewers! [scope viewers]
   #_(js/console.log :set-viewers! {:scope scope :viewers viewers})
   (swap! !viewers assoc scope (vec viewers))
@@ -695,12 +736,16 @@
   (let [re-eval (fn [{:keys [form]}] (viewer/->viewer-fn form))]
     (w/postwalk (fn [x] (cond-> x (viewer/viewer-fn? x) re-eval)) doc)))
 
+(defn replace-viewer-fns [{:as doc :keys [name->viewer]}]
+  (assoc (w/postwalk-replace name->viewer doc)
+         :name->viewer name->viewer))
+
 (defn ^:export set-state! [{:as state :keys [doc]}]
   (when (contains? state :doc)
     (when (exists? js/window)
       ;; TODO: can we restore the scroll position when navigating back?
       (.scrollTo js/window #js {:top 0}))
-    (reset! !doc doc))
+    (reset! !doc (replace-viewer-fns doc)))
   ;; (when (and error (contains? @!doc :status))
   ;;   (swap! !doc dissoc :status))
   (when (remount? doc)
@@ -713,7 +758,7 @@
 
 (defn patch-state! [{:keys [patch]}]
   (if (remount? patch)
-    (do (swap! !doc #(re-eval-viewer-fns (apply-patch % patch)))
+    (do (swap! !doc #(re-eval-viewer-fns (replace-viewer-fns (apply-patch % patch))))
         ;; TODO: figure out why it doesn't work without `js/setTimeout`
         (js/setTimeout #(swap! !eval-counter inc) 10))
     (swap! !doc apply-patch patch)))
@@ -967,6 +1012,9 @@
       [:span {:dangerouslySetInnerHTML {:__html (.renderToString katex tex-string (j/obj :displayMode (not inline?) :throwOnError false))}}]
       default-loading-view)))
 
+(defn render-katex-inline [tex opts]
+  (nextjournal.clerk.render/render-katex tex (assoc opts :inline? true)))
+
 (defn render-mathjax [value]
   (let [mathjax (hooks/use-d3-require "https://run.nextjournalusercontent.com/data/QmQadTUYtF4JjbwhUFzQy9BQiK52ace3KqVHreUqL7ohoZ?filename=es5/tex-svg-full.js&content-type=application/javascript")
         ref-fn (react/useCallback (fn [el]
@@ -1027,10 +1075,44 @@
         [render-code code-string (assoc opts :language "clojure")]]])))
 
 
+(defn render-example [{:keys [form val]} opts]
+  [:div.mb-3.last:mb-0
+   [:div.bg-slate-100.dark:bg-slate-800.px-4.py-2.border-l-2.border-slate-200.dark:border-slate-700
+    (inspect-presented opts form)]
+   [:div.pt-2.px-4.border-l-2.border-transparent
+    (inspect-presented opts val)]])
+
+(defn render-examples [examples opts]
+  [:div
+   [:div.uppercase.tracking-wider.text-xs.font-sans.font-bold.text-slate-500.dark:text-white.mb-2.mt-3 "Examples"]
+   (into [:div] (inspect-children opts) examples)])
+
+(defn render-row [items opts]
+  (into [:div {:class "md:flex md:flex-row md:gap-4 not-prose"
+               :style opts}]
+        (map (fn [item]
+               [:div.flex.items-center.justify-center.flex-auto
+                [inspect-presented opts item]]))
+        items))
+
+(defn render-col [items opts]
+  (into [:div {:class "md:flex md:flex-col md:gap-4 clerk-grid not-prose"
+               :style opts}]
+        (map (fn [item]
+               [:div.flex.items-center.justify-center
+                [inspect-presented opts item]]))
+        items))
+
+(defn render-empty-fragment [_ _] [:<>])
+
 (defn url-for [{:as src :keys [blob-id]}]
   (if (string? src)
     src
     (str "/_blob/" blob-id (when-let [opts (seq (dissoc src :blob-id))]
                              (str "?" (opts->query opts))))))
 
+(defn render-image [blob-or-url]
+  [:div.flex.flex-col.items-center.not-prose
+   [:img {:src (url-for blob-or-url)}]])
+
 (def consume-view-context view-context/consume)
diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc
index 7a5018fc6..17b18e32b 100644
--- a/src/nextjournal/clerk/viewer.cljc
+++ b/src/nextjournal/clerk/viewer.cljc
@@ -660,7 +660,7 @@
       (->> (mapv (partial with-viewer
                           (cond-> result-viewer
                             (hidden-viewer-eval-result? cell)
-                            (assoc :render-fn '(fn [_ _] [:<>]))))))))
+                            (assoc :render-fn 'nextjournal.clerk.render/render-empty-fragment)))))))
 
 (defn transform-cell [cell]
   (let [{:keys [code? result?]} (->visibility cell)]
@@ -677,7 +677,7 @@
 (def cell-viewer
   {:name `cell-viewer
    :transform-fn (update-val transform-cell)
-   :render-fn '(fn [xs opts] (into [:<>] (nextjournal.clerk.render/inspect-children opts) xs))})
+   :render-fn 'nextjournal.clerk.render/render-children})
 
 (defn lift-block-images
   "Lift an image node to top-level when it is the only child of a paragraph."
@@ -750,7 +750,7 @@
 (def table-missing-viewer
   {:name `table-missing-viewer
    :pred #{:nextjournal/missing}
-   :render-fn '(fn [x] [:<>])})
+   :render-fn 'nextjournal.clerk.render/render-empty-fragment})
 
 (def table-markup-viewer
   {:name `table-markup-viewer
@@ -809,7 +809,7 @@
    ;; formulas
    {:name :nextjournal.markdown/formula
     :transform-fn (comp :text ->value)
-    :render-fn '(fn [tex] (nextjournal.clerk.render/render-katex tex {:inline? true}))}
+    :render-fn 'nextjournal.clerk.render/render-katex-inline}
    {:name :nextjournal.markdown/block-formula
     :transform-fn (comp :text ->value)
     :render-fn 'nextjournal.clerk.render/render-katex}
@@ -855,7 +855,7 @@
     :transform-fn (fn [wrapped-value] (with-viewer `html-viewer [:sup.sidenote-ref (-> wrapped-value ->value :ref inc)]))}])
 
 (def char-viewer
-  {:name `char-viewer :pred char? :render-fn '(fn [c] [:span.cmt-string.inspected-value "\\" c])})
+  {:name `char-viewer :pred char? :render-fn 'nextjournal.clerk.render/char-viewer})
 
 (def string-viewer
   {:name `string-viewer
@@ -874,27 +874,25 @@
                                               (instance? clojure.lang.BigInt %)) pr-str))])})
 
 (def number-hex-viewer
-  {:name `number-hex-viewer :render-fn '(fn [num] (nextjournal.clerk.render/render-number (str "0x" (.toString (js/Number. num) 16))))})
+  {:name `number-hex-viewer :render-fn 'nextjournal.clerk.render/render-hex-number})
 
 (def symbol-viewer
-  {:name `symbol-viewer :pred symbol? :render-fn '(fn [x] [:span.cmt-keyword.inspected-value (str x)])})
+  {:name `symbol-viewer :pred symbol? :render-fn 'nextjournal.clerk.render/render-symbol})
 
 (def keyword-viewer
-  {:name `keyword-viewer :pred keyword? :render-fn '(fn [x] [:span.cmt-atom.inspected-value (str x)])})
+  {:name `keyword-viewer :pred keyword? :render-fn 'nextjournal.clerk.render/render-keyword})
 
 (def nil-viewer
-  {:name `nil-viewer :pred nil? :render-fn '(fn [_] [:span.cmt-default.inspected-value "nil"])})
+  {:name `nil-viewer :pred nil? :render-fn 'nextjournal.clerk.render/render-nil})
 
 (def boolean-viewer
-  {:name `boolean-viewer :pred boolean? :render-fn '(fn [x] [:span.cmt-bool.inspected-value (str x)])})
+  {:name `boolean-viewer :pred boolean? :render-fn 'nextjournal.clerk.render/render-boolean})
 
 (def map-entry-viewer
-  {:name `map-entry-viewer :pred map-entry? :render-fn '(fn [xs opts] (into [:<>] (comp (nextjournal.clerk.render/inspect-children opts) (interpose " ")) xs)) :page-size 2})
+  {:name `map-entry-viewer :pred map-entry? :render-fn 'nextjournal.clerk.render/render-map-entry :page-size 2})
 
 (def read+inspect-viewer
-  {:name `read+inspect-viewer :render-fn '(fn [x] (try [nextjournal.clerk.render/inspect (nextjournal.clerk.viewer/read-string-without-tag-table x)]
-                                                       (catch js/Error _e
-                                                         (nextjournal.clerk.render/render-unreadable-edn x))))})
+  {:name `read+inspect-viewer :render-fn 'nextjournal.clerk.render/render-read+inspect})
 
 (def vector-viewer
   {:name `vector-viewer :pred vector? :render-fn 'nextjournal.clerk.render/render-coll :opening-paren "[" :closing-paren "]" :page-size 20})
@@ -914,7 +912,7 @@
   {:name `var-viewer
    :pred var?
    :transform-fn (comp #?(:cljs var->symbol :clj symbol) ->value)
-   :render-fn '(fn [x] [:span.inspected-value [:span.cmt-meta "#'" (str x)]])})
+   :render-fn 'nextjournal.clerk.render/render-var})
 
 (defn ->opts [wrapped-value]
   (select-keys wrapped-value [:nextjournal/budget :nextjournal/css-class :nextjournal/width :nextjournal/render-opts
@@ -961,9 +959,7 @@
                                   :nextjournal/width (image-width image)}
                                  mark-presented))])
    :name `image-viewer
-   :render-fn '(fn [blob-or-url] [:div.flex.flex-col.items-center.not-prose
-                                  [:img {:src #?(:clj  (nextjournal.clerk.render/url-for blob-or-url)
-                                                 :cljs blob-or-url)}]])})
+   :render-fn 'nextjournal.clerk.render/render-image})
 
 (def ideref-viewer
   {:name `ideref-viewer
@@ -1038,21 +1034,10 @@
                        (update-val (fn [v] (if (string? v) v (str/trim (with-out-str (pprint/pprint v)))))))})
 
 (def row-viewer
-  {:name `row-viewer :render-fn '(fn [items opts]
-                                   (let [item-count (count items)]
-                                     (into [:div {:class "md:flex md:flex-row md:gap-4 not-prose"
-                                                  :style opts}]
-                                           (map (fn [item]
-                                                  [:div.flex.items-center.justify-center.flex-auto
-                                                   (nextjournal.clerk.render/inspect-presented opts item)])) items)))})
+  {:name `row-viewer :render-fn 'nextjournal.clerk.render/render-row})
 
 (def col-viewer
-  {:name `col-viewer :render-fn '(fn [items opts]
-                                   (into [:div {:class "md:flex md:flex-col md:gap-4 clerk-grid not-prose"
-                                                :style opts}]
-                                         (map (fn [item]
-                                                [:div.flex.items-center.justify-center
-                                                 (nextjournal.clerk.render/inspect-presented opts item)])) items))})
+  {:name `col-viewer :render-fn 'nextjournal.clerk.render/render-col})
 
 (def table-viewers
   [(-> string-viewer
@@ -1111,14 +1096,9 @@
 
 (def tagged-value-viewer
   {:name `tagged-value-viewer
-   :render-fn '(fn [{:keys [tag value space?]} opts]
-                 (nextjournal.clerk.render/render-tagged-value
-                  {:space? (:nextjournal/value space?)}
-                  (str "#" (:nextjournal/value tag))
-                  [nextjournal.clerk.render/inspect-presented value]))
+   :render-fn 'nextjournal.clerk.render/render-tagged-value
    :transform-fn mark-preserve-keys})
 
-
 #?(:cljs
    (def js-promise-viewer
      {:name `js-promise-viewer :pred #(instance? js/Promise %) :render-fn 'nextjournal.clerk.render/render-promise}))
@@ -1129,9 +1109,7 @@
       :pred goog/isObject
       :page-size 20
       :opening-paren "{" :closing-paren "}"
-      :render-fn '(fn [v opts] (nextjournal.clerk.render/render-tagged-value {:space? true}
-                                                                             "#js"
-                                                                             (nextjournal.clerk.render/render-map v opts)))
+      :render-fn 'nextjournal.clerk.render/render-js-object
       :transform-fn (update-val (fn [^js o]
                                   (into {}
                                         (comp (remove (fn [k] (identical? "function" (goog/typeOf (j/get o k)))))
@@ -1148,10 +1126,7 @@
      {:name `js-array-viewer
       :pred js-iterable?
       :transform-fn (update-val seq)
-      :render-fn '(fn [v opts]
-                    (nextjournal.clerk.render/render-tagged-value {:space? true}
-                                                                  "#js"
-                                                                  (nextjournal.clerk.render/render-coll v opts)))
+      :render-fn 'nextjournal.clerk.render/render-js-array
       :opening-paren "[" :closing-paren "]"
       :page-size 20}))
 
@@ -1977,12 +1952,7 @@
    :transform-fn (fn [wrapped-value]
                    (-> wrapped-value
                        mark-preserve-keys
-                       (assoc :nextjournal/viewer {:render-fn '(fn [{:keys [form val]} opts]
-                                                                 [:div.mb-3.last:mb-0
-                                                                  [:div.bg-slate-100.dark:bg-slate-800.px-4.py-2.border-l-2.border-slate-200.dark:border-slate-700
-                                                                   (nextjournal.clerk.render/inspect-presented opts form)]
-                                                                  [:div.pt-2.px-4.border-l-2.border-transparent
-                                                                   (nextjournal.clerk.render/inspect-presented opts val)]])})
+                       (assoc :nextjournal/viewer {:render-fn 'nextjournal.clerk.render/render-example})
                        (update-in [:nextjournal/value :val] maybe-wrap-var-from-def (get-in wrapped-value [:nextjournal/value :form]))
                        (update-in [:nextjournal/value :form] code)))})
 
@@ -1990,8 +1960,4 @@
   {:name `examples-viewer
    :transform-fn (update-val (fn [examples]
                                (mapv (partial with-viewer example-viewer) examples)))
-   :render-fn '(fn [examples opts]
-                 [:div
-                  [:div.uppercase.tracking-wider.text-xs.font-sans.font-bold.text-slate-500.dark:text-white.mb-2.mt-3 "Examples"]
-                  (into [:div]
-                        (nextjournal.clerk.render/inspect-children opts) examples)])})
+   :render-fn 'nextjournal.clerk.render/render-examples})