Skip to content

Commit

Permalink
Upgrade to React 18.2 and add thin cljs hooks layer (#242)
Browse files Browse the repository at this point in the history
This upgrades React to 18.2 and migrates to the new `createRoot` api. In addition, we add a thin layer over react hooks for more ergonomic cljs usage:

- the fn passed to `useEffect` returns `js/undefined` for any non-function value
- hooks that can take a deps array default to an empty array, and convert vectors/sequences to arrays
- `useState` and `useRef` return values that behave like atoms 

Co-authored-by: Matthew Huebert <mhuebert@gmail.com>
  • Loading branch information
mk and mhuebert authored Oct 28, 2022
1 parent 4c17b0f commit 10691bf
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 17 deletions.
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

0 comments on commit 10691bf

Please sign in to comment.