Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to React 18.2 and add thin cljs hooks layer #242

Merged
merged 5 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{"type": "module",
{
"type": "module",
"dependencies": {
"@codemirror/autocomplete": "^6.0.2",
"@codemirror/commands": "^6.0.0",
Expand All @@ -22,8 +23,9 @@
"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",
"use-sync-external-store": "^1.2.0",
"w3c-keyname": "2.2.4"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion resources/viewer-js-hash
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3qePCgpcmuVcJNqR8mK6LSfWdRT2
8j4R6ZW8WbFaZJhWFH3rAv4eY2J
2 changes: 1 addition & 1 deletion shadow-cljs.edn
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
100 changes: 92 additions & 8 deletions src/nextjournal/clerk/render.cljs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
(ns nextjournal.clerk.render
(: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]
Expand All @@ -19,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
Expand Down Expand Up @@ -560,10 +639,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)))
Expand Down Expand Up @@ -593,11 +675,13 @@
"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))))
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
Expand Down
13 changes: 9 additions & 4 deletions src/nextjournal/clerk/static_app.cljs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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
Expand Down