How to use re-frame with React components?
I am working on a ClojureScript project that uses re-framing. I need to create several React components and integrate them. If a component does not need access to the global state, there are no problems. But with the use of subscribe, issues arise. I am trying to export the subscribe and dispatch functions through a module in shadow-cljs. Here's how it looks:
{:client {:asset-path "/js"
:build-hooks [(client.styles.core/write-styles)]
:modules {:shared {:entries []}
:main {:init-fn client.core/main :depends-on #{:shared}}
:reframe {:init-fn client.models.reframe-export/init :depends-on #{:shared}}
:worker {:init-fn client.replicated-state.worker-processor/init :web-worker true :depends-on #{:shared}}
}
:devtools {:preloads [devtools.preload
debux.preload]}
:optimizations :none
:output-dir "public/js"
:target :browser}
As a result, a file is created in public/js, but I was only able to access the functions through the window object:
const { subscribe , dispatch } = window.client.models.reframe_export;
To use them in React components, I create hooks.
export const useReframe = (subscription) => {
const [value, setValue] = useState(null);
useEffect(() => {
const sub = subscribe(subscription);
setValue(sub.deref());
const watchKey = Symbol("watchKey");
sub.addWatch(watchKey, (_, newValue) => {
setValue(newValue);
});
return () => {
sub.removeWatch(watchKey);
};
}, [subscription]);
return value;
};
export const dispatchReframe = (event, ...args) => {
dispatch([event, ...args]);
};
dispatch works without any problems.`subscribe` provides access to the data in the global state, but the component does not re-render when the state changes. I think the problem is due to using window.
Has anyone encountered a similar task? Maybe there is another approach to integrating React with re-frame? I would appreciate any help.Yes, I tried this approach, and it does work. However, it would not be very convenient if the React component has nested components. I have to pass the subscription down through props, and I would like to avoid that.
In that case, I'd use higher-order components, where you pass around not data (or not just data), but full instances or component factories around.
Alternatively, you can wrap your React components with e.g. Helix, or maybe create them fully in Helix. https://github.com/lilactown/helix/blob/master/docs/integrating-libs.md describes how to use re-frame in that scenario. But I've never done it myself so no clue whether it's a sensible approach in your case.
I probably don't understand the case, but I think if you have a Reagent/re-frame app, then I would expect the React components to be
- in a small support role, so rather easy to integrate without any jumping through hoops,
- part of a company-wide component library / design system or so, and they likely support passing in e.g. value, onSave` , titleComponent etc. already or
- manage their own local state outside of re-frame/Reagent which is not interesting to replace.
I think if the description of the case was more concrete, there could be some more useful tips.
What you can try is write a hook that wraps re-frames subscribe function. Some companies have done this before to integrate re-frame better with functional components but unfortunately there’s no implementation publicly available
There's an attempt to implement it https://github.com/lilactown/helix/pull/53/files/d9fcd89f7fb5bd5fdcbba5c3edb7dd8d051dc1cf#diff-e8323cfd468c020ffb42c2beec64f1837b89fedacc416ff59a4fa6d1947f7a81R58 but it wasn't merged in because of apparent complexities. I would argue that if anyone wants to implement something similar, they'd have to know ins and outs of re-frame and Reagent quite well. So my suggestion to use Helix above should be paid little to no attention.
So you need to use re-frame from a React app? Yeah, just don't do that. It's not an officially supported use-case by any means. The intended usage is for Reagent to be the main driver of all React stuff. Re-frame drives the data and its changes, and React is just used for rendering.
What's the exact use-case that made you go this direction?
Just to expand a bit on my initial comment - if you actually have a Reagent project (where the main rendering is done via reagent.dom) and need to integrate a React component, it's possible. I would simply create a Reagent wrapper for each React component that needs to use re-frame.core/subscribe.
In principle. something like:
(def react-component some-js-package/Component)
(defn reagent-component []
(let [data @(rf/subscribe [:data])]
[:> react-component data]))