From 0cbac43b62632734a84cb6bcb0282bfc0cb653b0 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Sun, 23 Oct 2022 21:56:30 +0200 Subject: [PATCH 1/4] Try react 18 upgrade --- package.json | 4 ++-- resources/viewer-js-hash | 2 +- shadow-cljs.edn | 2 +- src/nextjournal/clerk/render.cljs | 10 +++++++--- src/nextjournal/clerk/static_app.cljs | 13 +++++++++---- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 5035f75ec..97b652cf4 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "markdown-it-texmath": "^0.9.1", "markdown-it-toc-done-right": "^4.2.0", "punycode": "2.1.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", "w3c-keyname": "2.2.4" }, "devDependencies": { diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index 0bdcfb225..fc9be0848 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -3kwcHSjavGDQtBtv48TBjRcx1Yf7 \ No newline at end of file +4Sj7XduDDTNti3MSJ3Wp6Q22AuC9 \ No newline at end of file diff --git a/shadow-cljs.edn b/shadow-cljs.edn index 8f15f2fce..8dcfefdaa 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -2,7 +2,7 @@ :dev-http {7778 {:roots ["public" "classpath:public"]}} :nrepl false :builds {:viewer {:target :esm - :runtime :custom ;; needed when developing ssr, node errors without it + :runtime :browser ;; needed when developing ssr, node errors without it :output-dir "public/js" :release {:output-dir "build/"} :compiler-options {:source-map true} diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 3f8fba802..cc6e5e71c 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -1,6 +1,7 @@ (ns nextjournal.clerk.render (:require ["d3-require" :as d3-require] ["react" :as react] + ["react-dom/client" :as react-client] [applied-science.js-interop :as j] [cljs.reader] [clojure.string :as str] @@ -559,10 +560,13 @@ (when-let [title (and (exists? js/document) (-> doc viewer/->value :title))] (set! (.-title js/document) title))) -(defn ^:export ^:dev/after-load mount [] +(defonce react-root (when-let [el (and (exists? js/document) (js/document.getElementById "clerk"))] - #_(rdom/unmount-component-at-node el) - (rdom/render [root] el))) + (react-client/createRoot el))) + +(defn ^:export ^:dev/after-load mount [] + (when react-root + (.render react-root (r/as-element [root])))) (defn clerk-eval [form] (.ws_send ^js goog/global (pr-str form))) diff --git a/src/nextjournal/clerk/static_app.cljs b/src/nextjournal/clerk/static_app.cljs index d4dc11fbb..915085431 100644 --- a/src/nextjournal/clerk/static_app.cljs +++ b/src/nextjournal/clerk/static_app.cljs @@ -1,8 +1,9 @@ (ns nextjournal.clerk.static-app - (:require [clojure.set :as set] + (:require ["react-dom/client" :as react-client] + [clojure.set :as set] [clojure.string :as str] - [nextjournal.clerk.sci-env :as sci-env] [nextjournal.clerk.render :as render] + [nextjournal.clerk.sci-env :as sci-env] [nextjournal.clerk.viewer :as v] [nextjournal.devcards :as dc] [reagent.core :as r] @@ -124,9 +125,13 @@ [view view-data] [:pre (pr-str match)])]])) -(defn ^:dev/after-load mount [] +(defonce react-root (when-let [el (and (exists? js/document) (js/document.getElementById "clerk-static-app"))] - (rdom/render [root] el))) + (react-client/createRoot el))) + +(defn ^:dev/after-load mount [] + (when react-root + (.render react-root (r/as-element [root])))) ;; next up ;; - jit compiling css From 0e85bfc92a2753a9914606215dc6c34b52ff2258 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Mon, 24 Oct 2022 12:55:20 +0200 Subject: [PATCH 2/4] Make plotly & vega load, still logs error --- resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render.cljs | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index fc9be0848..dbe3765aa 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -4Sj7XduDDTNti3MSJ3Wp6Q22AuC9 \ No newline at end of file +crEHBFwCv9fA6XC48uCiovbBCi2 \ No newline at end of file diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index cc6e5e71c..278dbd1cd 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -592,14 +592,23 @@ ;; TODO: remove (def reagent-viewer render-reagent) +(def async-constructor + (js/eval "(async function () {}).constructor")) + +(defn set-async! [f] + (set! (.-constructor f) async-constructor) + f) + (defn use-promise "React hook which resolves a promise and handles errors." [p] (let [handle-error (use-handle-error) [v v!] (react/useState)] - (react/useEffect (fn [] (-> p - (.then #(v! (constantly %))) - (.catch handle-error)))) + (react/useEffect (fn [] ((doto (fn [] (-> p + (.then #(v! (constantly %))) + (.catch handle-error))) + (set-async!)))) + #js []) v)) (defn ^js use-d3-require [package] From cc7059cf5a6a0a6f862723cab152108b093caee4 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Mon, 24 Oct 2022 12:57:31 +0200 Subject: [PATCH 3/4] Try returning nil from promise --- resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render.cljs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index dbe3765aa..067decb91 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -crEHBFwCv9fA6XC48uCiovbBCi2 \ No newline at end of file +2P26nwCccuUXTZQm1D1t5H5EANUT \ No newline at end of file diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 278dbd1cd..64c1aa549 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -606,6 +606,7 @@ [v v!] (react/useState)] (react/useEffect (fn [] ((doto (fn [] (-> p (.then #(v! (constantly %))) + (.then (constantly nil)) (.catch handle-error))) (set-async!)))) #js []) From 95b0db1a10b33c9852facb8151113a28d9478771 Mon Sep 17 00:00:00 2001 From: Matthew Huebert Date: Thu, 27 Oct 2022 11:50:55 +0200 Subject: [PATCH 4/4] Add a thin React hook cljs layer. --- package.json | 4 +- resources/viewer-js-hash | 2 +- src/nextjournal/clerk/render.cljs | 100 +++++++++++++++++++++++++----- 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 97b652cf4..36054baeb 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ -{"type": "module", +{ + "type": "module", "dependencies": { "@codemirror/autocomplete": "^6.0.2", "@codemirror/commands": "^6.0.0", @@ -24,6 +25,7 @@ "punycode": "2.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "use-sync-external-store": "^1.2.0", "w3c-keyname": "2.2.4" }, "devDependencies": { diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index 067decb91..9ef18c030 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -2P26nwCccuUXTZQm1D1t5H5EANUT \ No newline at end of file +2ynGsETKcfUbdNaurBZ8W9X9QuYL \ No newline at end of file diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 64c1aa549..98e31cd5f 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -2,6 +2,7 @@ (:require ["d3-require" :as d3-require] ["react" :as react] ["react-dom/client" :as react-client] + ["use-sync-external-store/shim" :refer [useSyncExternalStore]] [applied-science.js-interop :as j] [cljs.reader] [clojure.string :as str] @@ -20,6 +21,83 @@ [reagent.dom :as rdom] [reagent.ratom :as ratom])) +;; a type for wrapping react/useState to support reset! and swap! +(deftype WrappedState [st] + IIndexed + (-nth [coll i] (aget st i)) + (-nth [coll i nf] (or (aget st i) nf)) + IDeref + (-deref [^js this] (aget st 0)) + IReset + (-reset! [^js this new-value] + ;; `constantly` here ensures that if we reset state to a fn, + ;; it is stored as-is and not applied to prev value. + ((aget st 1) (constantly new-value))) + ISwap + (-swap! [this f] ((aget st 1) f)) + (-swap! [this f a] ((aget st 1) #(f % a))) + (-swap! [this f a b] ((aget st 1) #(f % a b))) + (-swap! [this f a b xs] ((aget st 1) #(apply f % a b xs)))) + +(defn- as-array [x] (cond-> x (not (array? x)) to-array)) + +(defn use-memo + "React hook: useMemo. Defaults to an empty `deps` array." + ([f] (react/useMemo f #js[])) + ([f deps] (react/useMemo f (as-array deps)))) + +(defn use-callback + "React hook: useCallback. Defaults to an empty `deps` array." + ([x] (use-callback x #js[])) + ([x deps] (react/useCallback x (to-array deps)))) + +(defn- wrap-effect + ;; utility for wrapping function to return `js/undefined` for non-functions + [f] #(let [v (f)] (if (fn? v) v js/undefined))) + +(defn use-effect + "React hook: useEffect. Defaults to an empty `deps` array. + Wraps `f` to return js/undefined for any non-function value." + ([f] (react/useEffect (wrap-effect f) #js[])) + ([f deps] (react/useEffect (wrap-effect f) (as-array deps)))) + +(defn use-state + "React hook: useState. Can be used like react/useState but also behaves like an atom." + [init] + (WrappedState. (react/useState init))) + +(defn- specify-atom! [ref-obj] + (specify! ref-obj + IDeref + (-deref [^js this] (.-current this)) + IReset + (-reset! [^js this new-value] (set! (.-current this) new-value)) + ISwap + (-swap! + ([o f] (reset! o (f o))) + ([o f a] (reset! o (f o a))) + ([o f a b] (reset! o (f o a b))) + ([o f a b xs] (reset! o (apply f o a b xs)))))) + +(defn use-ref + "React hook: useRef. Can also be used like an atom." + ([] (use-ref nil)) + ([init] (specify-atom! (react/useRef init)))) + +(defn use-sync-external-store [subscribe get-snapshot] + (useSyncExternalStore subscribe get-snapshot)) + +(defn use-watch + "Hook for reading value of an IWatchable. Compatible with reading Reagent reactions non-reactively." + [x] + (let [id (use-callback #js{})] + (use-sync-external-store + (use-callback + (fn [changed!] + (add-watch x id (fn [_ _ _ _] (changed!))) + #(remove-watch x id)) + #js[x]) + #(binding [reagent.ratom/*ratom-context* nil] @x)))) (when (exists? js/window) ;; conditionalized currently because this throws in node @@ -592,25 +670,17 @@ ;; TODO: remove (def reagent-viewer render-reagent) -(def async-constructor - (js/eval "(async function () {}).constructor")) - -(defn set-async! [f] - (set! (.-constructor f) async-constructor) - f) - (defn use-promise "React hook which resolves a promise and handles errors." [p] (let [handle-error (use-handle-error) - [v v!] (react/useState)] - (react/useEffect (fn [] ((doto (fn [] (-> p - (.then #(v! (constantly %))) - (.then (constantly nil)) - (.catch handle-error))) - (set-async!)))) - #js []) - v)) + !state (use-state nil)] + (use-effect (fn [] + (-> p + (.then #(reset! !state %)) + (.catch handle-error))) + #js []) + @!state)) (defn ^js use-d3-require [package] (let [p (react/useMemo #(apply d3-require/require