October 10, 2020
By: Kevin

Reagent文档: React Features

  1. 片段(Fragments)
  2. 上下文(Context)
  3. 错误边界(Error boundaries)
  4. 函数组件(Function components)
  5. 钩子(Hooks)
    1. 1.0 版本之前的解决方案
  6. 门户(Portals)
  7. 服务端渲染(Hydrate)
  8. 组件类

这是Reagent文档的中文翻译,Reagent 是一个为 ClojureScript 构建的轻量级 React.js 接口。

即使 Reagent 没有直接提供函数来使用它们,大多数 React 功能也应该可以在 Reagent 中使用。

片段(Fragments)

JSX 示例:

function example() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  );
}

Reagent 示例:

(defn example []
  [:<>
   [child-a]
   [child-b]
   [child-c]])

Reagent 语法遵循 React 片段的简短语法

上下文(Context)

(defonce my-context (react/createContext "default"))

(def Provider (.-Provider my-context))
(def Consumer (.-Consumer my-context))

(rdom/render
  [:> Provider {:value "bar"}
   [:> Consumer {}
    (fn [v]
      (r/as-element [:div "Context: " v]))]]
  container)

上下文示例项目 更好地解释了如何将属性转换为 JS 对象,并展示了如何在上下文中使用 Cljs 值。

或者,您可以使用 静态 contextType 属性

(defonce my-context (react/createContext "default"))

(def Provider (.-Provider my-context))

(defn show-context []
  (r/create-class
   {:context-type my-context
    :reagent-render (fn []
                      [:p (.-context (reagent.core/current-component))])}))

;; 或者用元数据在 form-1 组件上:
;;
;; (def show-context
;;   ^{:context-type my-context}
;;   (fn []
;;     [:p (.-context (reagent.core/current-component))]))

(rdom/render
  [:> Provider {:value "bar"}
   [show-context]]
  container)

上下文值也可以使用 useContext 钩子获得:

(defn show-context []
  (let [v (react/useContext my-context)]
    [:p v]))

(rdom/render
  [:> Provider {:value "bar"}
   [:f> show-context]]
  container)

测试包含使用旧版 React 生命周期上下文 API 的示例(context-wrapper 函数):测试

错误边界(Error boundaries)

相关方法文档

您可以使用 getDerivedStateFromError(自 React 16.6.0 和 Reagent 0.9 起)和 ComponentDidCatch 生命周期方法与 create-class 一起使用:

(defn error-boundary [comp]
  (let [error (r/atom nil)]
    (r/create-class
      {:component-did-catch (fn [this e info])
       :get-derived-state-from-error (fn [e]
                                            (reset! error e)
                                            #js {})
       :reagent-render (fn [comp]
                          (if @error
                            [:div
                             "Something went wrong."
                             [:button {:on-click #(reset! error nil)} "Try again"]]
                            comp))})))

或者,可以使用 React 状态而不是 RAtom 来跟踪错误状态,这在新的 getDerivedStateFromError 方法中更明显:

(defn error-boundary [comp]
  (r/create-class
    {:constructor (fn [this props]
                    (set! (.-state this) #js {:error nil}))
     :component-did-catch (fn [this e info])
     :get-derived-state-from-error (fn [error] #js {:error error})
     :render (fn [this]
               (r/as-element
                 (if-let [error (.. this -state -error)]
                   [:div
                    "Something went wrong."
                    [:button {:on-click #(.setState this #js {:error nil})} "Try again"]]


                   comp)))}))

根据 React 文档,getDerivedStateFromError 应该在错误后更新状态,也可以用来更新 RAtom,因为在 Reagent 中,即使对于静态方法,Ratom 也可在函数闭包中使用。ComponentDidCatch 可用于副作用,如记录错误。

函数组件(Function components)

JavaScript 函数是有效的 React 组件,但 Reagent 默认将在 Hiccup 向量中引用的 ClojureScript 函数转换为类组件。

然而,某些 React 功能,如钩子(Hooks),仅适用于函数式组件。有几种方法可以在 Reagent 中使用函数作为组件:

直接使用 r/create-element 调用 ClojureScript 函数不会在任何 Reagent 包装器中包装组件,并将创建函数式组件。在这种情况下,您需要在函数内使用 r/as-element 将 Hiccup 样式标记转换为元素,或者直接返回 React 元素。您也不能在这里使用 Ratoms,因为 Ratom 实现要求组件被 Reagent 包装。

:r> 快捷方式可用于创建类似于 r/create-element 的组件,子 Hiccup 形式会自动转换为 React 元素。

使用 adapt-react-class:> 也会调用 create-element,但这还会自动将 ClojureScript 参数转换为 JS 对象,如果组件是 ClojureScript 函数,则通常不需要这样做。

新的方法是配置 Reagent Hiccup 编译器来创建函数式组件:阅读编译器文档

:f> 快捷方式可用于从 Reagent 组件(函数)创建函数式组件,其中 RAtoms 和钩子(Hooks)都有效。

钩子(Hooks)

;; 这是与 :f> 一起使用的,所以这里的 Hooks 和 RAtoms 都有效
(defn example []
  (let [[count set-count] (react/useState 0)]
    [:div
     [:p "You clicked " count " times"]
     [:button
      {:on-click #(set-count inc)}
      "Click"]])))

(defn root []
  [:div
   [:f> example]])

1.0 版本之前的解决方案

注意:此部分仍然指的是在类组件中使用钩子(Hooks)的解决方法,请阅读上一节以创建函数式组件。

钩子不能在类组件中使用,Reagent 实现会从每个函数(即 Reagent 组件)创建一个类组件。

然而,您可以在 Reagent 中使用使用钩子的 React 组件,或在 Reagent 中使用 hx 组件。此外,由于 React 函数组件只是一个恰好返回 React 元素的函数,而 r/as-element 正好做到这一点,因此很容易从 Reagent 创建 React 组件:

;; 这是 React 函数组件。这里不能使用 Ratoms!
(defn example []
  (let [[count set-count] (react/useState 0)]
    (r/as-element
      [:div
       [:p "You clicked " count " times"]
       [:button
        {:on-click #(set-count inc)}
        "Click"]])))

;; Reagent 组件
(defn reagent-component []
  [:div
   ;; 注意 :> 用作 React 组件的函数
   [:> example]])

如果您需要将 RAtom 状态传递给这些组件,请在 Reagent 组件中解引用它们,并将值(如果需要,还有更新它们的函数)作为属性传递给 React 函数组件。

门户(Portals)

(defn reagent-component []
  (let [el (.. js/document (getElementById "portal-el"))]
    (react-dom/createPortal (r/as-element [:div "foo"]) el)))

服务端渲染(Hydrate)

(react-dom/hydrate (r/as-element [main-component]) container)

组件类

为了与 React 库互操作,你可能需要将组件类作为参数传递给其他组件。如果你有一个 Reagent 组件(一个函数),你可以使用 r/reactify-component,它会从该函数创建一个类。

如果父组件需要带有某些自定义方法或属性的类,你需要小心,可能应该使用 r/create-class。在这种情况下,你不想使用 r/reactify-component 和一个函数(即使该函数返回一个类),因为 r/reactify-component 会将该函数包装在另一个组件类中,父组件看不到正确的类。

;; 正确的方式
(def editor
  (r/create-class
    {:get-input-node (fn [this] ...)
     :reagent-render (fn [] [:input ...])}))

[:> SomeComponent
 {:editor-component editor}]

;; 通常不正确的方式
(defn editor [parameter]
  (r/create-class
    {:get-input-node (fn [this] ...)
     :reagent-render (fn [] [:input ...])}))

[:> SomeComponent
 {:editor-component (r/reactify-component editor)}]

在后一种情况中,:editor-component 是一个 Reagent 包装类组件,它没有 getInputNode 方法,并且使用 create-class 创建的组件进行渲染,该组件具有该方法。

如果你需要添加静态方法或属性,你需要自己修改 create-class 的返回值。该函数处理内置的静态方法(:childContextTypes :contextTypes :contextType :getDerivedStateFromProps :getDerivedStateFromError),但不处理其他方法。

(let [klass (r/create-class ...)]
  (set! (.-static-property klass) "foobar")
  (set! (.-static-method klass) (fn [param] ...))
  klass)
Tags: Reagent react clojurescript