Helix has a number of features that are actively being developed and experimented with, in production. The way these features are exposed is through feature flags that can be enabled on a per-component basis.
The defnc
macro can optionally take an map of options after the parameter list
just like defn
. In that map, passing a map of feature flags to the
:helix/features
allows you to enable/disable certain features.
(defnc my-component [props]
{:helix/features {:some-flag true}}
...)
Here is the current list of experimental features and flags associated with them.
This feature creates the component and then defines a factory function as the given name that returns a React element of the component type.
This means that you can now elide the $
macro call and simply call the factory
like a function to construct an element.
Example:
(defnc my-component [props]
{:helix/features {:define-factory true}
,,,)
;; Wrong!
($ my-component {:foo "bar"})
;; Right!
(my-component {:foo "bar"})
;; => #js {:type my-component :props #js {"helix/props" {:foo "bar"}}}
Everything else works exactly the same. Your component will receive {:foo "bar"}
as props when rendered.
If you do end up needing to access the actual component type generated by defnc
,
you can use helix.core/type
on the factory function.
($ (helix.core/type my-component) {:foo "bar"})
This feature analyzes the body of a component defined with defnc
, doing a best
effort to detect and throw a warning when hooks are used within a conditional or
loop.
This experimental feature can be enabled with the :check-invalid-hooks-usage
flag.
(defnc my-component [props]
{:helix/features {:check-invalid-hooks-usage true}}
(when (:foo props)
(hooks/use-effect :once (do-foo (:foo props))))
...)
;; Compile error! Invalid hooks usage `(hooks/use-effect :once (do-foo (:foo props)))`
React Fast Refresh is an experimental React feature that enables hot reloading of React components while preserving the state of components that use Hooks.
It's a first-class feature in React that allows you to reap the developer time benefits of using a global store (hot reloading without losing state) while still using local state in your components.
The feature works by analyzing the body of your component defined with defnc
and creating a signature based on the hooks used. When you change the hooks
used in the component, it will re-initialize the state; otherwise, it will
preserve the state.
To enable this feature, you will need to do three things. First, we'll need to install the correct version of the react-refresh package. Then, we'll need to add a hook to run after our code has been reloaded to trigger a React refresh. Finally, we'll enable the fast-refresh feature flag in our components.
Please note: React Developer Tools have to be installed.
For an example of how this all fits together see the helix-todo-mvc project.
The suggested way to trigger a refresh is by adding an "after-load" hook in a preload. Helix provides the functions to run in the preload, but not the preload itself.
In your project, create a src/my_project/dev.cljs
file. The name is not
important, but it will need to be consistent with the namespace we put it in and
on the classpath, just like any other ClojureScript source file.
shadow-cljs-specific setup
The contents will need to look something like this:
(ns my-project.dev
"A place to add preloads for developer tools!"
(:require
[helix.experimental.refresh :as r]))
;; inject-hook! needs to run on application start.
;; For ease, we run it at the top level.
;; This function adds the react-refresh runtime to the page
(r/inject-hook!)
;; shadow-cljs allows us to annotate a function name with `:dev/after-load`
;; to signal that it should be run after any code reload. We call the `refresh!`
;; function, which will tell react to refresh any components which have a
;; signature created by turning on the `:fast-refresh` feature flag.
(defn ^:dev/after-load refresh []
(r/refresh!))
We'll need to add this file to the :preloads
config in our project's
shadow-cljs.edn
file. Shadow-cljs also has an optional devtool configuration
that will reload all namespaces in a dependency chain. If we notice that some
changes to our app do not trigger a refresh, we can enable it to ensure that
when we change a namespace, all downstream dependents get the latest code:
{,,,
:builds
{:app {,,,
:devtools {
:reload-strategy :full
:preloads [my-project.dev]}}}}
figwheel-main-specific setup
The contents will need to look something like this:
;; figwheel-main allows us to annotate a namespace with `:fighweel-hooks`
;; to configure reload callbacks at runtime.
(ns ^:figwheel-hooks my-project.dev
"A place to add preloads for developer tools!"
(:require
[helix.experimental.refresh :as r]))
;; inject-hook! needs to run on application start.
;; For ease, we run it at the top level.
;; This function adds the react-refresh runtime to the page
(r/inject-hook!)
;; figwheel-main allows us to annotate a function name with `:after-load`
;; to signal that it should be run after any code reload. We call the `refresh!`
;; function, which will tell react to refresh any components which have a
;; signature created by turning on the `:fast-refresh` feature flag.
(defn ^:after-load refresh []
(r/refresh!))
We'll need to add this file to the :preloads
config in our project's
[build-name].cljs.edn
file:
{,,,
:preloads [my-project.dev]}
In your components, enable the :fast-refresh
flag.
(defnc my-component [props]
{:helix/features {:fast-refresh true}}
...)
If you want to do this for all components, it's suggested that you create a custom macro that you can use throughout your project without needing to add all feature flags by hand.
When the :metadata-optimizations
key is set to true
, metadata such as
^:memo
and ^:callback
may be used to optimize your components. The former,
for instance, may be used as follows.
(defnc example [{:keys [foo]}]
{:helix/features {:metadata-optimizations true}}
(let [foobar ^:memo [foo "bar"]
first-foobar (hooks/use-ref foobar)
[count set-count] (hooks/use-reducer inc 0)]
(d/div
(d/p (str (identical? foobar @first-foobar)))
(d/p "Count: " count)
(d/button {:on-click #(set-count inc)} "Re-render"))))
Then e.g. ($ example {:foo 123})
will render a component with a memoized
foobar
vector. Clicking the "Re-render" button will not cause foobar
to
change, which is evidenced by the (identical? foobar @first-foobar)
form
always evaluating to true
despite re-renders being triggered.