Merge pull request #443 from reagent-project/react-16-9

React 16.9
This commit is contained in:
Juho Teperi 2019-08-19 10:04:52 +03:00 committed by GitHub
commit bd8603dce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 210 additions and 43 deletions

View File

@ -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)

View File

@ -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",

View File

@ -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"]

View File

@ -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"}}

View File

@ -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]

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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")

View File

@ -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))))))