Skip to content

Latest commit

 

History

History
217 lines (163 loc) · 7.34 KB

experiments.md

File metadata and controls

217 lines (163 loc) · 7.34 KB

Experiments

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.

Define as factory function

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"})

Detect invalid hooks usage

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)))`

Fast refresh

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.

Triggering a refresh

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]}

Enabling the fast-refresh flag

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.

Metadata Optimizations

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.