Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

From the article:

> Building UIs by composing components and passing data in a unidirectional way is a paradigm shift in the frontend world. It’s a huge improvement in the way we reason about UI and it’s very hard to go back to imperative DOM manipulation once you get used to this.

IMO, UI structure and state management of the UI are orthogonal concerns. Trying to make these part of the same problem - a 'Component' - is how you wind up in the Angular-style trap where you can never quite cover 100% of the use cases, so you have to keep adding new leaky abstractions to compensate for prior ones. Passing events back up the chain is a particularly egregious constraint when you are trying to do things with the UI that the modeling does not agree with on paper (i.e. modals, status bars and nav menus are children of whom? One big global component?).

I have made this realization recently when working with Blazor. The statefulness of a UI is a completely different thing from the visual presentation of the UI. We do one-way binding into UI (e.g. foreach over collections to build table rows). But, stateful concerns are injected as scoped services (i.e. per user session/request). UI events are passed into the state service methods directly, and the UI is redrawn on top of the updated state. Each state service can only be mutated by way of one of its public methods.

This approach allows for us to build & test our UI state machines to deterministic levels of correctness without a single line of HTML/JS/CSS or manual user testing being involved. Once we have the state machine figured out, we can build whatever arbitrary UI we want on top of it.



Have you used React? It also uses a "Component" approach, but it doesn't suffer from the same abstraction issues as Angular. Your approach seems basically equivalent to the "redux" approach except with an OO flavour rather than a functional one.

Personally I've found a mix of "component state" for things like UI state (e.g. is that modal open? is that checkbox checked?) and "external state" for actual product data (e.g. array of users) works pretty well.


It's barely even lib-dependent -- we use rxjs instead of redux; react hooks have a useReducer now, and the "reduce" function isn't a new idea so it shows up in many more places where it can easily trigger events. I'll often use this approach along with a "controller component", which manages the less directly visible components. It's common already for things like routing.


I have not played around in the React space at all. I do agree that what you describe sounds similar to my ideology. The part that I would disagree with is the coupling of _any_ state with a UI component.

What happens if you want to move the confirmation checkbox from the modal back to the main view? Do you have to edit 2 state machines or 1?


You don't edit the "state machine" at all. As long as the component is reflecting the state (which happens automatically), you wouldn't have to make any changes other than the moving of the checkbox.


I’d go further in pushing back and actually say I think the single global state store (or stores) like redux and others are an anti-pattern.

You actually do want your state to live inside components. The examples are numerous, but it basically all comes down to composability.

Here’s a recent example:

You build a nice “autocomplete” for your site. In redux, you’d have it all in some namespace perhaps. But now you realize you need a second autocomplete somewhere. What do you do?

In the component model, you’d have already written the state such that there’s an “autocomplete state managing component” that accepts the right props to initiate it. Adding the next autocomplete widget now is as trivial as adding a component. But in the global/redux model, you would be far more likely/prone to not only couple the initial props and logic to the initial data, but you may not even have designed any aspect of it to be composable because you never considered it to be independent. How do you extract your autocomplete and publish it for other teams to use? It’s not possible, unless you do a lot of work and force them to use redux as well.

This may sound overly architected, but in fact it’s less so in the React world. Redux is the additional dependency, and additional and different layer of logic. Keeping within the component model is simpler and grows and composes more easily. Especially once you get into things like parent/child components that want to share state specifically for that sub-tree.

My take here is global stores are a bad solution for UI where the goal is tree-specific composability and generally building flexibly re-usable lego blocks is the goal. Redux is an anti-pattern that is simpler initially but encourages bad patterns, calcifies your stack, and ultimately slows you down as the app grows.


Agreed. But redux is useful for some things like data on the currently logged in user, or the state of feature flags. These are bits of data that you may well need all over the place, and pulling them in from redux is easier than threading them through components as props.


Redux works through a first-class React feature -- the context api. You can utilize the context api yourself to create modular state wrappers that can exist side-by-side rather than being restricted to a single global store.

https://reactjs.org/docs/context.html


At that point you now have the downside of writing reducers (verbose, unintuitive) and the dependency on a large library with a lot of customization to get it even close to syntax you'd prefer.

Namespacing doesn't solve what I'm talking about here completely, you'd still be writing state away from the components that use it and in a reducer form, plus there are numerous pitfalls to doing this in redux (you'd end up with a whole toolkit of addons to get it to be what you want).

I'd recommend looking at something like zustand or recoil, both which are headed in the direction we should be at. There are others, check out dai-shi's work on a few different state systems (use-atom looks decent).


One, since state lives within each component and can be passed as props.


> What happens if you want to move the confirmation checkbox from the modal back to the main view? Do you have to edit 2 state machines or 1?

Could be either, it's up to the dev to decide at what level handle that state.

Often with redux you use a single global state


See also [Ephemeral state vs. App state](https://flutter.dev/docs/development/data-and-backend/state-...) in the Flutter docs for a great description of this.


What you described is exactly what I’ve been doing when building UIs. All of the logic of the front end application is completely outside of the view layer. I think this coupling of the view vs. the front end application is bad, and it’s the most common default mode.

Then, the view has only minimal logic. Like you mentioned, it does require some code, like mapping over an array to create the individual rows of a table. You also need one level of if branching, because sometimes you need to show or hide a UI element based on some state.

But that’s it. I don’t even render components when testing generally. The setup code for actually rendering UI is very brittle, and when was the last time your bug was that you called the wrong function in response to a button click? The small layer between the UI framework and the brains of the application is best tested by actually looking at the rendered view anyway. Its completeness is qualitative.

Anyway, I’m really fond of this approach and wish more front end devs were open to doing it.


I find the reference to Angular confusing here, since generally in an Angular app you will keep a single client’s state concerns in Services that hold the results of API queries, with those results flowing into the UI reactively. You only pass input into Components from other Components for transient, presentational UI state.


React team seems to have realized the same thing, and their solution is to what they call React Hooks. It allows devs to write the implementation of the business logic imperatively, but the calling it is effectively declarative.

The hooks are then used to split out the business logic of the app from its view.


This reminds me amazingly of writing HTML and binding JavaScript to layout to handle events.


It's not quite the same. The main difference is that the data-binding is only one way, so it make the whole thing a lot easier to reason about.


Excessive property drilling and manual event bubbling is problematic but only really commonplace in small projects. It’s recommended to use a pattern like you’ve described leveraging Angular’s dependency injection to utilize services for managing your application state.

Judging from your other comment about Blazor I don’t see anything you can’t do in Angular.


I am interested in understanding this further -- is there a write-up or explanation with example anywhere regarding how you achieve this in Blazor.


As far as I am aware, there is no public document on the internet which describes this approach in a consolidated way. Unfortunately, I cannot share examples from the codebase I am currently operating on. That said, I can offer some MS documentation links in hopes that this paints a slightly better picture for how I achieve this outcome.

This describes how you would request a scoped stateful service from an arbitrary Razor Component:

https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamen...

This describes how you would inject these stateful services into DI in a way that scopes them as desired:

https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamen...

Note that we are using Server-Side Blazor, so this approach works for us. I do not think it works with WASM (at least out of the box), but we do not care for this form of Blazor hosting right now, so its not a concern for us.


Note that we are using Server-Side Blazor, so this approach works for us. I do not think it works with WASM (at least out of the box), but we do not care for this form of Blazor hosting right now, so its not a concern for us.

Hmm interesting, how does that work? What enables the interactivity in the browser?


Blazor Server downloads a small js script (SignalR) to the client on connection. All user events are sent back to the server through SignalR, then DOM changes are sent back to the client based off of user input.

I use it at work for internal intranet web apps so it's pretty handy for our use case.


> Once we have the state machine figured out, we can build whatever arbitrary UI we want on top of it.

You can. It's probably still better to use a comfortable UI library to help, and if you're doing your app state transitions in a central place, pick a view library that works well with that pattern (React, its siblings, or Vue for example).

In a toy app, I recently tried updating some DOM using the unidirectional data flow model, but without a UI library. It was horrid. The DOM really fights you in this regard: it's pure OO and stateful itself so you're better off using a library to tear down and rebuild your DOM for you.


what is the appeal of Blazor (as for that matter yew), when now there's a healthy ecosystem based on Typescript for the front-end? Is it just the ability to never leave C# (or rust)?


Productivity is the biggest appeal. 99% of our experience with Blazor involves writing some Razor-blessed HTML, taking dependency on state services and plugging things into DI. Our existing business services are simply consumed by the UI state services, so there isn't even any code involved for wiring this part up aside from some AddScoped<T>() calls in your bootstrapping code.

Things that we do not worry about anymore: Javascript, controllers, CORS, JSON, HTTP status codes, API documentation, 3rd party dependencies.

I say we don't worry about javascript and that is true, but I want to be clear that there is still a tiny amount of javascript involved. We do maintain a single "BlazorExtensions.js" file which covers all js interop needs across the entire solution. This file is currently only 120 lines long. So, while we do have javascript, we absolutely do not lose sleep over it anymore.


> Things that we do not worry about anymore: Javascript, controllers, CORS, JSON, HTTP status codes, API documentation, 3rd party dependencies.

That's a good looking list of things not to worry about. Especially CORS, the bane of my life.. the fact it's pre-flight has caused me so many problems in my (tiny) UI responsibility.


I've lost approximately a week of my life to it. Never again.


How does Blazor avoid CORS issues? It's still running client-side, right? And thus would be subject to the same CORS restrictions as JavaScript.


How is the performance? Is this for an internal application, or a public facing one?


Performance is good, but I would only use it on well-bounded target audiences. Anything over 10000 simultaneous users on a single instance would probably start to concern me. It is perfect for any internal or B2B UI you might want to build.

That said, combine Blazor with other Microsoft magic like Orleans, and you can start to horizontally scale. Might even be possible to make Server-Side Blazor viable for mass public consumption using this approach.

Edit: It does appear this would be fairly easy to rig a test for as of latest versions of everything. Should be able to throw Blazor and Orleans on top of the same IWebHost instance.

https://devblogs.microsoft.com/dotnet/orleans-3-0/#co-hostin...


Yes, there is always some value in having the same language client and server. Both in terms of being able to have shared types and functions, and less cognitive overheard when switching from client to server.


I like SwiftUI. Haven't done much with it yet, just a few small experimental apps, but I like the concept. Especially 2-way binding.

Not being able to create components is a problem. How else are you going to divide and conquer? So you will need components for any reasonably complex UI. So yeah, the challenge is how to do components in a good way, not how to avoid them.


I really wish there were a Blazor for Java or Rust or anything other than C# really.


Did you have a look at LiveView for Elixir/Phoenix?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: