reagent/doc/ReactFeatures.md

212 lines
6.9 KiB
Markdown
Raw Permalink Normal View History

# React Features
Most React features should be usable from Reagent, even if Reagent doesn't
provide functions to use them directly.
2019-05-23 18:00:24 +00:00
## [Fragments](https://reactjs.org/docs/fragments.html)
JSX:
```js
2019-05-23 18:01:05 +00:00
function example() {
2019-05-23 18:00:24 +00:00
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
```
Reagent:
```cljs
(defn example []
[:<>
[child-a]
[child-b]
[child-c]])
```
2019-05-23 18:00:35 +00:00
Reagent syntax follows [React Fragment short syntax](https://reactjs.org/docs/fragments.html#short-syntax).
2019-05-23 18:00:24 +00:00
2019-05-23 18:05:31 +00:00
## [Context](https://reactjs.org/docs/context.html)
```cljs
(defonce my-context (react/createContext "default"))
(def Provider (.-Provider my-context))
(def Consumer (.-Consumer my-context))
2019-12-17 20:50:14 +00:00
(rdom/render
[:> Provider {:value "bar"}
[:> Consumer {}
(fn [v]
(r/as-element [:div "Context: " v]))]]
container)
```
2020-03-28 12:07:59 +00:00
[Context example project](../examples/react-context/src/example/core.cljs)
2020-03-28 12:07:24 +00:00
better explains how
`:>` or `adapt-react-class` convert the properties to JS objects,
and shows how to use Cljs values with context.
Alternatively you can use the [static contextType property](https://reactjs.org/docs/context.html#classcontexttype)
```cljs
(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))])}))
;; Alternatively with metadata on a form-1 component:
;;
;; (def show-context
;; ^{:context-type my-context}
;; (fn []
;; [:p (.-context (reagent.core/current-component))]))
2019-12-17 20:50:14 +00:00
(rdom/render
[:> Provider {:value "bar"}
[show-context]]
container)
```
Tests contain example of using old React lifecycle Context API (`context-wrapper` function):
2019-12-17 00:21:05 +00:00
[tests](https://github.com/reagent-project/reagent/blob/master/test/reagenttest/testreagent.cljs#L1159-L1168)
2019-05-23 18:05:31 +00:00
## [Error boundaries](https://reactjs.org/docs/error-boundaries.html)
[Relevant method docs](https://reactjs.org/docs/react-component.html#static-getderivedstatefromerror)
You can use `getDerivedStateFromError` (since React 16.6.0 and Reagent 0.9) (and `ComponentDidCatch`) lifecycle method with `create-class`:
```cljs
(defn error-boundary [comp]
(let [error (r/atom nil)]
(r/create-class
{:component-did-catch (fn [this e info])
2019-10-09 08:06:54 +00:00
: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))})))
```
Alternatively, one could use React state instead of RAtom to keep track of error state, which
can be more obvious with the new `getDerivedStateFromError` method:
```cljs
(defn error-boundary [comp]
(r/create-class
{:constructor (fn [this props]
(set! (.-state this) #js {:error nil}))
:component-did-catch (fn [this e info])
2019-10-09 08:06:54 +00:00
:get-derived-state-from-error (fn [error] #js {:error error})
:render (fn [this]
(r/as-element
(if @error
[:div
"Something went wrong."
[:button {:on-click #(.setState this #js {:error nil})} "Try again"]]
(into [:<>] (r/children this)))})))
```
As per React docs, `getDerivedStateFromError` is what should update the state
after error, it can be also used to update RAtom as in Reagent the Ratom is available
in function closure even for static methods. `ComponentDidCatch` can be used
for side-effects, like logging the error.
2019-05-23 18:05:31 +00:00
## [Hooks](https://reactjs.org/docs/hooks-intro.html)
Hooks can't be used inside class components, and Reagent implementation creates
a class component from every function (i.e. Reagent component).
However, you can use React components using Hooks inside Reagent, or use
[hx](https://github.com/Lokeh/hx) components inside Reagent. Also, it is
possible to create React components from Reagent quite easily, because React
function component is just a function that happens to return React elements,
and `r/as-element` does just that:
```cljs
;; This is React function component. Can't use Ratoms here!
(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 component
(defn reagent-component []
[:div
;; Note :> to use a function as React component
[:> example]])
```
If you need to pass RAtom state into these components, dereference them in
the Reagent components and pass the value (and if needed, function to update them)
as properties into the React function component.
2019-05-23 18:05:31 +00:00
## [Portals](https://reactjs.org/docs/portals.html)
```cljs
(defn reagent-component []
2020-04-15 17:23:22 +00:00
(let [el (.. js/document (getElementById "portal-el"))]
(react-dom/createPortal (r/as-element [:div "foo"]) el)))
```
2019-05-23 18:05:31 +00:00
## [Hydrate](https://reactjs.org/docs/react-dom.html#hydrate)
```cljs
(react-dom/hydrate (r/as-element [main-component]) container)
```
2020-03-29 08:42:33 +00:00
## Component classes
For interop with React libraries, you might need to pass Component classes to other components as parameter. If you have a Reagent component (a function) you can use `r/reactify-component` which returns creates a Class from the function.
If the parent Component awaits classes with some custom methods or properties, you need to be careful and probably should use `r/create-class`. In this case you don't want to use `r/reactify-component` with a function (even if the function returns a class) because `r/reactify-component` wraps the function in another Component class, and parent Component doesn't see the correct class.
```cljs
;; Correct way
(def editor
(r/create-class
{:get-input-node (fn [this] ...)
:reagent-render (fn [] [:input ...])})))
[:> SomeComponent
{:editor-component editor}]
;; Often incorrect way
(defn editor [parameter]
(r/create-class
{:get-input-node (fn [this] ...)
:reagent-render (fn [] [:input ...])})))
[:> SomeComponent
{:editor-component (r/reactify-component editor)}]
```
In the latter case, `:editor-component` is a Reagent wrapper class component, which doesn't have the `getInputNode` method and is rendered using the Component created by `create-class` and which has the method.
If you need to add static methods or properties, you need to modify `create-class` return value yourself. The function handles the built-in static-methods (`:childContextTypes :contextTypes :contextType :getDerivedStateFromProps :getDerivedStateFromError`), but not others.
```cljs
(let [klass (r/create-class ...)]
(set! (.-static-property klass) "foobar")
(set! (.-static-method klass) (fn [param] ...))
klass)
```