I think React is a big deal because we can finally think of UIs as pure functions of data.
However, the entire mentality of the community is moving towards having behavior tied to specific portions of the tree; in essence it's common-practice to have stateful components. With Hooks this is even more the case.
Instead, the nature of UIs is that changing something in a part of a UI might and will effect something in a completely different location: when I click the button, open a dialog, show a spinner and reset the user profile screen.
The problem with writing stateful components is that the behavior (such as the one above) is tied to the UI representation (at which level of the tree should I write this logic so that it effects all the aforementioned components?).
We are essentially going back to tying logic and representation; something that we've been trying to avoid over the past decade or so.
I'm not saying React doesn't allow you to do differently; things like global-state (Redux, MobX, etc.) allow you to separate behavior and representation quite well. I'm more confused about the recent lack of best-practices surrounding this idea of separating logic-and-UI, and instead they're pushing towards: "Yeah now with Hooks you can even more easily bundle logic-and-UI together and have them tightly coupled". Which to me seems really confusing.
I’ve become a big fan of using finite state machines (more specifically xstate [1]) to encapsulate all the logic and states of a particular UI. You can essentially create all the behaviour of your application without writing a single line of UI code. At that point, you can use React exactly as you described, as pure functions of data.
As a company we’ve been experimenting with xstate for about a year. Generally speaking, bug counts go down as a direct result. We’re at a point now that it’s part of our process — a key deliverable as part of our first phase of development is what we call a “navigational statechart” which informs the development of the wireframes. The beauty of this process is that we can take that same statechart directly into the later development stages.
We recently did a proof-of-concept at my company and came to some similar conclusions. We can see state charts driving our development process exactly as you described, so it’s good hear that others are finding success with this approach!
We are going to be incorporating xstate at a basic level in one of our next projects, but I’d eventually like to see it become entrenched in how we do all UI development.
Really awesome to hear! I strongly agree with the notion that all popular front-end frameworks have an API that encourages tying business logic with UIs, and this is has been a known anti-pattern for decades now.
I don’t think this criticism of hooks is valid. A hook that exposes a “incrementCounter” function could be implemented with local component state, or global state via React context. The developer could trivially swap in one implementation for another.
The issue is that the component abstraction makes thinking of state as being specific to a subtree feel better encapsulated then having all components enmeshed with global state in their own way.
Doing it in a way that best utilizes the strengths of both of those abstractions will happen eventually, but old habits die hard. As evidence look at all the convoluted ways people try to make all style information public, conflating the public aspects and implementation details.
There is just a ton of sloppy thinking and very little design pattern leadership. For example Airbnb used react for a long time and all it really gave back to the community was an extremely anal set of linting rules which incidentally differ from Facebook’s in annoying, nit picky ways.
One can imagine the amount of bike shedding that such minutiae creates across the entire react community.
This is a model I sorely miss when writing server-side node applications. I really appreciate the shared-nothing architecture of PHP when I'm trying to figure out why some async code is causing two parallel requests to get confused with each other.
But I believe the GP is referring to interactive UIs, not static HTML. For example, you would never have a date picker widget render each month's calendar using a full server-side page refresh.
Hooks can be abused but they have their uses because some state is temporal. Forms before they're submitted and animations are two good examples. There's no need to store local animation state in a global state store every time you use a new component.
I think it also shines light on why GraphQL has proven to be such a great fit for UI programming, more so, I feel, than inherently tree-oriented solutions to state management like Redux.
This quote from a blog post from the Apollo folks really internalized the essence of GraphQL for me:
"GraphQL allows us to extract trees from the app data graph."
Highly recommend taking a look at the article if you're interested at all in GraphQL. I think the first few sections at least serve as a great primer, though later sections go into Apollo specific implementation details that might not be as interesting for a newcomer.
The apples to apples comparison to Redux here would be approaches like Apollo's apollo-link-state and Relay's client schema extensions that allow you to manage local state with GraphQL (in addition to server state).
Although Redux also allows you to store state in a purely normalized form that's more amenable to graph traversal, and the prevailing wisdom in the community says that is the de-facto approach for managing state in complex apps, Redux certainly doesn't enforce it or make it effortless and foolproof. That is in contrast to most GraphQL based solutions where normalized state stores are the default, and you have to work to deviate from it.
In essence, with GraphQL-based solutions, you conceptualize your data as a graph, extracting trees out of it to use in your UI (which is fundamentally a tree) through queries/fragments. That Apollo/Relay stores the data in a normalized tree by default is an implementation detail that you normally wouldn't have to worry about (in fact there are alternative implementations of the Apollo cache that stores data in graph form to enable a different set of performance tradeoffs: https://github.com/convoyinc/apollo-cache-hermes). Whereas with Redux, that implementation detail is the user's responsibility, i.e. you have to manually implement the transformation of your data graph into tree form, normalized or not.
I'd say yes, at least until we out grow the "UI as trees" paradigm that dominates pretty much all of modern UI development at the moment.
I can see that potentially changing if/when AR/VR takes off and we start building UIs that are 3D in nature, for instance, since the hierarchical structure of a tree no longer make any sense for objects in 3D space that have 6DOF.
A-frame might offer some early insights in this area since it's attempting to bridge the gap between the 2D document model of the web and 3D interfaces of AR/VR.
I think we're confusing coupling with co-locating. Hooks actually make it easier to separate things like side effects and state management from a component when compared to the status quo of class based components with lifecycle methods.
I'm a huge functional programming fanboy myself (my first full time job out of college was a Clojure/ClojureScript gig, by deliberate choice), so a completely pure mapping from state to UI sounded like a great idea when I first started diving into the world of UI programming.
Soon however, I realized that in practice (at least in the context of a React-based application), there's little to be gained by storing state that is truly temporal/localized in a persistent/global store (which is necessary for enabling a truly pure mapping of state to UI), and that there's in fact much to lose, because:
1) By nature of being temporal, they're state that needs to be initialized when they need to be used and destroyed afterwards, otherwise we risk exposing stale state to the next usage in time.
The initialization/destruction process often needs to be synchronized with some lifecycle of a component. Animations on mount/unmount, and form input that needs to reset when closing a modal, etc, are great examples of this.
If we used component local state to begin with, that state lives and dies with the appropriate component automatically, with no additional ceremony.
On the other hand, if we were to store that temporal state in a persistent state store, we'd still end up having to create components that hook into component lifecycles to create/destroy that state at the appropriate time in the persistent store manually, which, even if we ignore that it's often a tedious, error prone process, means introducing impure components into our tree of pure components anyways, so we've really gained nothing over the component local state approach.
Not to mention that temporal state is often frequently updated (as an example of an extreme, FLIP animations (https://css-tricks.com/animating-layouts-with-the-flip-techn...) by nature require at least 60 state updates per second), and having a few of these in a persistent state store can wreak absolute havoc on application performance if we mess up even a little bit in our pure rendering optimizations, which is notoriously tricky to get right perfectly due to all the edge cases we have to be mindful of (not creating new event handlers functions with every render is especially tricky in a codebase that's committed to using only "pure" components; in fact, I'm not sure if that's even possible in the strictest definition, since all the techniques I've worked with involve creating an impure class component to memoize the function, and the new useCallback hook likely isn't pure in the strictest sense either).
2) By nature of being localized, they're state that no other component should ever be concerned with.
Making them accessible at all to other components, as we do by storing it in a global state store, is by definition a leak of implementation detail, which over the long term makes it harder to change the components that use localized state, because we have no easy way to guarantee its "localized" state isn't being depended on by something else. In fact, to suggest that they're using "localized" state at all at this point is more wishful thinking than anything else, because nothing exists to enforce this localization once we put it in the global store.
At best they're just extra noise we need to ignore/filter out when debugging/serializing the state of the store, and at their worst they can lead to extremely brittle and over-entangled component trees where changing one part of the tree can inexplicably break seemingly unrelated parts.
On the other hand, component local state is for all intents and purposes, truly localized. We cannot access component local state outside of the component itself unless we explicitly expose it to children as props or to parents by refactoring it up a few levels, at which point we're making the explicit decision to change the locality of that state to include more/different components, and that decision is completely self-evident.
Whenever we decide to use component local state, we can rest assured knowing that their locality is enforced by the React runtime, rather than some handwavy global store access convention that we'd otherwise have to resort to if we stored localized state in a global store, which involves an ongoing cost in terms of enforcement while offering little in the way of real safety.
To be perfectly honest, some/all of the points I mentioned could potentially be attributed to React not abstracting us far enough away from the stateful nature of the underlying platforms it builds upon (the DOM for web, native UI platforms for React Native). For the case of temporal state, I can imagine for instance a potential alternative/higher-level library where React's stateful lifecycle hooks are replaced with some elegant purely functional primitive for supporting the same use cases, perhaps something that models _time_ explicitly as part of state, like Datomic does with transactions (nothing similar comes to mind for handling localized state though, so perhaps encapsulation and true purity are just at odds on a fundamental level).
Though I have not yet seen anything that would enable a truly pure state -> UI mapping for building non-trivial applications while avoiding the aforementioned drawbacks, I'd of course happily re-evaluate my position on the feasibility of this approach when I encounter such a solution.
WRT >there's little to be gained by storing state that is truly temporal/localized in a persistent/global store (which is necessary for enabling a truly pure mapping of state to UI),
Perhaps, I mis-understood, but when I do
.setState({data}, fnToCallAfter)
as a user of React, I do not really know where the state is stored, in other words, I do not store in global/persistent store.
---
I do use React.Context quite a bit to store my LoginState,
my special purpose in memory store (using Immutable.js) that acts as cache for some often used/expensive to get backend data.
When my LoginData, or CacheData get updated, react automagically calls 'render' (and static friends) on all the mounted components that have signed up to observers to the Context's changes.
This is very similar to newly release AndroidX jetpack Lifecycle-aware ViewModel, that calls the observer methods, only on activites/fragments whose lifecycle is 'active' (this reduces complexity of managing android activities during the 'rotation/config' changes.
---
I am just not clear where you run into a situation where you had to use the component-level state management in react, that required you to see the innerworkings (or implement) your mechanism to store component's state.
However, the entire mentality of the community is moving towards having behavior tied to specific portions of the tree; in essence it's common-practice to have stateful components. With Hooks this is even more the case.
Instead, the nature of UIs is that changing something in a part of a UI might and will effect something in a completely different location: when I click the button, open a dialog, show a spinner and reset the user profile screen.
The problem with writing stateful components is that the behavior (such as the one above) is tied to the UI representation (at which level of the tree should I write this logic so that it effects all the aforementioned components?).
We are essentially going back to tying logic and representation; something that we've been trying to avoid over the past decade or so.
I'm not saying React doesn't allow you to do differently; things like global-state (Redux, MobX, etc.) allow you to separate behavior and representation quite well. I'm more confused about the recent lack of best-practices surrounding this idea of separating logic-and-UI, and instead they're pushing towards: "Yeah now with Hooks you can even more easily bundle logic-and-UI together and have them tightly coupled". Which to me seems really confusing.