React: New HMR with ClojureScript

Created on 15 Jun 2019  Â·  11Comments  Â·  Source: facebook/react

Creating this issue to get guidance on how to experiment with the new Hot Reloading functionality in React with ClojureScript.

In ClojureScript, we typically try to avoid webpack and other modern JS bundlers, and instead leverage a homegrown ClojureScript -> JS compiler, which outputs Google Closure-compatible JS. The code is then passed through the Google Closure Compiler to do advanced optimizations and bundling. We have also built some plumbing to interop with most NPM libs assuming they use fairly standard CJS or ESM.

The basic idea I will be experimenting with is outputting the necessary calls to React’s hot loading mechanism via a normal CLJS macro, analyzing the body of a function component for any useX calls at compile-time.

Based on what @gaearon told me on twitter, it sounds like I will currently need to build from master. I think that the easiest way for me to consume this in a CLJS project would be to build React in a way such that I can consume it just like an NPM module, either via npm link or installing it directly in node_modules, to avoid having to faff about too much with Google Closure and React compatibility / externs / etc. Including it directly on the page as a JS file would be the next best thing, it will just require some more work on my end to ensure it works the same as when a user installs it via NPM.

Looking forward to testing things out. If it’s helpful, I will report back here any issues I run into. Thanks!

Stale

All 11 comments

The easiest way to start would be:

  1. Build React. This means clone the repo, yarn, then yarn build react/index react-dom scheduler react-refresh.
  2. Take packages from build/node_modules and plug them into your project somehow.
  3. This integration test can serve as inspiration for what's necessary to get it running. Basically you'd want to emit calls to ReactFreshRuntime.register for everything that looks component-like, and ReactFreshRuntime.setSignature and ReactFreshRuntime.collectCustomHooksForSignature for functions that call Hooks.
  4. We use Babel plugin to emit those calls. Here's example inputs and outputs. Note we emit calls to higher-level functions called __register__ and __signature__. The integration test in step (3) defines those in terms of ReactFreshRuntime.

Hope that helps with first steps, happy to answer more questions. I should also be able to share a more complete demo soon.

I was able to successfully build React from master, and I moved the packages in build/node_modules directly into my project's node_modules folder. The only issue I ran into was:

The required JS dependency "prop-types/checkPropTypes" is not available, it was required by "node_modules/react/cjs/react.development.js".

Searched in:/Users/will/Code/react-hmr-cljs/node_modules

You probably need to run:
  npm install prop-types/checkPropTypes

Which was resolved by installing the prop-types package via npm.

Working on the macro now, will report back when I have a working example or if I get stuck.

So far, I have implemented a macro that registers a function and assigns a signature to it based on the code I read in the integration test:

https://github.com/Lokeh/react-hmr-cljs/blob/master/src/react_hmr/fresh.clj#L32-L43

Is the macro, it basically:

  1. Defines a new multi-arity "signature" function for the component
  2. Inserts a 0-arity call to it inside the component's body; I believe this is to support reloading custom hooks defined outside the file?
  3. Finds all useX or use-x function calls in the body, and calls the signature function with the component and a string comprised of all the Hooks calls.
  4. Registers the component with the component and the component name.

Currently, I am _not_ seeing my state persisted after doing a hot-reload of the file my component is defined in (by e.g. deleting a new line) with my CLJS toolchain.

I see in the integration tests a call ReactFreshRuntime.prepareUpdate(): how does this fit in to the strategy being used? Must I call this on each reload in order to persist the state?

Is there any other way I can introspect why my state is not persisted?

Thanks!

In fact it looks like there's something going on with scheduleHotUpdate where it captures the lastRoot and uses that to patch in the hotUpdate returned by ReactFreshRuntime.prepareUpdate().

It looks like both scheduleHotUpdate and lastRoot are being injected via some global devtools hooks? Not sure how I should be integrating with it.

Decided to just straight up copy what was done in the integration test and it seems to be working for simple cases!

For reference, is this the correct way to set __REACT_DEVTOOLS_GLOBAL_HOOK__?

(gobj/set js/window "__REACT_DEVTOOLS_GLOBAL_HOOK__"
          #js {
               ;; supportsFiber: true,
               :supportsFiber true
               ;; inject: injected => {
               ;;                      scheduleHotUpdate = injected.scheduleHotUpdate;
               ;;                      },
               :inject (fn [injected]
                         (set! scheduleHotUpdate (.-scheduleHotUpdate ^js injected)))

               ;; onCommitFiberRoot: (id, root) => {
               ;;                                   lastRoot = root;
               ;;                                   },
               :onCommitFiberRoot (fn [id root] (reset! last-root root))
               ;; onCommitFiberUnmount: () => {}, 
               :onCommitFiberUnmount (fn [])
               })

I'm not sure how/if I should act as if other programs are mutating the __REACT_DEVTOOLS_GLOBAL_HOOK__ object, e.g. React DevTools.

I’m going to add a thing to the runtime that does this because it’s not 100% straightforward. You want to define the hook if it doesn’t exist — but monkeypatch an existing one if it does.

Gotcha, that makes sense.

I have a question about custom hooks: I’m not sure if the ClojureScript analyzer gives me enough information to 100% know that a Hook comes from React vs. a 3rd party custom hook.

I could encode some assumptions (e.g. that the React module is imported via the symbol react, but that would be brittle), or I could dispatch based on the name (but wouldn’t work if someone defined their own useState etc.).

I’m wondering if there’s any downside to putting _all_ hooks (even React’s fundamental ones) into the getCustomHooks function? That way I wouldn’t have to discern between the two kinds.

No downside to putting all of them there. In the Babel plugin I've opted to exclude them by name as I don't expect people to define their own useState in practice.

I'm revisiting this after several months. I've implemented based on the latest API in React 16.11 in an alpha branch of my React wrapper for CLJS, hx.

Three things I'm still thinking about:

  1. What's the case when we need to register a signature for a custom hook?

  2. I'm trying to figure out a good way to determine whether a namespace is "safe" to refresh vs. call React.render (or root.render) again.

  3. What's the general strategy that webpack/parcel/etc. are using for recovering from runtime errors within a render? My current implementation loses it's previous state after fixing the error.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution.

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!

Was this page helpful?
0 / 5 - 0 ratings