diff --git a/CHANGELOG.md b/CHANGELOG.md index db1e246..e480b00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,13 @@ **[compare](https://github.com/reagent-project/reagent/compare/v0.8.1...master)** +- Default to React 16.9 - Fix using `with-let` macro in namespaces with `*warn-on-infer*` enabled ([#420](https://github.com/reagent-project/reagent/issues/420)) - Fix using metadata to set React key with Fragment shortcut (`:<>`) ([#401](https://github.com/reagent-project/reagent/issues/401)) - Create React Component without `create-react-class` ([#416](https://github.com/reagent-project/reagent/issues/416)) + - `React.Component` doesn't have `getInitialState` method, but this is implemented by + Reagent for compatibility with old components. + - `constructor` can be used to initialize components (e.g. set the state) - Allow any number of arguments for `reagent.core/merge-props` and ensure `:class` is merged correctly when it is defined as collection. ([#412](https://github.com/reagent-project/reagent/issues/412)) - Add `reagent.core/class-names` utility functions which can be used @@ -17,6 +21,9 @@ uses correct Object interop forms, allowing use of ClojureScript `:checked-array - Deprecated `reagent.interop` namespace - It is better to use proper object interop forms or `goog.object` functions instead. - Drop `:export` metadata from `force-update-all` function +- `componentWillReceiveProps`, `componentWillUpdate` and `componentWillMount` lifecycle methods are deprecated + - Using these directly will show warning, using `UNSAFE_` prefixed version will silence the warning. + - These methods will continue to work with React 16.9 and 17. ## 0.8.1 (2018-05-15) diff --git a/package.json b/package.json index 3451920..e252d8f 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,11 @@ "dependencies": { "@cljs-oss/module-deps": "1.1.1", "prop-types": "15.6.2", - "react": "16.8.6", - "react-dom": "16.8.6" + "react": "16.9.0", + "react-dom": "16.9.0" + }, + "scripts": { + "start": "lein figwheel client-npm" }, "devDependencies": { "gzip-size-cli": "3.0.0", diff --git a/project.clj b/project.clj index 2f69200..ba59ed8 100644 --- a/project.clj +++ b/project.clj @@ -7,9 +7,9 @@ ;; If :npm-deps enabled, these are used only for externs. ;; Without direct react dependency, other packages, ;; like react-leaflet might have closer dependency to a other version. - [cljsjs/react "16.8.6-0"] - [cljsjs/react-dom "16.8.6-0"] - [cljsjs/react-dom-server "16.8.6-0"]] + [cljsjs/react "16.9.0-0"] + [cljsjs/react-dom "16.9.0-0"] + [cljsjs/react-dom-server "16.9.0-0"]] :plugins [[lein-cljsbuild "1.1.7"] [lein-doo "0.1.11"] diff --git a/src/deps.cljs b/src/deps.cljs index 3c109f0..e949044 100644 --- a/src/deps.cljs +++ b/src/deps.cljs @@ -1,2 +1,2 @@ -{:npm-deps {"react" "16.8.6" - "react-dom" "16.8.6"}} +{:npm-deps {"react" "16.9.0" + "react-dom" "16.9.0"}} diff --git a/src/reagent/core.cljs b/src/reagent/core.cljs index 027957a..af96b5e 100644 --- a/src/reagent/core.cljs +++ b/src/reagent/core.cljs @@ -106,22 +106,40 @@ "Creates JS class based on provided Clojure map, for example: ```cljs - {:get-initial-state (fn [this]) - :component-will-receive-props (fn [this new-argv]) + {;; Constructor + :constructor (fn [this props]) + :get-initial-state (fn [this]) + ;; Static methods + :get-derived-state-from-props (fn [props state] partial-state) + :get-derived-state-from-error (fn [error] partial-state) + ;; Methods + :get-snapshot-before-update (fn [this old-argv new-argv] snapshot) :should-component-update (fn [this old-argv new-argv]) - :component-will-mount (fn [this]) :component-did-mount (fn [this]) - :component-will-update (fn [this new-argv]) - :component-did-update (fn [this old-argv]) + :component-did-update (fn [this old-argv old-state snapshot]) :component-will-unmount (fn [this]) - :reagent-render (fn [args....])} ;; or :render (fn [this]) + :component-did-catch (fn [this error info]) + :reagent-render (fn [args....]) + ;; Or alternatively: + :render (fn [this]) + ;; Deprecated methods: + :UNSAFE_component-will-receive-props (fn [this new-argv]) + :UNSAFE_component-will-update (fn [this new-argv new-state]) + :UNSAFE_component-will-mount (fn [this])} ``` Everything is optional, except either :reagent-render or :render. Map keys should use `React.Component` method names (https://reactjs.org/docs/react-component.html), and can be provided in snake-case or camelCase. - Constructor function is defined using key `:get-initial-state`. + + State can be initialized using constructor, which matches React.Component class, + or using getInitialState which matches old React createClass function and is + now implemented by Reagent for compatibility. + + State can usually be anything, e.g. Cljs object. But if using getDerivedState + methods, the state has to be plain JS object as React implementation uses + Object.assign to merge partial state into the current state. React built-in static methods or properties are automatically defined as statics." [spec] diff --git a/src/reagent/impl/batching.cljs b/src/reagent/impl/batching.cljs index 81385d8..5282c81 100644 --- a/src/reagent/impl/batching.cljs +++ b/src/reagent/impl/batching.cljs @@ -23,9 +23,14 @@ (.-msRequestAnimationFrame w) fake-raf)))) -(defn compare-mount-order [c1 c2] - (- (.-cljsMountOrder c1) - (.-cljsMountOrder c2))) +(defn compare-mount-order + [c1 c2] + ;; Mount order is now set in DidMount method. I.e. the + ;; top-most component is mounted last and gets largest + ;; number. This is reverse compared to WillMount where method + ;; for top component gets called first. + (- (.-cljsMountOrder c2) + (.-cljsMountOrder c1))) (defn run-queue [a] ;; sort components by mount order, to make sure parents diff --git a/src/reagent/impl/component.cljs b/src/reagent/impl/component.cljs index 851ad05..c4b1552 100644 --- a/src/reagent/impl/component.cljs +++ b/src/reagent/impl/component.cljs @@ -162,15 +162,30 @@ :getDefaultProps (throw (js/Error. "getDefaultProps not supported")) + :getDerivedStateFromProps + (fn getDerivedStateFromProps [props state] + ;; Read props from Reagent argv + (.call f nil (if-some [a (.-argv props)] (extract-props a) props) state)) + ;; In ES6 React, this is now part of the constructor :getInitialState (fn getInitialState [c] (reset! (state-atom c) (.call f c c))) + :getSnapshotBeforeUpdate + (fn getSnapshotBeforeUpdate [oldprops oldstate] + (this-as c (.call f c c (props-argv c oldprops) oldstate))) + + ;; Deprecated - warning in 16.9 will work through 17.x :componentWillReceiveProps (fn componentWillReceiveProps [nextprops] (this-as c (.call f c c (props-argv c nextprops)))) + ;; Deprecated - will work in 17.x + :UNSAFE_componentWillReceiveProps + (fn componentWillReceiveProps [nextprops] + (this-as c (.call f c c (props-argv c nextprops)))) + :shouldComponentUpdate (fn shouldComponentUpdate [nextprops nextstate] (or util/*always-update* @@ -188,24 +203,38 @@ noargv (.call f c c (get-argv c) (props-argv c nextprops)) :else (.call f c c old-argv new-argv)))))) + ;; Deprecated - warning in 16.9 will work through 17.x :componentWillUpdate - (fn componentWillUpdate [nextprops] - (this-as c (.call f c c (props-argv c nextprops)))) + (fn componentWillUpdate [nextprops nextstate] + (this-as c (.call f c c (props-argv c nextprops) nextstate))) + + ;; Deprecated - will work in 17.x + :UNSAFE_componentWillUpdate + (fn componentWillUpdate [nextprops nextstate] + (this-as c (.call f c c (props-argv c nextprops) nextstate))) :componentDidUpdate - (fn componentDidUpdate [oldprops] - (this-as c (.call f c c (props-argv c oldprops)))) + (fn componentDidUpdate [oldprops oldstate snapshot] + (this-as c (.call f c c (props-argv c oldprops) oldstate snapshot))) + ;; Deprecated - warning in 16.9 will work through 17.x :componentWillMount (fn componentWillMount [] - (this-as c - (set! (.-cljsMountOrder c) (batch/next-mount-count)) - (when-not (nil? f) - (.call f c c)))) + (this-as c (.call f c c))) + + ;; Deprecated - will work in 17.x + :UNSAFE_componentWillMount + (fn componentWillMount [] + (this-as c (.call f c c))) :componentDidMount (fn componentDidMount [] - (this-as c (.call f c c))) + (this-as c + ;; This method is called after everything inside the + ;; has been mounted. This is reverse compared to WillMount. + (set! (.-cljsMountOrder c) (batch/next-mount-count)) + (when-not (nil? f) + (.call f c c)))) :componentWillUnmount (fn componentWillUnmount [] @@ -230,14 +259,14 @@ ;; Though the value is nil here, the wrapper function will be ;; added to class to manage Reagent ratom lifecycle. (def obligatory {:shouldComponentUpdate nil - :componentWillMount nil + :componentDidMount nil :componentWillUnmount nil}) -(def dash-to-camel (util/memoize-1 util/dash-to-camel)) +(def dash-to-method-name (util/memoize-1 util/dash-to-method-name)) (defn camelify-map-keys [fun-map] (reduce-kv (fn [m k v] - (assoc m (-> k dash-to-camel keyword) v)) + (assoc m (-> k dash-to-method-name keyword) v)) {} fun-map)) (defn add-obligatory [fun-map] @@ -297,17 +326,20 @@ [body] {:pre [(map? body)]} (let [body (cljsify body) - methods (map-to-js (apply dissoc body :displayName :getInitialState + methods (map-to-js (apply dissoc body :displayName :getInitialState :constructor :render :reagentRender built-in-static-method-names)) static-methods (map-to-js (select-keys body built-in-static-method-names)) display-name (:displayName body) - construct (:getInitialState body) + get-initial-state (:getInitialState body) + construct (:constructor body) cmp (fn [props context updater] (this-as this (.call react/Component this props context updater) (when construct - (construct this)) + (construct this props)) + (when get-initial-state + (set! (.-state this) (get-initial-state this))) this))] (gobj/extend (.-prototype cmp) (.-prototype react/Component) methods) diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs index 6fdf419..01b704a 100644 --- a/src/reagent/impl/template.cljs +++ b/src/reagent/impl/template.cljs @@ -46,7 +46,7 @@ (if (named? k) (if-some [k' (cache-get prop-name-cache (name k))] k' - (let [v (util/dash-to-camel k)] + (let [v (util/dash-to-prop-name k)] (gobj/set prop-name-cache (name k)) v)) k)) @@ -78,7 +78,7 @@ (if (named? k) (if-some [k' (cache-get custom-prop-name-cache (name k))] k' - (let [v (util/dash-to-camel k)] + (let [v (util/dash-to-prop-name k)] (gobj/set custom-prop-name-cache (name k) v) v)) k)) diff --git a/src/reagent/impl/util.cljs b/src/reagent/impl/util.cljs index a590c20..c055394 100644 --- a/src/reagent/impl/util.cljs +++ b/src/reagent/impl/util.cljs @@ -28,7 +28,7 @@ (string/upper-case s) (str (string/upper-case (subs s 0 1)) (subs s 1)))) -(defn dash-to-camel [dashed] +(defn dash-to-prop-name [dashed] (if (string? dashed) dashed (let [name-str (name dashed) @@ -37,6 +37,14 @@ name-str (apply str start (map capitalize parts)))))) +(defn dash-to-method-name [dashed] + (if (string? dashed) + dashed + (let [name-str (name dashed) + name-str (string/replace name-str #"(unsafe|UNSAFE)[-_]" "UNSAFE_") + [start & parts] (string/split name-str #"-")] + (apply str start (map capitalize parts))))) + (defn fun-name [f] (let [n (or (and (fn? f) (or (.-displayName f) diff --git a/test/reagent/impl/util_test.cljs b/test/reagent/impl/util_test.cljs index 311d4a3..3c312bf 100644 --- a/test/reagent/impl/util_test.cljs +++ b/test/reagent/impl/util_test.cljs @@ -19,6 +19,20 @@ (is (= "a b c d" (util/class-names "a" "b" nil ["c" "d"])))) +(deftest dash-to-prop-name-test + (is (= "tabIndex" (util/dash-to-prop-name :tab-index))) + (is (= "data-foo-bar" (util/dash-to-prop-name :data-foo-bar)))) + +(deftest dash-to-method-name-test + (is (= "componentDidMount" + (util/dash-to-method-name :component-did-mount))) + (is (= "componentDidMount" + (util/dash-to-method-name :componentDidMount))) + (is (= "UNSAFE_componentDidMount" + (util/dash-to-method-name :unsafe-component-did-mount))) + (is (= "UNSAFE_componentDidMount" + (util/dash-to-method-name :unsafe_componentDidMount)))) + ; (simple-benchmark [] ; (do (util/class-names "a" "b") ; (util/class-names nil "a") diff --git a/test/reagenttest/testreagent.cljs b/test/reagenttest/testreagent.cljs index 2209ff6..904aaf1 100644 --- a/test/reagenttest/testreagent.cljs +++ b/test/reagenttest/testreagent.cljs @@ -7,6 +7,7 @@ [reagent.dom.server :as server] [reagent.impl.util :as util] [reagenttest.utils :as u :refer [with-mounted-component found-in]] + [clojure.string :as string] [goog.string :as gstr] [goog.object :as gobj] [prop-types :as prop-types])) @@ -740,7 +741,7 @@ (reset! t (first args)) (add-args :initial-state args) {:foo "bar"}) - :component-will-mount + :UNSAFE_component-will-mount (fn [& args] (this-as c (is (= c (first args)))) (add-args :will-mount args)) @@ -752,11 +753,11 @@ (fn [& args] (this-as c (is (= c (first args)))) (add-args :should-update args) true) - :component-will-receive-props + :UNSAFE_component-will-receive-props (fn [& args] (this-as c (is (= c (first args)))) (add-args :will-receive args)) - :component-will-update + :UNSAFE_component-will-update (fn [& args] (this-as c (is (= c (first args)))) (add-args :will-update args)) @@ -794,11 +795,11 @@ (is (= (:should-update @res) {:at 6 :args [@t [@comp "a" "b"] [@comp "a" "c"]]})) (is (= (:will-update @res) - {:at 7 :args [@t [@comp "a" "c"]]})) + {:at 7 :args [@t [@comp "a" "c"] {:foo "bar"}]})) (is (= (:render @res) {:at 8 :args ["a" "c"]})) (is (= (:did-update @res) - {:at 9 :args [@t [@comp "a" "b"]]})))] + {:at 9 :args [@t [@comp "a" "b"] {:foo "bar"} nil]})))] (when isClient (with-mounted-component [c2] check) (is (= (:will-unmount @res) @@ -839,7 +840,7 @@ (reset! oldprops (-> args first r/props)) (add-args :initial-state args) {:foo "bar"}) - :component-will-mount + :UNSAFE_component-will-mount (fn [& args] (this-as c (is (= c (first args)))) (add-args :will-mount args)) @@ -851,14 +852,14 @@ (fn [& args] (this-as c (is (= c (first args)))) (add-args :should-update args) true) - :component-will-receive-props + :UNSAFE_component-will-receive-props (fn [& args] (reset! newprops (-> args second second)) (this-as c (is (= c (first args))) (add-args :will-receive (into [(dissoc (r/props c) :children)] (:children (r/props c)))))) - :component-will-update + :UNSAFE_component-will-update (fn [& args] (this-as c (is (= c (first args)))) (add-args :will-update args)) @@ -1266,3 +1267,82 @@ (is (not @error-thrown-after-updating-props)))))] (is (re-find #"Warning: Exception thrown while comparing argv's in shouldComponentUpdate:" (first (:warn e)))))))) + +(deftest get-derived-state-from-props-test + (when isClient + (let [prop (r/atom 0) + ;; Usually one can use Cljs object as React state. However, + ;; getDerivedStateFromProps implementation in React uses + ;; Object.assign to merge current state and partial state returned + ;; from the method, so the state has to be plain old object. + pure-component (r/create-class + {:constructor (fn [this] + (set! (.-state this) #js {})) + :get-derived-state-from-props (fn [props state] + ;; "Expensive" calculation based on the props + #js {:v (string/join " " (repeat (inc (:value props)) "foo"))}) + :render (fn [this] + (r/as-element [:p "Value " (gobj/get (.-state this) "v")]))}) + component (fn [] + [pure-component {:value @prop}])] + (with-mounted-component [component] + (fn [c div] + (is (found-in #"Value foo" div)) + (swap! prop inc) + (r/flush) + (is (found-in #"Value foo foo" div))))))) + +(deftest get-derived-state-from-error-test + (when isClient + (let [prop (r/atom 0) + component (r/create-class + {:constructor (fn [this props] + (set! (.-state this) #js {:hasError false})) + :get-derived-state-from-error (fn [error] + #js {:hasError true}) + :component-did-catch (fn [this e info]) + :render (fn [this] + (js/console.log (r/children this)) + (r/as-element (if (.-hasError (.-state this)) + [:p "Error"] + (into [:<>] (r/children this)))))}) + bad-component (fn [] + (if (= 0 @prop) + [:div "Ok"] + (throw (js/Error. "foo"))))] + (wrap-capture-window-error + (wrap-capture-console-error + #(with-mounted-component [component [bad-component]] + (fn [c div] + (is (found-in #"Ok" div)) + (swap! prop inc) + (r/flush) + (is (found-in #"Error" div))))))))) + +(deftest get-snapshot-before-update-test + (when isClient + (let [ref (react/createRef) + prop (r/atom 0) + did-update (atom nil) + component (r/create-class + {:get-snapshot-before-update (fn [this [_ prev-props] prev-state] + {:height (.. ref -current -scrollHeight)}) + :component-did-update (fn [this [_ prev-props] prev-state snapshot] + (reset! did-update snapshot)) + :render (fn [this] + (r/as-element + [:div + {:ref ref + :style {:height "20px"}} + "foo"]))}) + component-2 (fn [] + [component {:value @prop}])] + (with-mounted-component [component-2] + (fn [c div] + ;; Attach to DOM to get real height value + (.appendChild js/document.body div) + (is (found-in #"foo" div)) + (swap! prop inc) + (r/flush) + (is (= {:height 20} @did-update)) + (.removeChild js/document.body div))))))