Fork me on GitHub
#clojurescript
<
2022-02-07
>
Mathieu Pasquet11:02:23

Hey, bit of an architecture / preference question. I'm building my first "large" frontend using re-frame and I'm wondering about error handling. I have a utility function that takes a date, computes the difference between that date and now, and returns a pretty string:

(defn match-time-str
  [sale-date]
  (let [now (dt/now)
        upcoming? (dt/after? sale-date now)
        interval (if upcoming? (dt/interval now sale-date)
                     (dt/interval sale-date now))
        days (Math/abs (dt/in-days interval))]
    (str days " day" (when (> 1 days) "s") (when upcoming? " overdue"))))
The implementation itself isn't super important, but what I'm trying to figure out is what I should do when sale-date isn't valid. Imaging I call
(match-time-str nil)
from within my component, that function is going to throw. Should I just let that happen, should I add an error boundary to the component, or perhaps check that the date is valid in the match-time-str function and console.error if it isn't but still return an empty string? Maybe I should just add spec validation to my app state and then trust that because of that, I'll never call match-time-str with a bad date? Keen to hear your thoughts.

p-himik11:02:07

The way you should handle a particular error depends on the context of that error and the things that led to it. In this case, the function is simple and by itself seems to be context-free. So I'd just throw an exception. Then the callers can figure out what to do with that exception, if anything at all. So for example, suppose the sale-date argument comes directly from a user. Than the thrown exception should result in something that tells the user that the date is incorrect. On the other hand, if sale-date is provided by the result of some e.g. HTTP request, the user won't care about the date. But they might need to know that something is wrong, and you as a developer definitely need to know that there's something going on, so some external logging should also be set up, ideally.

p-himik11:02:06

Regarding spec - spec'ing the whole app-db can be useful, but it's not a real solution. It won't help you notify users in a meaningful way, it won't help you when some workflow involves multiple events. E.g. you set an HTTP request up with one event while also marking something as "in progress", and then you receive the response, fire another event, and that event creates app-db that violates the spec. Even if you tell the user "oops, something went wrong" and rollback that event, you'll still have the result of the event that started the whole process, so there will be that "in progress" mark there. To prevent all that and to enable meaningful feedback for users, additional mechanisms are needed. Unfortunately, I can't really tell what that would be, but if you really want to make something as user-friendly as possible, I'd start with https://lucywang000.github.io/clj-statecharts/docs/integration/re-frame/ given how it supports nested states, so hypothetically maybe one could have sub-states for situations similar to the one I described above, and then the whole parent state could be treated as failed when something happens during any of the sub-states.

Mathieu Pasquet13:02:11

Thanks for the response @U2FRKM4TW! So what I'm thinking then is that my util functions should throw sensible errors given invalid inputs (this way I can test the functions and make sure they handle both proper data and throw on invalid data), but then handle that error properly within the context of the caller component, by - for example - adding a special 'error state', and perhaps logging the fact that we entered that error state as well if it's not expected. Regarding your second point around state management and invalid states, I'll give statecharts a look. Would you say it's similar to day8.re-frame/async-flow-fx ?

p-himik13:02:12

async-flow-fx is much more rudimentary and global.

maverick13:02:53

How can I view a pdf file in CLojurescript ?

Mathieu Pasquet13:02:21

@U02E91ATTLL, that really depends on how you're using clojurescript. If you're using a framework that implements react for example (like reagent), you may wish to look into a react based PDF viewer such as https://github.com/wojtekmaj/react-pdf

Mathieu Pasquet13:02:42

When asking questions like this, it's always best to provide some context around what you're doing, what tools you're using, and why you're trying to do what you're doing. Because there is rarely one 'correct' answer, and so the above info will help us all select the most appropriate answer for your use-case.

maverick13:02:30

@UTAGJEYSK I am using reagent and I am trying to open pdf inside a modal popup.

Mathieu Pasquet14:02:40

Then yeah, take a look at the react-pdf integration I linked to above. Here is some completely untested code:

(ns acme.pdfviewer
    :require [react-pdf :refer [Document Page]])

[:> Document
   {:file "somefile.pdf"}
   [:> Page {:pageNumber 1}]]
Depending on your build tools, you'll need to make sure you have react-pdf available. Are you using shadow-cljs?

maverick14:02:39

No I am not using shadow-cljs only project.clj

maverick15:02:05

So I can see react-pdf is not available in cljs packages

Mathieu Pasquet15:02:49

Not sure how to import js libs in that case. But if you can figure that out, the rest should fall into place.

rgm16:02:08

I'm getting more interested in the separation that David Nolen has been talking about, orchestrating JS components developed with Storybook's help via a thin Reagent-based wrapper. Are there any good demo repos around showing the gory setup here? I'm assuming the cljs state-managing tree consumes the JS component leaves using :bundle, implying the JS is packaged up as an NPM. I could probably figure this out OK but ... 1. did I understand all this right, and 2. does anyone have a minimal-bells-whistles toy version of this they could share? The pattern is attractive to me since it seems like a nice 80-20 thing: give up having components in hiccup in exchange for being able to farm out sizeable chunks and more easily collaborate with designers, while keeping away from React state management libs.

dnolen16:02:45

@rgm if you're going that route there's hardly anything at all to do - since the component library will just be JS

dnolen16:02:04

you can just follow JS docs, conventions etc. for that stuff

rgm16:02:28

haha that's what I was worried about

rgm16:02:06

ok no worries I'll sort it out

dnolen16:02:10

the ClojureScript bits just pulls in library and uses the components, and yes you need some bundler

p-himik16:02:36

I would love to be proven wrong but there's one downside in this approach that might or might not be relevant to your use-case. If you have reusable components that require tons of data that can change, you gonna have a bad time. React components themselves can't know about ratoms, so you will end up feeding the actual data to the components. Meaning, if you have e.g. a large table as a reusable component and change only one cell in that table, the whole table will be re-rendered.

lilactown16:02:04

it depends on the API you choose for your components. if you allow a certain amount of control from the app side you can have your tables and make it fast too

p-himik16:02:40

Could you go a bit into the weeds here?

lilactown17:02:43

if you build in a bunch of complex logic into a single large Table component you're right. but if you break it up into smaller components that you can compose together, and provide helper fns for the complex bits, then you can optimize how you build the UI for each case

lilactown17:02:35

for instance, this is the general API we have for tables in our app

($ grid/table
       ($ grid/head
          ($ grid/th {:sort true} "Col 1")
          ($ grid/th {:sort :desc} "Col 2")
          ($ grid/th {:sort :asc} "Col 3"))
       ($ grid/body
          (for [[i row] (map-indexed vector rows)]
            ($ grid/tr
               {:key i}
               (for [col row]
                 ($ grid/td {:key col} col))))))

lilactown17:02:06

this is helix syntax but you can easily use this with reagent too

lilactown17:02:35

if a cell, row, etc. needs to have some internal state, it's easy to do - pull it out into a component that returns e.g. a grid/tr

p-himik17:02:56

Right, in your case components would be small. But in the case of applications that I'm writing, the reusable components are often huge, and they are reusable in a lot of places, sometimes even nested, so I can't really remake the structure like you have every time while putting all the data in the right place. Or maybe I completely misunderstand what you mean.

p-himik17:02:49

And in your case, the whole grid/body will still be re-rendered when rows changes, no? I assume that rows contains the actual data.

lilactown17:02:36

in this simple example yes. but you could create your own row component like

(defn my-row
  [id]
  (let [data (rf/subscribe [::entity id])]
    [:> grid/tr
     (for [attr data]
       [:> grid/td attr])]))
that has internal state and would not rerender the parent

lilactown17:02:09

and that component composes with any usage of grid/body

lilactown17:02:51

the components in your JS library ought to only concern itself w/ look and feel. so building them in more atomic parts allows the application to maintain the look and feel while not losing out on performance or functionality

lilactown17:02:10

you then compose them into more opinionated components in the application

p-himik17:02:39

Ah, but then we're creating components outside of the JS world! :D So we can't use Storybook for them, at least not easily.

lilactown17:02:13

i don't really know what you mean

lilactown17:02:46

of course you aren't going to write your whole app in JS

lilactown17:02:42

storybook is, at the end of the day, a great tool for documenting a library - bad tool for writing an application. separating the two pieces is the key

lilactown17:02:17

i can't know exactly how @U050B88UR’s team works but making this separation between a "design" library which implements look and feel, using storybook for docs and dev, and then composing them in our large re-frame app, has worked well for us

p-himik17:02:22

Let me put it in different words. "Needs look and feel" is done in JS and is used with Storybook. "Complex and handles data" is done in CLJS, without Storybook. But as soon as you have "complex, handles data, and needs look and feel", you're still stuck in the CLJS world, without Storybook.

lilactown17:02:23

> But as soon as you have "complex, handles data, and needs look and feel", you're still stuck in the CLJS world, without Storybook. idgi. you put the look and feel in JS and then compose it with your data in CLJS

lilactown17:02:04

if that's a struggle, you haven't found the right way to abstract and compose your components yet

lilactown17:02:14

you can implement complex display logic in JS, but you have to think more like a library author and less like an app developer.

lilactown17:02:07

for example, react-select implements a lot of complex display logic. it also provides a lot of tools to delegate control to the app dev using it

lilactown17:02:06

there's no silver bullet- there's a cost to abstracting and creating composable components. creating really big components that are reused throughout the app is a design choice that makes sense in your context and that's good

p-himik17:02:32

> you put the look and feel in JS and then compose it with your data in CLJS The reusable part is the composition of simple components. And the composition's look and feel does depend upon the way it's built - you can't assess it from its constituents, you have to see it in full. React Select is an incredibly trivial component. Imagine you're in a plane's cockpit. It has that huge dashboard. Now imagine your app is about showing hundreds of cockpits, all with the same dashboard but in different states. Sure, a knob or a gauge are reusable, but they are not important. Dashboards are.

p-himik17:02:44

My whole point is that there are scenarios where implementing some of the reusable things in JS and putting them in Storybook does not make sense.

lilactown17:02:57

yes agreed

🙂 1
lilactown17:02:40

you started w/ what i thought was a good question - "a large table as a reusable component and change only one cell in that table, the whole table will be re-rendered." - and i feel like i'm now being asked to "prove" that there is one true way to writing apps, which there is not

lilactown17:02:58

i hope i at least answered that question

p-himik17:02:15

Oh, of course, that wasn't my intention. That sentence was in the context of "a reusable JS component suitable for Storybook", where all the data is passed directly to the component.

rgm17:02:47

wow this is helpful

gratitude 1
rgm17:02:03

also: I don't think we're in the plane cockpit end of things, re: reusable components.

rgm19:02:18

it's a bit funny to me how much of this seems to circle back to older OO patterns in a functional guise, eg the NEXTStep/Cocoa-style delegate pattern, which I still think was a pretty insightful way to have reuse and customization at the same time. Feels to me like a lot of "headless" react libs are doing this implicitly

rgm19:02:52

eg. react-select ... I keyed on the word "delegate" above and remembered that it can be a nice practical system for reusable UI bits when it's pervasive through a UI framework

rgm19:02:49

(like Greenspun's 10th Rule but for UI ... "every sufficiently complicated UI toolkit contains an ad-hoc, informally-specified, bug-ridden slow implementation of the delegate pattern").

lilactown19:02:54

the key IME is to focus on composition over delegation at first, and only go to delegation (by which i mean passing in some handler or object that adheres to an abstract API to the component) when you you don't want to decouple the state from the component

lilactown19:02:38

too many reusable components end up with dozens of props to pass in to customize the behavior, that could be solved much more elegantly by providing a few other components that the consumer can compose together to fit their required use

👍 2
lilactown19:02:57

you can still document those compositions using storybook. the example i posted above w/ the table is lifted directly from a story we have in storybook

rgm19:02:17

for sure. Delegation only really comes into play when the reusable component has complex internal state.

rgm19:02:37

or should only, anyway

lilactown19:02:11

and a lot of complex internal state doesn't have to be packaged into the component. many React libs have learned that it's actually better to reproduce those as custom hooks

lilactown19:02:09

it's IME not very useful to write hooks in JS over CLJS or use storybook w/ them, but my opinion might change in the future. most of the custom hooks we use are for things like integrating with re-frame for data fetching, auth, etc.

rgm19:02:07

yeah a lot of my motivation here is feeling somewhat left behind on the hooks stuff and my wait-and-see attitude flipped over into discomfort recently

rgm19:02:49

so you mean your hooks work tends to be in cljs

lilactown20:02:18

yeah. most of our hooks are application concerns so we write them in CLJS

lilactown20:02:07

e.g. we have hooks for fetching and subscribing to data from our API

👍 1
lilactown20:02:49

which rn uses re-frame under the hood

lilactown20:02:41

we are looking at adopting something like react-hook-form in the future and we'll have to figure out if we want that to live in our design lib or in our app code

rgm16:02:16

I do in fact have a table, but I'm thinking of dropping to react-table or similar via helix

p-himik16:02:44

How do you provide the data for react-table?

rgm16:02:18

probably server-side pagination

p-himik16:02:53

I mean the component itself, right in the code. Just as an example:

(defn my-table [data]
  ...)
Here you provide the whole data via a single argument. Not a path to the data in some storage, not a ratom - the data itself. If that's the case with your usage of react-table, then it doesn't matter what exactly you're using - the my-table function will be called when data changes. Which can lead to terrible performance if there are a lot of things. Maybe there's some magic in react-table that I simply can't fathom that would prevent that. But I have no clue how that would be possible.

rgm17:02:35

yeah, fair enough, I was going to just try to lean on cljs-bean and avoid round-tripping and hope for the best in the first instance

rgm16:02:01

anyway as I'm understanding this approach I'm also free to smooth things over by staying on the cljs side of the line where appropriate

rgm16:02:26

sounds like I have the broad conceptual understanding right so maybe I'll set up this toy version and make it available

rgm17:02:40

(my experience of JS tooling docs is that they're not that great on providing minimalist untangled examples, so it's sometimes a bit of a slog to determine what really matters in a config versus what's there because it was extracted from some project with 10 other popular libs and no one even sees the noise any more).

dnolen17:02:29

Storybook was relatively easy to get going w/ - I don't think it needs more than a couple of weeks to get a good feel for how to use it effectively

rgm17:02:18

agreed, it was pretty easy to set up create-react-app plus storybook, no complaints there.

rgm17:02:08

I'm just having a hunch that there are some suboptimal things I could do by mistake in the npm packaging

rgm17:02:04

(ironically things I'd probably be good at if I hadn't instead honed my skills in really understanding cljs, but non, je ne regrette rien)

dnolen17:02:51

@rgm we use the Node.js git deps feature to pull in the components - this is for React Native though - maybe for web stuff it's easier to develop against a local lib

dnolen17:02:14

but given that development mostly looks like - Storybook stuff first, then app stuff - this isn't really an interesting problem

rgm17:02:45

I was planning to try local lib first, yeah. My expectation is that we'll get it set up once then probably never look at it again until I get bored or am procrastinating on a bug.