Skip to content

Commit 12d95da

Browse files
committed
more thoughts
1 parent 991e2c4 commit 12d95da

File tree

1 file changed

+189
-0
lines changed

1 file changed

+189
-0
lines changed

doc/why-rewrite-components.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ Could even force this computation early if needed, just `(-> state (assoc :data
276276

277277
The DB needs something like this too still.
278278

279+
280+
279281
## Further Thoughts?
280282

281283
In some sense this is just `react` `useReducer` disguised as a component? Is that a bad thing? Should it be factored out and be separate from components altogether?
@@ -492,3 +494,190 @@ Should "hooks" push events to the components or should be components pull data f
492494
The component should already queue events anyway, but it may not be able to decide if events can be dropped? ie. if two query results arrive before the component even rendered an update. It could drop the first? So, pull seems like the better design. The implementations can always decide if it wants to return all, first, latest or some aggregate? Under normal circumstances this won't matter since we are likely to process events fast "enough". Should there be some kind of back-pressure or is that overkill?
493495

494496
Events ideally also have some kind of priority? Process high priority stuff first, while potentially doing offscreen stuff "later".
497+
498+
499+
## Execution order
500+
501+
One thing `defc` ensured was ordering of execution. Each bind would execute in order, when needed. You could ensure that one block finishes before the next one starts.
502+
503+
504+
```clojure
505+
(defc ui-thing [ident]
506+
(bind data (sg/query-ident ident))
507+
(bind result (compute-with data))
508+
(render ...))
509+
```
510+
511+
The loop thing cannot ensure this, and may become a little messy?
512+
513+
```clojure
514+
(defc ui-thing
515+
(arg ident)
516+
(compute :data [state data]
517+
(assoc state :result (compute-with data)))
518+
(on-init [state ident]
519+
(sg/query-ident ident)
520+
state)
521+
(render
522+
...))
523+
```
524+
525+
It could maybe ensure that `render` is last and `on-init` before any `compute` but the whole thing becomes very "callback" based potentially.
526+
527+
```clojure
528+
(defc ui-thing [ident arg]
529+
(bind data (sg/query-ident ident))
530+
(bind a (compute-with data))
531+
(bind b (compute-else arg))
532+
(bind c (+ a b))
533+
(render ...))
534+
```
535+
536+
This order is ensured and the code is concise. It also can only get to `render` after running through all `bind`.
537+
538+
It gets more noisy with the loop thing.
539+
540+
```clojure
541+
(defc ui-thing
542+
(arg ident)
543+
(arg arg)
544+
(on-init [state ident arg]
545+
(sg/query-ident ident {:query-id :data!})
546+
(assoc state :ident ident :arg arg))
547+
(on :data! [state result]
548+
(assoc state :data result))
549+
(on-arg-change arg [state old new]
550+
(assoc state :b (compute-else new)))
551+
(compute :data [state data]
552+
(assoc state :a (compute-with data)))
553+
(compute #{:a :b} [state old {:keys [a b]}]
554+
(assoc state :c (+ a b)))
555+
(render {:keys [c]}
556+
...))
557+
```
558+
559+
Not too bad, but very verbose compared. It can also get to render before any of the `compute` because they trigger based on `:data` which may come at any point, unless the query implicitly suspends? The hook impl suspends if a query cannot be immediately answer?
560+
561+
Could make things a little more compact by making the macro smarter?
562+
563+
```clojure
564+
(defc ui-thing
565+
;; (arg ..) not needed, because inferred from binding names in init?
566+
;; initial state created here?
567+
(on-init [ident arg]
568+
(sg/query-ident ident {:query-id :data!})
569+
{:ident ident :arg arg})
570+
571+
;; non-state maps returned are merged into state?
572+
(on :data! [state result]
573+
{:data result})
574+
(on-arg-change arg [state old new]
575+
{:b (compute-else new)})
576+
(compute :data [state data]
577+
{:a (compute-with data)})
578+
(compute #{:a :b} [state old {:keys [a b]}]
579+
{:c (+ a b)})
580+
581+
;; or flipping how compute works
582+
;; in theory the dependencies of a compute can be inferred?
583+
;; so instead declare which attr the compute will provide?
584+
(compute :c [{:keys [a b] :as state} prev-state]
585+
;; often won't need prev-state?
586+
;; prev-state represents the last return value of this compute?
587+
;; or the actual previous state it received? diff calcs are useful sometimes
588+
;; results in (assoc state :c result-of-compute)
589+
(+ a b))
590+
591+
;; compute multiple things?
592+
(compute [{:keys [a b] :as state} prev-state]
593+
{:c (+ a b) :d (- a b)})
594+
595+
(render {:keys [c]}
596+
...))
597+
```
598+
599+
The loop things definitely wins as soon as local state is needed though. Since it is threaded through everything, adding/changing/removing it is trivial.
600+
601+
It also wins for changing args/state because the old/new is provided automatically. Possible with `bind` but trickier.
602+
603+
## What if: Keep defc, but rewrite how hooks work?
604+
605+
The main issue I have with hooks is the protocol stuff. What if this is just replaced with runtime binding, which the executing code can talk to. `bind` could create a "register", as in memory storage. Code executing in the associated "block" can "claim" that register and write data to it. On write the component triggers further updates.
606+
607+
```clojure
608+
(defn query-ident [ident]
609+
(let [register (reg/claim!)
610+
;; just to only allow one claim per bind
611+
;; can't have two things trying to write a value to it
612+
613+
;; persistent storage between runs?
614+
;; maybe register is just a ref type internally to swap!/reset!?
615+
query-id (reg/use-state register :query-id #(random-uuid))]
616+
617+
;; this runs again whenever the "dependencies" change, so need to check
618+
;; if this did run before. register could maintain a counter, so we can easily
619+
;; tell the first run?
620+
(when-not (reg/cleanup-set?)
621+
(reg/on-cleanup register #(db/remove-query query-id)))
622+
623+
;; when the block re-runs replace a potentially existing query
624+
(db/query-ident query-id
625+
(fn [data]
626+
;; nice to have a specific place to write to, instead of a generic event?
627+
(reg/write! register data)))))
628+
629+
(defc ui-thing [ident arg]
630+
(bind data (query-ident ident))
631+
(render ...))
632+
```
633+
634+
This is still substantially easier to write than the IHook protocol mess.
635+
636+
But how does this handle side effects during read that should only happen once?
637+
638+
639+
```clojure
640+
(defc ui-thing [ident]
641+
(bind {:keys [summary] :as data}
642+
(sg/query-ident ident))
643+
(bind _
644+
(when-not summary
645+
(load-summary ident)))
646+
(render ...))
647+
```
648+
649+
Assuming the `load-summary` ends up eventually writing to the DB. Then the query will invalidate the first register. That'll move `summary` from `nil` to data, the second bind block will re-run, and do nothing because of the `when-not`. Conditionals are problematic with hooks, are they here?
650+
651+
```clojure
652+
(defc ui-thing [ident]
653+
(bind data
654+
(sg/query-ident ident))
655+
(bind _
656+
(when-not (:summary data)
657+
(load-summary ident)))
658+
(render ...))
659+
```
660+
661+
This will also run the second bind whenever `data` changes, which might be often?
662+
663+
```clojure
664+
(defc ui-thing [ident]
665+
(bind data
666+
(sg/query-ident ident))
667+
(hook
668+
(when-not (:summary data)
669+
(load-summary ident)))
670+
(render ...))
671+
```
672+
673+
Could keep the separate thing for something that doesn't need a register?
674+
675+
676+
## What if: Do both?
677+
678+
- Keep `defc` for "def component", with maybe rewritten hooks but otherwise the same.
679+
- Add `defsc` for "def stateful component", which always has managed local state and doesn't need refs?
680+
681+
Let time figure out which one is better?
682+
683+
Problem is that queries must work differently and therefor would need two separate `sg/query` variants which is not great? Most hooks for that matter need two variants? Too many options also may not be a good thing? Leaves users confused on what they should use.

0 commit comments

Comments
 (0)