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.
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 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.
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).
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
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.
The hooks are then used to split out the business logic of the app from its view.
Judging from your other comment about Blazor I don’t see anything you can’t do in Angular.
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.
Hmm interesting, how does that work? What enables the interactivity in the browser?
I use it at work for internal intranet web apps so it's pretty handy for our use case.
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.
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.
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.
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...
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.
> 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.