Skip to content

Commit

Permalink
Merge pull request #477 from reagent-project/feature/functional-compo…
Browse files Browse the repository at this point in the history
…nents

Test creating functional components
  • Loading branch information
Deraen authored Apr 26, 2020
2 parents 8f2d37f + 14cbfea commit 4decfa6
Show file tree
Hide file tree
Showing 26 changed files with 2,243 additions and 1,597 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@

### Features and changes

- **Option to render Reagent components as React functional components instead of
class components**
- To ensure backwards compatibility by default, Reagent works as previously and
by default creates class components.
- New Compiler object can be created and passed to functions to control
how Reagent converts Hiccup-style markup to React components and classes:
`(r/create-compiler {:functional-components? true})`
- Passing this options to `render`, `as-element` and other calls will control how
that markup tree will be converted.
- `(r/set-default-compiler! compiler)` call can be used to set the default
compiler object for all calls.
- [Read more](./doc/reagent-compiler.md)
- [Check example](./example/functional-components-and-hooks/src/example/core.cljs)
- Change RAtom (all types) print format to be readable using ClojureScript reader,
similar to normal Atom ([#439](https://github.com/reagent-project/reagent/issues/439))
- Old print output: `#<Atom: 0>`
Expand Down
5 changes: 5 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
coverage:
 status:
 patch:
 default:
 enabled: no
32 changes: 29 additions & 3 deletions doc/ReactFeatures.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,34 @@ after error, it can be also used to update RAtom as in Reagent the Ratom is avai
in function closure even for static methods. `ComponentDidCatch` can be used
for side-effects, like logging the error.

## [Function components](https://reactjs.org/docs/components-and-props.html#function-and-class-components)

JavaScript functions are valid React components, but Reagent implementation
by default turns the ClojureScript functions referred in Hiccup-vectors to
Class components.

However, some React features, like Hooks, only work with Functional components.
There are several ways to use functions as components with Reagent:

Calling `r/create-element` directly with a ClojureScript function doesn't
wrap the component in any Reagent wrappers, and will create functional components.
In this case you need to use `r/as-element` inside the function to convert
Hiccup-style markup to elements, or just returns React Elements yourself.
You also can't use Ratoms here, as Ratom implementation requires the component
is wrapped by Reagent.

Using `adapt-react-class` or `:>` is also calls `create-element`, but that
also does automatic conversion of ClojureScript parameters to JS objects,
which isn't usually desired if the component is ClojureScript function.

New way is to configure Reagent Hiccup-compiler to create functional components:
[Read Compiler documentation](./ReagentCompiler.md)

## [Hooks](https://reactjs.org/docs/hooks-intro.html)

NOTE: This section still refers to workaround using Hooks inside
class components, read the previous section to create functional components.

Hooks can't be used inside class components, and Reagent implementation creates
a class component from every function (i.e. Reagent component).

Expand Down Expand Up @@ -184,16 +210,16 @@ If the parent Component awaits classes with some custom methods or properties, y
(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)}]
```
Expand Down
46 changes: 46 additions & 0 deletions doc/ReagentCompiler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Reagent Compiler

Reagent Compiler object is a new way to configure how Reagent
turns the Hiccup-style markup into React components and elements.

As a first step, this can be used to turn on option to create
functional components when a function is referred in a Hiccup vector:
`[component-fn parameters]`.

[./ReactFeatures.md#hooks](React more about Hooks)

```cljs
(def functional-compiler (r/create-compiler {:functional-components? true}))

;; Using the option
(r/render [main] div functional-compiler)
(r/as-element [main] functional-compiler)
;; Setting compiler as the default
(r/set-default-compiler! functional-compiler)
```

## Reasoning

Now that this mechanism to control how Reagent compiles Hiccup-style markup
to React calls is in place, it will be probably used later to control
some other things also:

From [Clojurist Together announcenment](https://www.clojuriststogether.org/news/q1-2020-funding-announcement/):

> As this [hooks] affects how Reagent turns Hiccup to React elements and components, I
> have some ideas on allowing users configure the Reagent Hiccup compiler,
> similar to what [Hicada](https://github.com/rauhs/hicada) does. This would also allow introducing optional
> features which would break existing Reagent code, by making users opt-in to
> these. One case would be to make React component interop simpler.
Some ideas:

- Providing options to control how component parameters are converted to JS
objects (or even disable automatic conversion)
- Implement support for custom tags (if you can provide your own function
to create element from a keyword, this will be easy)

Open questions:

- Will this cause problems for libraries? Do the libraries have to start
calling `as-element` with their own Compiler to ensure compatibility.
7 changes: 7 additions & 0 deletions examples/functional-components-and-hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Reagent example app

Run "`lein figwheel`" in a terminal to compile the app, and then open http://localhost:3449

Any changes to ClojureScript source files (in `src`) will be reflected in the running page immediately (while "`lein figwheel`" is running).

Run "`lein clean; lein with-profile prod compile`" to compile an optimized version.
32 changes: 32 additions & 0 deletions examples/functional-components-and-hooks/project.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
(defproject functional-components-and-hooks "0.6.0"
:dependencies [[org.clojure/clojure "1.10.1"]
[org.clojure/clojurescript "1.10.597"]
[reagent "1.0.0-SNAPSHOT"]
[figwheel "0.5.19"]]

:plugins [[lein-cljsbuild "1.1.7"]
[lein-figwheel "0.5.19"]]

:resource-paths ["resources" "target"]
:clean-targets ^{:protect false} [:target-path]

:profiles {:dev {:cljsbuild
{:builds {:client
{:figwheel {:on-jsload "example.core/run"}
:compiler {:main "example.core"
:optimizations :none}}}}}

:prod {:cljsbuild
{:builds {:client
{:compiler {:optimizations :advanced
:elide-asserts true
:pretty-print false}}}}}}

:figwheel {:repl false
:http-server-root "public"}

:cljsbuild {:builds {:client
{:source-paths ["src"]
:compiler {:output-dir "target/public/client"
:asset-path "client"
:output-to "target/public/client.js"}}}})
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

div, h1, input {
font-family: HelveticaNeue, Helvetica;
color: #777;
}

.example-clock {
font-size: 128px;
line-height: 1.2em;
font-family: HelveticaNeue-UltraLight, Helvetica;
}

@media (max-width: 768px) {
.example-clock {
font-size: 64px;
}
}

.color-input, .color-input input {
font-size: 24px;
line-height: 1.5em;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Example</title>
<link rel="stylesheet" href="example.css">
</head>
<body>
<div id="app">
<h1>Reagent example app – see README.md</h1>
</div>
<script src="client.js"></script>
</body>
</html>
43 changes: 43 additions & 0 deletions examples/functional-components-and-hooks/src/example/core.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
(ns example.core
(:require [reagent.core :as r]
[reagent.dom :as rdom]
[clojure.string :as str]
["react" :as react]))

;; Same as simpleexample, but uses Hooks instead of Ratoms

(defn greeting [message]
[:h1 message])

(defn clock [time-color]
(let [[timer update-time] (react/useState (js/Date.))
time-str (-> timer .toTimeString (str/split " ") first)]
(react/useEffect
(fn []
(let [i (js/setInterval #(update-time (js/Date.)) 1000)]
(fn []
(js/clearInterval i)))))
[:div.example-clock
{:style {:color time-color}}
time-str]))

(defn color-input [time-color update-time-color]
[:div.color-input
"Time color: "
[:input {:type "text"
:value time-color
:on-change #(update-time-color (-> % .-target .-value))}]])

(defn simple-example []
(let [[time-color update-time-color] (react/useState "#f34")]
[:div
[greeting "Hello world, it is now"]
[clock time-color]
[color-input time-color update-time-color]]))

(def functional-compiler (r/create-compiler {:functional-components? true}))

(defn run []
(rdom/render [simple-example] (js/document.getElementById "app") functional-compiler))

(run)
8 changes: 6 additions & 2 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject reagent "0.10.0"
(defproject reagent "1.0.0-SNAPSHOT"
:url "http://github.com/reagent-project/reagent"
:license {:name "MIT"}
:description "A simple ClojureScript interface to React"
Expand All @@ -25,16 +25,20 @@

:profiles {:dev {:dependencies [[org.clojure/clojurescript "1.10.597"]
[figwheel "0.5.19"]
[figwheel-sidecar "0.5.19"]
[doo "0.1.11"]
[cljsjs/prop-types "15.7.2-0"]]
:source-paths ["demo" "test" "examples/todomvc/src" "examples/simple/src" "examples/geometry/src"]
:resource-paths ["site" "target/cljsbuild/client" "target/cljsbuild/client-npm"]}}

:clean-targets ^{:protect false} [:target-path :compile-path "out"]

:repl-options {:init (do (require '[figwheel-sidecar.repl-api :refer :all]))}

:figwheel {:http-server-root "public" ;; assumes "resources"
:css-dirs ["site/public/css"]
:repl false}
:repl true
:nrepl-port 27397}

;; No profiles and merging - just manual configuration for each build type.
;; For :optimization :none ClojureScript compiler will compile all
Expand Down
35 changes: 28 additions & 7 deletions src/reagent/core.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
[reagent.impl.component :as comp]
[reagent.impl.util :as util]
[reagent.impl.batching :as batch]
[reagent.impl.protocols :as p]
[reagent.ratom :as ratom]
[reagent.debug :as deb :refer-macros [assert-some assert-component
assert-js-object assert-new-state
Expand Down Expand Up @@ -46,8 +47,8 @@
(defn as-element
"Turns a vector of Hiccup syntax into a React element. Returns form
unchanged if it is not a vector."
[form]
(tmpl/as-element form))
([form] (p/as-element tmpl/default-compiler form))
([form compiler] (p/as-element compiler form)))

(defn adapt-react-class
"Returns an adapter for a native React class, that may be used
Expand All @@ -60,9 +61,10 @@
"Returns an adapter for a Reagent component, that may be used from
React, for example in JSX. A single argument, props, is passed to
the component, converted to a map."
[c]
(assert-some c "Component")
(comp/reactify-component c))
([c] (reactify-component c tmpl/default-compiler))
([c compiler]
(assert-some c "Component")
(comp/reactify-component c compiler)))

(defn render
"Render a Reagent component into the DOM. The first argument may be
Expand Down Expand Up @@ -140,8 +142,10 @@
Object.assign to merge partial state into the current state.
React built-in static methods or properties are automatically defined as statics."
[spec]
(comp/create-class spec))
([spec]
(comp/create-class spec tmpl/default-compiler))
([spec compiler]
(comp/create-class spec compiler)))


(defn current-component
Expand Down Expand Up @@ -388,3 +392,20 @@
"Works just like clojure.core/partial, but the result can be compared with ="
[f & args]
(util/make-partial-fn f args))

(defn create-compiler
"Creates Compiler object with given `opts`,
this can be passed to `render`, `as-element` and other functions to control
how they turn the Reagent-style Hiccup into React components and elements."
[opts]
(tmpl/create-compiler opts))

(defn set-default-compiler!
"Globally sets the Compiler object used by `render`, `as-element` and other
calls by default, when no `compiler` parameter is provided.
Use `nil` value to restore the original default compiler."
[compiler]
(tmpl/set-default-compiler! (if (nil? compiler)
tmpl/default-compiler*
compiler)))
Loading

0 comments on commit 4decfa6

Please sign in to comment.