Skip to content

Commit 4decfa6

Browse files
authored
Merge pull request #477 from reagent-project/feature/functional-components
Test creating functional components
2 parents 8f2d37f + 14cbfea commit 4decfa6

File tree

26 files changed

+2243
-1597
lines changed

26 files changed

+2243
-1597
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44

55
### Features and changes
66

7+
- **Option to render Reagent components as React functional components instead of
8+
class components**
9+
- To ensure backwards compatibility by default, Reagent works as previously and
10+
by default creates class components.
11+
- New Compiler object can be created and passed to functions to control
12+
how Reagent converts Hiccup-style markup to React components and classes:
13+
`(r/create-compiler {:functional-components? true})`
14+
- Passing this options to `render`, `as-element` and other calls will control how
15+
that markup tree will be converted.
16+
- `(r/set-default-compiler! compiler)` call can be used to set the default
17+
compiler object for all calls.
18+
- [Read more](./doc/reagent-compiler.md)
19+
- [Check example](./example/functional-components-and-hooks/src/example/core.cljs)
720
- Change RAtom (all types) print format to be readable using ClojureScript reader,
821
similar to normal Atom ([#439](https://github.com/reagent-project/reagent/issues/439))
922
- Old print output: `#<Atom: 0>`

codecov.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
coverage:
2+
 status:
3+
 patch:
4+
 default:
5+
 enabled: no

doc/ReactFeatures.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,34 @@ after error, it can be also used to update RAtom as in Reagent the Ratom is avai
125125
in function closure even for static methods. `ComponentDidCatch` can be used
126126
for side-effects, like logging the error.
127127

128+
## [Function components](https://reactjs.org/docs/components-and-props.html#function-and-class-components)
129+
130+
JavaScript functions are valid React components, but Reagent implementation
131+
by default turns the ClojureScript functions referred in Hiccup-vectors to
132+
Class components.
133+
134+
However, some React features, like Hooks, only work with Functional components.
135+
There are several ways to use functions as components with Reagent:
136+
137+
Calling `r/create-element` directly with a ClojureScript function doesn't
138+
wrap the component in any Reagent wrappers, and will create functional components.
139+
In this case you need to use `r/as-element` inside the function to convert
140+
Hiccup-style markup to elements, or just returns React Elements yourself.
141+
You also can't use Ratoms here, as Ratom implementation requires the component
142+
is wrapped by Reagent.
143+
144+
Using `adapt-react-class` or `:>` is also calls `create-element`, but that
145+
also does automatic conversion of ClojureScript parameters to JS objects,
146+
which isn't usually desired if the component is ClojureScript function.
147+
148+
New way is to configure Reagent Hiccup-compiler to create functional components:
149+
[Read Compiler documentation](./ReagentCompiler.md)
150+
128151
## [Hooks](https://reactjs.org/docs/hooks-intro.html)
129152

153+
NOTE: This section still refers to workaround using Hooks inside
154+
class components, read the previous section to create functional components.
155+
130156
Hooks can't be used inside class components, and Reagent implementation creates
131157
a class component from every function (i.e. Reagent component).
132158

@@ -184,16 +210,16 @@ If the parent Component awaits classes with some custom methods or properties, y
184210
(r/create-class
185211
{:get-input-node (fn [this] ...)
186212
:reagent-render (fn [] [:input ...])})))
187-
213+
188214
[:> SomeComponent
189215
{:editor-component editor}]
190-
216+
191217
;; Often incorrect way
192218
(defn editor [parameter]
193219
(r/create-class
194220
{:get-input-node (fn [this] ...)
195221
:reagent-render (fn [] [:input ...])})))
196-
222+
197223
[:> SomeComponent
198224
{:editor-component (r/reactify-component editor)}]
199225
```

doc/ReagentCompiler.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Reagent Compiler
2+
3+
Reagent Compiler object is a new way to configure how Reagent
4+
turns the Hiccup-style markup into React components and elements.
5+
6+
As a first step, this can be used to turn on option to create
7+
functional components when a function is referred in a Hiccup vector:
8+
`[component-fn parameters]`.
9+
10+
[./ReactFeatures.md#hooks](React more about Hooks)
11+
12+
```cljs
13+
(def functional-compiler (r/create-compiler {:functional-components? true}))
14+
15+
;; Using the option
16+
(r/render [main] div functional-compiler)
17+
(r/as-element [main] functional-compiler)
18+
;; Setting compiler as the default
19+
(r/set-default-compiler! functional-compiler)
20+
```
21+
22+
## Reasoning
23+
24+
Now that this mechanism to control how Reagent compiles Hiccup-style markup
25+
to React calls is in place, it will be probably used later to control
26+
some other things also:
27+
28+
From [Clojurist Together announcenment](https://www.clojuriststogether.org/news/q1-2020-funding-announcement/):
29+
30+
> As this [hooks] affects how Reagent turns Hiccup to React elements and components, I
31+
> have some ideas on allowing users configure the Reagent Hiccup compiler,
32+
> similar to what [Hicada](https://github.com/rauhs/hicada) does. This would also allow introducing optional
33+
> features which would break existing Reagent code, by making users opt-in to
34+
> these. One case would be to make React component interop simpler.
35+
36+
Some ideas:
37+
38+
- Providing options to control how component parameters are converted to JS
39+
objects (or even disable automatic conversion)
40+
- Implement support for custom tags (if you can provide your own function
41+
to create element from a keyword, this will be easy)
42+
43+
Open questions:
44+
45+
- Will this cause problems for libraries? Do the libraries have to start
46+
calling `as-element` with their own Compiler to ensure compatibility.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Reagent example app
2+
3+
Run "`lein figwheel`" in a terminal to compile the app, and then open http://localhost:3449
4+
5+
Any changes to ClojureScript source files (in `src`) will be reflected in the running page immediately (while "`lein figwheel`" is running).
6+
7+
Run "`lein clean; lein with-profile prod compile`" to compile an optimized version.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
(defproject functional-components-and-hooks "0.6.0"
2+
:dependencies [[org.clojure/clojure "1.10.1"]
3+
[org.clojure/clojurescript "1.10.597"]
4+
[reagent "1.0.0-SNAPSHOT"]
5+
[figwheel "0.5.19"]]
6+
7+
:plugins [[lein-cljsbuild "1.1.7"]
8+
[lein-figwheel "0.5.19"]]
9+
10+
:resource-paths ["resources" "target"]
11+
:clean-targets ^{:protect false} [:target-path]
12+
13+
:profiles {:dev {:cljsbuild
14+
{:builds {:client
15+
{:figwheel {:on-jsload "example.core/run"}
16+
:compiler {:main "example.core"
17+
:optimizations :none}}}}}
18+
19+
:prod {:cljsbuild
20+
{:builds {:client
21+
{:compiler {:optimizations :advanced
22+
:elide-asserts true
23+
:pretty-print false}}}}}}
24+
25+
:figwheel {:repl false
26+
:http-server-root "public"}
27+
28+
:cljsbuild {:builds {:client
29+
{:source-paths ["src"]
30+
:compiler {:output-dir "target/public/client"
31+
:asset-path "client"
32+
:output-to "target/public/client.js"}}}})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
div, h1, input {
3+
font-family: HelveticaNeue, Helvetica;
4+
color: #777;
5+
}
6+
7+
.example-clock {
8+
font-size: 128px;
9+
line-height: 1.2em;
10+
font-family: HelveticaNeue-UltraLight, Helvetica;
11+
}
12+
13+
@media (max-width: 768px) {
14+
.example-clock {
15+
font-size: 64px;
16+
}
17+
}
18+
19+
.color-input, .color-input input {
20+
font-size: 24px;
21+
line-height: 1.5em;
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Example</title>
6+
<link rel="stylesheet" href="example.css">
7+
</head>
8+
<body>
9+
<div id="app">
10+
<h1>Reagent example app – see README.md</h1>
11+
</div>
12+
<script src="client.js"></script>
13+
</body>
14+
</html>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
(ns example.core
2+
(:require [reagent.core :as r]
3+
[reagent.dom :as rdom]
4+
[clojure.string :as str]
5+
["react" :as react]))
6+
7+
;; Same as simpleexample, but uses Hooks instead of Ratoms
8+
9+
(defn greeting [message]
10+
[:h1 message])
11+
12+
(defn clock [time-color]
13+
(let [[timer update-time] (react/useState (js/Date.))
14+
time-str (-> timer .toTimeString (str/split " ") first)]
15+
(react/useEffect
16+
(fn []
17+
(let [i (js/setInterval #(update-time (js/Date.)) 1000)]
18+
(fn []
19+
(js/clearInterval i)))))
20+
[:div.example-clock
21+
{:style {:color time-color}}
22+
time-str]))
23+
24+
(defn color-input [time-color update-time-color]
25+
[:div.color-input
26+
"Time color: "
27+
[:input {:type "text"
28+
:value time-color
29+
:on-change #(update-time-color (-> % .-target .-value))}]])
30+
31+
(defn simple-example []
32+
(let [[time-color update-time-color] (react/useState "#f34")]
33+
[:div
34+
[greeting "Hello world, it is now"]
35+
[clock time-color]
36+
[color-input time-color update-time-color]]))
37+
38+
(def functional-compiler (r/create-compiler {:functional-components? true}))
39+
40+
(defn run []
41+
(rdom/render [simple-example] (js/document.getElementById "app") functional-compiler))
42+
43+
(run)

project.clj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(defproject reagent "0.10.0"
1+
(defproject reagent "1.0.0-SNAPSHOT"
22
:url "http://github.com/reagent-project/reagent"
33
:license {:name "MIT"}
44
:description "A simple ClojureScript interface to React"
@@ -25,16 +25,20 @@
2525

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

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

36+
:repl-options {:init (do (require '[figwheel-sidecar.repl-api :refer :all]))}
37+
3538
:figwheel {:http-server-root "public" ;; assumes "resources"
3639
:css-dirs ["site/public/css"]
37-
:repl false}
40+
:repl true
41+
:nrepl-port 27397}
3842

3943
;; No profiles and merging - just manual configuration for each build type.
4044
;; For :optimization :none ClojureScript compiler will compile all

0 commit comments

Comments
 (0)