Comment by suis_siva

16 days ago

One of the talks I really enjoyed is https://www.youtube.com/watch?v=h9SDuTSy7ps. In my experience, React's architecture is really good and lends itself quite well to making large applications.

Unfortunately, React's biggest problem is that it forces you into the JS/TS ecosystem, which is, without a doubt in my mind, a compilation target rather than a system I wish to interact with natively.

I'm happy with Elm -- the community is really small, and sometimes you have to roll your own libraries. TEA is sometimes... unnatural (coming from React), but the fact that you do not have to worry about implicit and unexpected state (see useEffect), I always get excited to work with Elm.

Additionally, Claude seems to manage itself better in Elm than in React, at least within large, scary codebases.

Elm is essentially dead in my experience. I'd rather stick with React and TypeScript with libraries that continue to work. And there have been attempts at making TypeScript natively compilable too.

  • I wouldn't call Elm dead. Development has frozen, this is true, but X11's development was frozen and it still powered so much for decades. This is actually super common in the Clojure community -- many libraries look dead, but they're just feature-complete and FP is usually quite bug-free, when implemented correctly.

    Totally recognize and agree with the lack of libraries -- that's really Elm's weakest side.

I like TEA but don't fully grasp how it scales for apps that may have reusable components or sufficiently complex pages. Is there an agreed-upon way(s) to deal with this? I know state is a big NO so it seems a bit at odds, but also does this essentially mean all Elm apps are just a global Redux and React app with no effects? Curious about more details to what you enjoy and how you like to work in Elm. Links also perfectly fine too.

  • Not the person you were asking, but I shall share my experienc..

    >how it scales for apps that may have reusable components or sufficiently complex

    It is a lot of boilerplate, but it is mechanical, straightforward changes. I think it is entirely possible to automate it (not even using LLM).

    > state..

    I have used ELM ports to interact with JS and localstorage/indexeddb.

  • In my experience, the biggest problem in Elm is how unintuitive TEA's docs are. In some ways, Elm really feels like Haskell -- you want to avoid creating "stateful" modules and you prefer stateless modules.

    For example, https://harmont.dev's app (landing page is not Elm) has a component to render a pipeline graph called DagGraph. In React, you'd do something like

      function DagGraph() {
        const steps = useState<Jobs[]>([]);
        const selectedJobIdx = useState<number | null>(null);
        return ...;
      }
    

    In elm, I lift this up (I will keep using React syntax for the sake of wider-audience readability)

      function DagGraph({ steps, selectedJobIdx } : IDagGraphModel) {
        return ...;
      }
    

    This allows for better testability of the graph view. The parent then injects the model

      function PipelinePage({ graphMode, selectedPipeline }: IPipelineModel) {
        return (
          graphMode === GraphMode.DagView
            ? <DagGraph steps={selectedPipeline.steps} selectedJobIdx={selectedPipeline.selectedStep} />
            : <TableView steps={selectedPipeline.steps} selectedJobIdx={selectedPipeline.selectedStep} />
         );
      }
    

    Now -- we need to bubble up the selected pipeline index based on when the user clicks a node in the DAG view. I personally prefer to do this completely orthogonally to the view rendering pipeline (which remains mostly pure). I create a new model, and define the message relationships to it:

      namespace PipelineState {
        interface Model {
          steps: Step[];
          selectedStep: number | null;
        };
    
        interface ISelectStepMessage { stepIdx: number };
        interface IUnselectStepMessage;
    
        type Message = ISelectStepMessage | IUnselectStepMessage;
    
        function update(m: Model, msg: Message): Model { return...; }
      }
    
      // main model
      interface Model {
        pipeline: PipelineState.Model,
      }
    
      type Message = PipelineState.Message | SomeOtherMessage | ...;
    
      function update(m: Model, msg: Message): Model {
        switch (msg) {
          case PipelineState.Message as msg:
            return update(m, msg);
          ...
        }
      }
    

    And this seems to work quite well. Personally, I really like this architecture, because it enables me to think about state and UI separately. One of the really big gripes I have with React is how most components end up with `useEffect` and `useState`, and then immediately become untestable. Elm literally doesn't allow for that.

    At the end of the day, there's a trade-off -- my logic is no longer localized to the "component" relevant to rendering the pipeline DAG. I never have one file open editing Elm code, but I'm a vim user so it doesn't bother me. In some ways, semantically, I think it makes sense to split the "state backend" of your UI from your UI, in my opinion.

    Most of my app states are represented as state machines (coming from embedded this is quite natural) and UI is represented as pure transformations of that state.

    With Elm, it really feels like you reason about state and UI separately, and I actually prefer that, both for testability and that's just how my brain works.