Fork me on GitHub
#re-frame
<
2021-06-14
>
zackteo14:06:32

Hello, I'm looking to manipulate a leaflet map from within re-frame, my immediate thoughts is that the map needs to be accessible, meaning its context will be part of my re-frame db. Are there any resources I can look at to better understand how to do this? I feel it is a bit confusing because I need to be able to access the leaflet context, and it seems like the javascript way is to use a react hook https://react-leaflet.js.org/docs/core-api#useleafletcontext . Sorry that my question is very leaflet specific, not sure where and how else to ask it. I am able to display the map already - just that I need to be able to dynamically add and remove points on the map etc

zalky15:06:44

@UUSQHP535, we have found that a functional reactive approach can be very effective when dealing with mutable JS view objects like a Leaflet object: 1. Model all aspects of your Leaflet map as immutable data in your db. If you want to have multiple maps on the page, your db model has to support multiple model instances. 2. Bind your Leaflet object to the relevant dom node when the view component is created 3. Produce a subscription to your map's immutable data 4. Create a reagent reaction that updates the JS Leaflet object with the immutable data as input. There are two important restrictions: 1) your immutable data is your only input to the reaction 2) the only side-effects of the reaction are changes to your mutable JS object. Your Leaflet object effectively becomes a materialized view of your app db. The only way to make changes to your mutable Leaflet object is by transforming the immutable data in the app db. Reagent/re-frame will take care of keeping your mutable JS in sync with your app db.

zackteo05:06:22

@zalky did you use leaflet directly or react-leaflet ?

zalky12:06:23

@UUSQHP535, started with leaflet, eventually migrated to react-leaflet.

zackteo12:06:46

Oo, okay okay - I was wondering if the reverse might be needed but i guess react-leaflet is actually more than flexible

zackteo12:06:53

@zalky btw, by any chance do you have an example of this approach? If not no worries, I'll try to figure it out along the way 🙂

zalky13:06:42

@UUSQHP535, unfortunately I can't share the code, but there are not too many moving parts. Mostly using [:> Component props children ...] operators plus the update approach described above. I think as we moved to react-leaflet, we moved some of the leaflet mutations out of the update reaction, and just passed them as simple props to the react component (zoom, center, stuff like that). Maybe you've already seen this, but if you haven't, here is a very common pattern that is effectively the same thing as using an update reaction: https://github.com/day8/re-frame/blob/master/docs/Using-Stateful-JS-Components.md

zalky13:06:28

Well, it's not exactly the same: with this approach, you do have to option of make your reactive update a function of two states. Potentially with this approach you can be more efficient and refined in how you update your mutable JS object, but it can be more complicated to reason about, and depending on your implementation, you can potentially lose some nice properties, such as time travel.

zackteo13:06:39

Thanks for your help!! 😄

zalky13:06:52

No problem, good luck!

zackteo09:06:42

"Create a reagent reaction that updates the JS Leaflet object" @zalky how do I do this ? Do I need to use reagent/create-class ? And how does `Bind your Leaflet object to the relevant dom node when the view component is created" work ?

p-himik09:06:33

Reagent has reactions, a way to generate new cached state based on existing ratoms/reactions: https://github.com/reagent-project/reagent/blob/master/doc/ManagingState.md Regarding the "bind" part - attach a ref to the relevant React component, use that ref to get the DOM node and pass it to Leaflet. Reagent has some relevant and I would even say must-read examples in its repo on how to interact with React and JS components.

zackteo09:06:49

I don't really understand (isn't this what re-frame subscriptions do already?) - do I just wrap my subscription like so (reagent.ratom/make-reaction @(rf/subscribe [:results]))

p-himik09:06:33

Re-frame subscriptions are based on Reagent reactions. No need to wrap them. The only reason to wrap them is if you want to add some computation to them. But even then - it's better to have a proper subscription for it.

zackteo10:06:29

hmmm, okay, in that case reagent.ratom/make-reaction isn't what I want right? Since I have re-frame I think my issue now is that I believe I am using a subscription correctly, but it is not re-rendering the component.(leaflet map)

zackteo10:06:59

So to force my leaflet map to re-render, do I have to wrap my map component with reagent/create-class as in here? https://github.com/day8/re-frame/blob/master/docs/Using-Stateful-JS-Components.md

p-himik10:06:58

I can't answer that question because I have no idea what your current code is doing.

zackteo10:06:37

okay - maybe ill create a minimal example

p-himik10:06:19

When you use reagent/create-class, you create what's called a form-3 component. They're useful for interacting with JS components, but are not truly necessary because we also have form-2 components and reagent.core/with-let which often can be used instead.

p-himik10:06:25

Before you create a minimal example, I still urge you to go through the relevant Reagent examples. And write a simple Leaflet app without re-frame so that you aren't compounding points of failure.

p-himik10:06:51

A learning process should be gradual, not in the "let me tackle it all at once" kind of way.

zackteo10:06:51

Thanks for your help @U2FRKM4TW. I actually have been using re-frame already so I don't think im compounding points of failure per se. It's just that the repo I'm working on is private cause it is my final year capstone project with a company. I already did a simple example in pure reagent to display a geojson area on my map. Whereupon I moved it into my initial re-frame db state and it still displays. So now the step I'm taking is to be able to click a button and change update that part of the db. Which does work. But it does not update the map

p-himik10:06:53

> to be able to click a button and change update that part of the db > it does not update the map That should work in the Reagent app as well. If you haven't tried it there, then the example is too minimal. :)

zackteo10:06:59

And I'm just trying to figure out how to make the map reactive in that sense. But the jump from normal components to having to create my own stateful component seems quite big. So I'm trying to understand how to best accomplish this re-rendering of the map

p-himik10:06:49

Please go through the Reagent examples. Everything is there. This is a very popular use-case, so it has been thoroughly documented and tested.

zackteo10:06:36

Okay I'll try doing so again! Tho my past attempts have been a bit confusing. Especially because I'm not exactly sure how to apply it to leaflet instead - I'll try coding it out

p-himik10:06:43

After doing that, try creating such a Reagent example yourself, but for Leaflet and with a button that updates the state. No re-frame, only Reagent. If that works, then you're all set and it should be trivial to switch to re-frame after that. If that doesn't work, link that example here and I'll take a look.

zackteo10:06:36

Alright! Thanks for your help! Honestly reagent/clojurescript stuff can come across as pretty daunting compared to clojure. Perhaps particularly because I have some slight js/react experience but not nearly enough, and none with setting things up from scratch

p-himik10:06:49

Sure thing. Indeed. React by itself is enough for a novice to feel daunted. Adding Reagent with ClojureScript in top certainly doesn't help. :) That's why learning gradually is important.

zalky13:06:28

@UUSQHP535, I think p-himik provides some good advice. A strong understanding of Reagent will come in real handy building any kind of Re-frame app beyond simple examples. The only thing I'll add is to draw your attention to two important things about https://github.com/day8/re-frame/blob/master/docs/Using-Stateful-JS-Components.md example. 1. Notice that all the updates to the mutable javascript object (aside from the initial creation) happen in a single update fn in the :component-did-update lifecycle method. The question then is, where does this update fn get its data? The answer is you have to build out an immutable data model for your map in your re-frame app (in the example it would be single set of coordinates). Your event handlers update this immutable model in your db, and then your subscriptions ( :current-position in the example) get that immutable data to your update fn. create-react-class on its own doesn't help update your map, unless you close out the whole event handler -> immutable data -> subscription -> update fn loop. 2. Notice that the mutable JS object is isolated inside the view component gmap (atom nil). Because it is mutable state, it is tempting to try updating it directly via event/effect handlers. While this can work, you lose some important properties of your system, one of them being that mutations on your JS object produce view changes directly, and break the MVC isolation that Reagent/Re-frame try to impose. Instead, the https://github.com/day8/re-frame/blob/master/docs/Using-Stateful-JS-Components.md approach isolates your JS objects as part of the view, and makes them a function of the immutable data provided by your subscriptions. This is similar to how the stateful DOM structure is a function of the data from your subscriptions, and not something you manipulate in your event/effect handlers.

zackteo14:06:01

https://github.com/zackteo/leaflet-example I couldn't figure out how to use make-reaction or reaction for now. I put just used an atom directly and that won't re-render the map. Am not sure what my train of thought should be on how to get my leaflet-map to re-render

zackteo14:06:56

Will continue when I wake up tmr :x

p-himik16:06:28

I had to change your code a bit so it loads. Your GeoJSON is wrong.

p-himik17:06:55

Apart from that, react-leaflet is not a good wrapper - it does not respect data changes of already rendered components. To fix that, add ^{:key @state} in front of [GeoJSON ...].

p-himik17:06:10

Here's a simplified and fixed version of your code. I used a simpler GeoJSON feature just because it's easier to see with it that it works.

(ns leaflet-example.core
  (:require [reagent.core :as r]
            [reagent.dom :as d]
            ["react-leaflet" :refer [MapContainer TileLayer GeoJSON]]))

(def point [-104.99404 39.75621])
(def geojson {:type     "Feature"
              :geometry {:type        "Point"
                         :coordinates point}})

(defn leaflet-map [state]
  [:div
   [:> MapContainer
    {:center (reverse point) :zoom 11
     :style {:width "1000px" :height "1000px"}}
    [:> TileLayer {:url "//{s}.}]
    ^{:key state}
    [:> GeoJSON
     {:data (clj->js state)}]]])

(defn app []
  (r/with-let [state (r/atom geojson)
               move-left (fn [state]
                           (update-in state [:geometry :coordinates 0]
                                      (fn [x]
                                        (- x 0.01))))
               move-left! #(swap! state move-left)]
    [:div
     [:button {:on-click move-left!}
      "Move point left"]
     [leaflet-map @state]]))

(defn mount-root []
  (d/render [app] (.getElementById js/document "app")))

(defn ^:export init []
  (mount-root))

zackteo03:06:27

@U2FRKM4TW Strange, am certain my geoJSON is correct - but I do have this weird situation where the app won't "compile" until I save. But anyhow, I got it working! 🙂 https://github.com/zackteo/leaflet-example/blob/master/src/leaflet_example/core.cljs I do however have some questions, 1. ^{:key @state} , how might I know I would need to do something like this? Is there documentation that I can look at ? o: 2. I tried using reset! to change my geoJSON from a small square (geojson) on the left to a giant triangle on the right (other-geojson), I don't quite understand why the map will only re-render if I first change it to nil is this something to do with what triggers the re-render?

p-himik04:06:36

Regarding GeoJSON being incorrect - it very well may be that it were simply my own experiments that were incorrect. And during my prior experience with GeoJSON, I always used feature and feature collections as the top-level objects. But seems like the spec allows having the geometry at the top level as well, so all is good. 1. When you use the same @state in a :div and in some component, and the latter is updated but the former is not. It means that the component has some issues in its state management or it simply disallows changing at least some of its state for some reason. Providing the :key metadata (or via a regular attribute, if the component itself supports it, like all the plain HTML entities) makes sure that when that key is different, the whole component is re-rendered. One thing to note - that key is stringified when it's used. 2. I blame it on the GeoJSON component's implementation. It has nothing to do with Reagent.

zackteo04:06:58

Is the :key metadata is a HTML thing? or is that regular attribute ? Is there a link I can read up on this? :o I guess there's why in some example they wrap react-leaflet with some of their own structure https://github.com/instedd/planwise/blob/f7fde4e1d17c7aaba82357de7b8a8b1492440ec0/client/src/leaflet/core.cljs

p-himik05:06:20

It's a React thing. Despite most of the documentation mentioning lists of all sorts, it's applicable outside of lists as well. • https://reactjs.org/docs/reconciliation.htmlhttps://reactjs.org/docs/lists-and-keys.html Planwise doesn't use react-leaflet. It's the problem with react-leaflet, not with leaflet itself. There are two ways to use :key in Reagent:

^{:key something} [:div {:class ...} ...]
and
[:div {:key something, :class ...} ...]
I'm not 100% sure whether the latter works if the component doesn't handle :key explicitly. I just stick to the metadata way at all times.

zackteo06:06:31

Okay! Thanks for all your help!! 😄 Appreciate it

👍 3
zackteo14:06:06

and it is abit confusing if i should use the components in react-leaflet or go to leaflet in the map creation

p-himik15:06:56

This is pretty much a Reagent question and not a re-frame one because your main concern is not how to use app-db here but rather how to drive a JS library using CLJS data. Reagent has a few relevant examples and a documentation page about interacting with React components.

oliver15:06:04

Hi, I have written a self-contained https://energiewende-rechner.org/][Re-Frame that I would like to complement with some backend functionality.  Specifically, I'd like to render certain components of the app as preview images on the server. These images should be different for different app states. I have implemented a basic backend in https://macchiato-framework.github.io , which seems to be the ideal choice. So far it works nicely: I can initalize the app on every request, load a state and then render HTML/SVG via `reagent.dom.server/render-to-string`. Here's what I'm uncomfortable with: AFAIK the Re-frame DB is always global and would be shared among multliple requests-handlings – I expect problems as soon as I get multiple, concurrent requests, where request A needs to render a component in state α while Request B must render it in state β. Is there a way to use separate db atoms for every request or any other way to ensure a certain state when invoking `reagent.dom.server/render-to-string`?

p-himik15:06:37

Regarding render-to-string - why not just use [:img ...] and serve its content via a URL? Regarding state - no, but you can provide a separate key to each event handler that creates a request. Should go well with the built-in path interceptor.

oliver15:06:47

Thanks for taking the time! 1. The handlers are actually supposed to generate the images. I want to render an svg-component with a certain state an save it into a static png. That png will then simply be served by nginx/apache 2. If I understand correctly your suggestion rests on the premise that the handlers are queried from my frontend code. I doubt this is possible, because the links to the endpoint have to be part of the static html (og:image meta-tag Social Media previews)

oliver15:06:06

If I'm wrong about 2, I'd be glad if you could clarify,

p-himik16:06:11

(reg-event-fx :make-request
  (fn [db [_ key & params]]
    {:http-xhrio {...
                  :on-success [:on-success key]
                  :on-failure [:on-failure key]}}))

(reg-event-db :on-success
  (fn [db [_ key result]]
    (assoc db key result)))

(reg-event-db :on-failure
  (fn [db [_ key error]]
    ...))
Does it make it clearer what I meant?

p-himik16:06:28

The crux is the key part.

p-himik16:06:02

> the links to the endpoint have to be part of the static html I don't see how it would prevent anything from doing anything. I don't see how it's relevant to app-db, but you can get the value of any <meta> tag in runtime.

oliver16:06:54

I can (i.e. my frontend app can), but Facebook and Twitter may not…

oliver16:06:48

Thanks again, for wrapping your head around this…to me it seems, that you intend your snippet to be frontend code. However, what I'd like is for any consumer (custom PHP, Facebook, Twitter, Google…) to send some app state (be it as JSON or a query param) to my endpoint and get as a response a screenshot of my app for that state).

p-himik16:06:09

You have asked a question about re-frame, that's exactly why I'm indeed talking about the frontend part, given that re-frame is a frontend library. It can be used on backend, but it was not designed for that, IIRC. > However, what I'd like is for any consumer (custom PHP, Facebook, Twitter, Google…) to send some app state (be it as JSON or a query param) to my endpoint and get as a response a screenshot of my app for that state). So what does it have to do with re-frame?

oliver16:06:13

(I actually have that functionality working with a brute force approach: fire up a headless browser navigate to a local copy of the app, render, save a screenshot and serve that. But this is very heavy handed if I can find away to reuse my code on the server side.)

p-himik16:06:02

OK, I think I get what you're doing now. app-db is global for an app instance. It's not shared between instances. You can simply open a new headless browser tab or window - it'll have its own app-db.

p-himik16:06:10

I would definitely not use proper SSR for that. At least, not with re-frame.

oliver16:06:08

Ok, that's an answer I can live with… it was just that it's already working… just that concurrency will probably become an issue as soon as I get more than one request at the same time.

oliver16:06:43

Thanks again for your assessment… I'm not sure I'll give up just yet, but I see that this is not a solved problem (rendering inside a headless browser on every request still seems too brute-force to me if I can avoid it;)

p-himik17:06:59

FWIW, a headless browser is a much more reliable solution, given that your UI can potentially have custom JS and CSS.

oliver18:06:26

With that I agree… there's four reasons why I's like to at least try the SSR approach: 1. Performance 2. easier content tweaking (Hiccup/svg) 3. Works in shared hosting (where I cannot start a headless browser) 4. Sheer fun and elegance… For now, I'll see how this works out… and hell, maybe I'll learn something about how Re-Frame/Reagent work under the hood and find a way to properly solve this.

👍 2
rberger02:06:25

@UMWM02TED The need to render a static view for social media crawlers is a real pain in the butt when doing a SPA. As you discovered, there is no way to have the meta tags be generated on the page by javascript, the crawlers won’t run the javascript. It requires some form of SSR at least for requests from the crawlers. Its even more painful if the SPA is being served up from something like AWS Cloudfront. I spent a week iterating trying to make the SPA work with social links until I learnt that its not possible. Ended up doing a rather painful implementation with lambda@edge that detects if the request is from one of the social crawler bots and effectively SSR renders a custom page with just the header / meta tags and the minimum content (images primarily). Hopefully a blog post someday. Since you are using nginx/apache, you can just have the routes in there look at the user-agent for crawlers and send it to the service route to generate the special page, which can be simple hiccup like https://gist.github.com/rberger/b2b484e6a4af8b029e8f2d776b31bbb2 And for non crawler routes just go to your normal path to server your SPA.

oliver07:06:50

Good summary of the problem… and interesting ideas. The project at hand being a hobby I'm under no pressure to make this work; just found it to be a good occasion to take a first stab at SSR. As I've said: I've already gotten this to work: 1. Endpoint receives a request containing app state (as b64 query-param) 2. Set Re-frame to that state (via multiple dispatches) 3. Render SVG-component via reagent.dom.server/render-to-string 4. Convert SVG to PNG and save to disk 5. Redirect to the static PNG (with 302) – works with crawlers. Steps 2–4 are skipped if an image for that particular state already exists. My only remaining worry is about concurret requests: What if another request comes in during 2. Could the DB-Atom be reset by handler B dipatching against it while handler A is still building the state it wants to render. Whith a global DB atom I smell trouble, but this is hard to test – maybe there would be no issue. With my current project there wouldn't be, since I get a couple of hunded reuqests per day at best – but in principle? If I ran this on an actual server rather than a shared hosting limited to a single node runtime I could just add another node build, that starts no server at all but only takes the encoded state and spits the PNG to disk. The handlers would then just shell out, run that node script and then redirect to the resulting PNG. This would separate the Re-frame instances – but, again, I'd like to find something more elegant, even if it's less brutal than firing up Puppeteer all the time.

p-himik10:06:40

Re-frame acts on a queue - no handlers are run in threads. So no handlers can run in parallel. However, workflows can run concurrently if they consist of multiple events - when handling an event results in dispatching another event via some means.

ribelo17:06:48

I once read somewhere not to do a something like `:get-in`  subscription, but I can no longer find this information or what risks it involves

ribelo17:06:12

I'd like to use doxa with re-frame, but I'm not happy about writing all the subscriptions by hand and would like to just use the pull syntax, but I suspect this is analogous to :get-in subscription

p-himik17:06:12

A generic get-in sub would make views know about the app-db structure. Some common pull functionality is not necessarily that. I consider it to be fine, as long as it has a very specific and limited scope.

ribelo18:06:57

thanks, sounds reasonable

ribelo18:06:50

and that was the text I was looking for