For me the issue of state management mostly went away after starting to use React Query. It turned out that the most problematic state was server-side state for me, which is handled very well by libraries built for that purpose.
The state that remains is often simple enough that plain old React state management with useState/useReducer is sufficient. A lot of state can be local, and local state is easy to handle with the built-in tools of React. And global state is only really an issue if it changes often. For mostly static state like information about the current user, theme or UI settings and similar kinds of state using React Context works perfectly fine.
Of course this depends heavily on the kind of web application you write, if your application is closer to Photoshop in the browser than a simple CRUD app you probably can make good use of more complex state management libraries.
Agree completely, React Query has made it so much easier to build any kind of app that deals with server state, stuff that would have been extremely challenging to write in the past is now just a couple of lines and works better than before.
I personally like using Jotai (if I just want a better version of React Context) or Zustand (if I need a bit more than that) for “client state” alongside React Query, but I’ve also built projects where I didn’t need a client state management solution at all.
Exact same experience here. We were using Redux to manage server-side data and switching to React Query massively simplified everything. It appears that managing server-side data in an efficient way is an insanely complicated topic and React Query just solved everything about it that I can think of.
Yeah, both the React Query and Redux maintainers agree that you shouldn't be writing data fetching and caching logic yourself. That's why we recently added a new "RTK Query" data fetching and caching API to Redux Toolkit, so that if you're using Redux it handles that work for you:
I took a look at react query and it seemed to me that the core of it was caching the network request promises with named keys, so that you could inefficiently call the same endpoint redundantly a bunch of times and not get punished for it. Even if a codebase could get away with such sloppiness it should probably avoid it.
And then as I recall it featured some out of the box refetching to unstale the cached data. I think this core idea is pretty good, though I don't like the cultishness of the people who use this lib. And the vast majority of pages in the wild I have worked on don't need this auto-refresh, as the data will be static until the user does something new (i.e. because the user is viewing personal data). Even if I wanted to refresh the data often (i.e. I had a site like reddit) I would prob just roll my own custom refresh logic.
If server side logic is "insanely complicated", and is routinely being solved in two lines post react query, instantly working better than files worth of server side data fetching code, why the need to charge 149 to learn the fundamentals? If I had taken a paid course to learn a state management library I would probably advocate for it everywhere I went.
It's a great benefit for a component to be able to specify what data it needs from remote without worrying about if an ancestor or sibling also needed it. React query will only fetch once the unique queries in the whole tree.
Without this feature, every component needing remote data D must share a parent ancestor fetching D for all of them, even when the children are not conceptually related. Adding another component higher in the tree means you have to hoist the fetcher up to that level, etc. React query is an incredible upgrade to anything else I've used over the years.
An external service fetcher doesn't have integration with React because the entire tree potentially needs to know when the data changed. Once you write the code to subscribe to those changes where necessary, you've basically invented another state library.
* Service fetcher does fetching and updating store
* Component does subscribing
* React does updating component tree with changes.
If a component wants changes it must subscribe. If not, it doesn't.
If there is a change, subscribed components get updates. If not, they don't.
Responses are (conditionally) cached in the store by its respective service. Any component that wants data just asks the service. Service fetches from the store or remote. I can customize and unit test the the service.
I don't see the overlap in responsibility between any state lib (react or whatever in this case) and the purpose built service.
You’re hand-waving away “subscribing” as if it’s not a time bomb.
The set of possible events any given query might need to subscribe to is massive. Yes, you can build a large app with a pub/sub model but the number of subscriptions runs away from you and it gets really hard to know which of 1000 subscriptions you need to refresh when an association between two random pieces of data is created/deleted.
Associations are where you really get screwed IMO.
That said, despite exploring lots of possibilities I don’t think there’s any perfect way to do this automatically. So while I think you’re hand-waving away a hard problem, I also think your proposed solution is probably one of the better ones.
Still, I don’t think you should pooh-pooh people trying to solve the general problem. It’s a worthy one.
Being hard to define and manage subscriptions is a problem, a bigger deeper problem that react query or any other lib isn't solving.
By "set of possible events any given query might need to subscribe to is massive" i assume you meant "component" and not query, if not then i don't know what that means, if yes, that component needs refactoring.
I'm interested in libraries not because of what it can do for my code or me or my team but what problem it solved for the team that needed it in the first place, bad enough to write a lib for it. I want to avoid those problems.
I start with importing libs for quick TAT and slowly replace them with a few hundred lines of code that i can understand, modify, debug, monitor, unit and integration test. I usually end up redefining or better defining the problem and rescoping the issue to not needing to solve said problems.
Most libs do NOT provide that level of valuable solutions to worthy problems to go keep them around as deps. Deps are not free and have no liability to me or my code. React query is one more in that long list. IMHO.
I do mean “query”. I am presuming you want every query in your app to live update when the results change. The problem I’m indicate is: how do you know which queries are invalid after any given event?
Its role is to do everything you just said in one package, plus other networking features. It fetches, updates the store, subscribes the components, caches the responses, performantly rerenders. It allows configuration of when to refetch, how to cache, polling, pagination, infinite scroll, etc etc via a simple API.
Nothing is stopping you from writing all this yourself, but libs exist for a reason. It's a terribly useful networking package. If you just use a generic store and write a fetcher yourself, you have to at the very least write logic for when to (re)fetch and for persisting the responses to the store.
The state that remains is often simple enough that plain old React state management with useState/useReducer is sufficient
hah this is an understatement. For hooks, the React runtime already does a ton of non-standard-js things under the hood. useState gives a method to update and "subscribes" a component to updates on the hook's value. Put that code in a custom hook, export it and you re-invented Redux. In fact, exporting a single big useReducer for all your state gives you something almost identical to old style Redux.
FWIW, there _are_ a number of technical and conceptual differences between Context+`useReducer` and Redux. I wrote an extensive post describing those differences and potential use cases for each:
React Query's critical design flaw (from when I used it, anyway), is that data is scoped globally by default, as opposed to per mount.
Using the global cache is kind of the point, yes, but that's implicitly the case and that's the exact opposite of how useState works.
I think that is an egregious mistake, since it is totally unclear to a higher level consumer that this is the case.
This may seem unreasonable, but I've seen it cause severe issues due to this choice and I would recommend against it for many teams unless they can properly communicate that it does not match the behaviour of useState - you need to look through layers to be convinced it's going to do what you expect.
A simple flag to opt-in to the global cache associated to the provided key would solve a lot of problems.
To summarize, I like the API they provide a fair bit, but it's just too easy to misuse and create a damn mess.
data is scoped globally by default, as opposed to per mount.
Uh, is the cache key the same? To me it is 100% clear that the cache key is used for a global cache, and that this is one of the core design principles of the library. This global cache management is exactly what I desire over useState without writing my own boilerplate.
I personally liked the library for its query and mutation APIs, but would have preferred caching be left out or opt-in.
I ended up having to do this myself by wrapping it all up and providing an option for the caller to opt-in to the underlying global key. If the option was not passed, it would append a unique ID to the base key by default so it would become scoped to the particular mount.
Nothing implicit about react query caching requests. That's the whole point of the library in the first place, otherwise it would just be a fetch wrapper.
And having it behave differently from useState is fine, and correct. The library is used to fetch data, not state. Those are two different things
I ditched React/Redux for Vue 3 & Pinia and my god, what a world of a difference. Everything is intuitive and I am not googling days to fix tooling issues, or having to do it the React/Redux way which was honestly overly engineered and non-productive.
Frontend shouldn't be complicated and we've been lied to, you are not Facebook, you don't have billions to throw at engineering, you must pick the path of list resistance
As someone who didn't get react at all before hooks, and still doesn't 100% get react with class components (but is quite proficient with hooks) what's the issue with hooks specifically? genuinely curious. I find react with class components very messy, and especially in the basic components, I find useState + useEffect a lot cleaner and easy to reason about than setState and all the lifecycle hooks...
I had a project where state was coming through redux, from parents via props, context somewhere from parent elements, xhr fetch/graphql, local storage... Debugging was hell. I've always hated hooks, they replaced easy and clear lifecycle functions and rely on magic to work, literally don't function like regular js functions but look like regular functions
> Debugging was hell. I've always hated hooks, they replaced easy and clear lifecycle functions and rely on magic to work, literally don't function like regular js functions but look like regular functions
You know, with how hooks work in Vue, things seem be easier to understand and get started with.
Though at least with React I've had plenty of projects end up with render loops that are hard to debug because of how everything is written. So much so, that I wrote a blog post about it a while back "Modern React is broken": https://blog.kronis.dev/everything%20is%20broken/modern-reac...
Personally I wish that we could already have some tooling that'd tell you something along the lines of:
Render loop detected! The following chain of calls was responsible for it: A -> B -> C -> A -> B -> ...
Please check the useEffect hook on line X, which has the following items in its dependency array which changed: [Y, Z]
React components don't function like regular JS functions, but look like regular functions. So that's obviously not a legitimate knock against them - you're working within the React runtime. It's not normal Javascript.
And lifecycle methods are just as "magical" as hooks. If you just thought about why the "rules of hooks" exist for a moment instead of just hating change for the sake of it existing, you could probably intuit how they work under the hood.
Lastly, lifecycle methods don't allow you to co-locate feature-related code, can actually create more bugs (if you have logic in `componentDidMount` but forget to add something similar to `componentDidUpdate` for example), and any logic contained within lifecycle methods isn't composable / reusable.
I really don't understand where you're coming from at all with this.
React should act like a framework/library, not become a transpiler and create javascript 2.0. It's hugely confusing, and harder to reason. I shouldn't have to learn about internals of the library to figure out what's wrong with my code. it's a smell of bad library, this wasn't the problem when simple class functions were used as lifecycle methods.
I recently did the same, and hand's down, the vue ecosystem is weak.
Vue is difficult to work with (unable to render content without a functional component wrapper, poor support for style libraries like tailwind), and the state management ecosystem is fractured between vuex and pinia. Worse, much of the help online is for old versions (vue2). Storybook 'out of the box' is broken and doesn't work at all (unable to resolve '@/...' imports).
It's been painful.
> Everything is intuitive and I am not googling days to fix tooling issues
Can't relate. Tailwind works fine with anything that supports PostCSS. I run it with Vite and there's zero issues.
> the state management ecosystem is fractured between vuex and pinia
This is also just not true. Pinia is officially replacing Vuex as the recommended store library for Vue [1]. They're also vastly similar in how they do things, so the knowledge transfer over from Vuex to Pinia. And Pinia just address most of the design goals mentioned in the article in the most simple way.
As for Vue 2 -> 3 transition, lots of the larger UI frameworks in the ecosystem is struggling to migrate, despite lots of efforts on the compat layer to smooth the transition, which is a bummer. But as long as you're not doing those sophisticated things, Vue 2 examples should work out-of-box on Vue 3 as well. There are surely less resources for the composition API, but the official introduction guide has been good enough in my experience.
> As for Vue 2 -> 3 transition, lots of the larger UI frameworks in the ecosystem is struggling to migrate, despite lots of efforts on the compat layer to smooth the transition, which is a bummer.
I actually recently looked into most of the frameworks out there and their migration efforts.
So far, I only found three viable options for Vue 3:
- PrimeVue https://www.primefaces.org/primevue/
- Quasar https://quasar.dev/
- Element Plus https://element-plus.org/en-US/
We went with PrimeVue and while using PrimeFaces was an incredible pain with Java, the Vue version seems a bit better. Then again, it's kind of odd that libraries as popular as Bootstrap don't have complete bindings in the form of Vue 3 components.
I like React's easy approach to making components a bit more, even more than Vue 3 with Composition API. That said, Pinia (https://pinia.vuejs.org/) does indeed seem like an excellent approach to state management!
My only problem is that my IDE of choice (WebStorm) is incapable of providing any sort of autocomplete for it, with JavaScript. For example, consider the following store, as an example from their documentation:
Really simple to define it and then change it with $patch, right? Well, when I try to access the field which is included in the default values, my IDE won't show me the available fields when used in some component:
const counterStore = useCounterStore();
const isSomethingExceeded = counterStore.count; // .count does not get offered as an autocomplete option
That is kind of annoying, when you have a whole bunch of different fields in there, even if you can predict which kinds.
Well, there's also the thing where WebStorm refuses to insert <script setup> tag or even add imports inside of it if it's present in the header of the page, but instead I get a redundant <script> tag at the bottom with the old Vue syntax for defining components, which I then have to clean up manually.
I find it very disappointing that this article does not attempt to draw a distinction between state management for complex local states (imagine an image editor), or state management for fetching and updating data via APIs
I think the shape of solution needed for these two problems are quite different.
Exactly. I think people who complains about complexity of some of the tools, just have not worked on the complex webapp(not the webpage) before. Unfortunately these 2 always get conflated.
Those are conflated because on complex webapp developers own the problems with state management, sequencing of methods, concurrency, caching, queueing, categorizing actors, etc. The developers in this set would understand the need to separate these things because they often have to customize it and state management are often not suitable for customization.
Meanwhile, most developers don't need to have customized actions sequence. Therefore they can just pick one state management for them to delegate these stuffs.
Yeah. But web developers need to understand, broadly speaking there are 2 very distinct use cases. They NEED to scope whether they are making a webapp which could even be comparable to a native app, or a webpage which is just dealing with some async requests.
100% this. Though devs in one might not move to the other in short period. So this knowledge should come from outside of their work, which I don't find many in the net.
Same here. Local state and server state are not always the same. I see a lot of people saying how replacing redux with react query worked well and that’s great but there’s definitely times when you have a lot of state in a client side heavy SPA. The distinction should really be highlighted more.
I think the obsession the react space has with "state management" is a by-product of how incredibly convoluted reacts rendering is.
The other reason is a lack of exposure to how other technologies for GUIs have handled state for decades.
Maybe look outside the react bubble and see how many of these "issues" just disappear when you stop acting like react is some fundamental particle of the web.
Frankly I feel state management is a difficult task on desktop apps as well, to the point that tracking spaghetti-shaped causation and control flow is beyond my mental abilities. Qt itself as well as many apps are rife with redundantly calculating state or redrawing GUIs when changing the same value multiple times, or changing two values which both affect an outcome (my StateTransaction pattern mostly alleviates this issue with a set of dirty bitflags and recomputing all state dependent on those bits, though the reactivity system is currently hard-coded and statically dispatched, and generalizes poorly to open-ended state or managing the local state of many dialogs of the same type). And one of the craziest errors caused by witnessing malformed intermediate values is https://github.com/Dn-Programming-Core-Management/Dn-FamiTra..., where a sloppily-written "load document" function redrew the UI in the middle of mutating document state, causing the GUI to crash after observing a broken invariant.
It saddens me that so much of research in developing better state management techniques is in such a bloated and dependency-laden environment as JavaScript on the web. I like QML's reactivity system, but its evaluation engine is JS-based, dynamically-typed, and dynamically-scoped, and the UI engine itself is a buggy mess. And GTK4's list APIs promise to be better than the clusterfuck of Qt Widgets/Quick's QAbstractItem{Model/View} system (which abstracts poorly over list/column/tree collections, and widget-internal, cross-widget, and cross-application drag-and-drop), but I haven't tried that either.
While I don't deal with the ui too much (and hate the area / complexity), the best system I've seen so far is .net's WPF with MVVM pattern. You wire up the ui elements to models and things just work. Data dependencies are explicit, useless redraws are not happening if you schedule the changes on the right thread, the whole system can raise a coherent "binding error" rather than explode. It's definitely a solution I hate the least.
I haven't worked with Qt so I don't know how the API looks,
But on a complex app, to see "dirty bitflags", "intermediate values", "redrew the UI in the middle of mutating document state", I feel what you feel, and I want to share what helps me and my team overcome this kind of complexity.
This happened a few years ago to my team. We alleviated this with several solutions. The most effective solutions is to 1.) separate UI logic/compositive component from UI visual/appearance component, 2.) separate UI component from actors.
1.) Separating logic from visual helps developers define problems separately, especially those around the required states, step-functions, and lifecycle. We made this separation because of three things: 1.) We observed that our programmers are simply divided into these two categories, 2.) The dependencies of these two sets of components are simply different (e.g. logic --depends-on-> actors and ownership/lifetime management modules, while visuals depends on DOM, rendering, setting up callbacks), 3.) It is actually good to have a high cohesion between UI and logic modules. You may want to use the same UI for a slightly different logic, and vice versa.
2.) Separating UI component from actors helps a lot with decoupling state changes and re-rendering, and actually gives developer a chance to separate/abstract/generalize concerns when things have gotten too complex to be written in a UI component.
Actors is what I call any kinds of data that can act, have their own agency. It can be what you call a service, worker. It can have a Timer-based or queue-based internal lifecycle-management that does not require interaction from user (very useful for things like updating background data at interval)
UI component can both "own" or "borrow" actors. "owned" actors dies when its parent component is destroyed, while "borrowed" actors does not die when its borrowing component is destroyed.
We decouple actors from UI component and set up bridge between them. UI component can signal actors by function call, and actors can get back to the UI component by using either a return value or event/callback system.
And the last but not least, writing app in a smartly typed language combined with functional domain modelling helps a lot (see about it here https://www.youtube.com/watch?v=Up7LcbGZFuo).
Sorry, I don't understand what you're getting at. When timers or user actions are triggered, what code runs, where is the state it modifies located (next to the specific timer/action, within a module or dialog's object, or globally), how does the function determine which parts of the UI to reload from state, and when does it reload the UI? (Is this explained in the video or not? I haven't watched it yet.)
About the video, I should have made it clear that the video only talks about functional domain modelling.
Before we got into where state should be and when is UI reload/rerender triggered. I'm going to tell you a little imaginary problem.
Imagine you are building an application for downloading several huge files.
1.) This app must have a download page to trigger the downloading of various files as well as monitor their statuses.
2.) a system to manage multiple downloads. It can queue multiple downloads but only one should run at a time. But, regardless of the download page is open or not, the downloader should run its download.
3.) a persistent notification system for the app to notify the user (e.g. if a download succeeded or failed). This notification should be persisting, meaning that if the user does not dismiss a notification, it will stay there. The UI for the notification system looks like a smart phone's one.
4.) There are several other pages in the app.
Before we got into asking where state should be placed, we should examine what actors are there. There are the "downloader", the "download page", the "notification system", the "notification UI".
Because we have these several actors, we need to assume that all of these have several local states. All of these actors need to be in the component tree, but not necessarily bound to the rendering lifecycle.
These dependency arrows form the component tree automatically. Now, let's examine the solution:
- The state that tracks the notification should be in the notification system. The user-facing message in a notification item should be a copy that is put into the memory of the notification system.
- The state that tracks the queue of multiple downloads and the function that schedule the downloads should live in the downloader. These scheduler will have its own timer-like mechanism to notify itself that it needs to run other task if one is finished.
- The download page "listens" to events and "borrows" data from the "downloader". So if the downloader makes a change, the downloader page will change too.
- Last, the notification UI. The notification UI lives longer than the download page, because the user can switch between page but the notification UI stays on. The notification UI listens to the notification system for changes in its state and borrows its state.
- If you pay attention to the dependency arrow, notification UI and and download page both are the descendant of the notification system, but only notification UI react to "change-signal" from the notification system. Download page should not react to any signal from notification UI (e.g. no rerender).
- This is an indicator that the notification system must not be bound into the re-render lifecycle of the tree of components. Notification UI should explicitly subscribe to the notification system in order to allow other page to ignore the notification system. In JavaScript/TypeScript it is pretty easy to implement a callback-based event emitter that can be attached/detached.
- The notification system and the downloader is what I call an actor that can, but not always, be bound into the render lifecycle. It lives with the component that spawns and destroy it, but is detached from it.
Almost true. As someone who have been working on a complex desktop app for 4 years now.
I found that state management solutions are always incomplete for the problems we have in our team.
When you say "stop acting like react", it is actually true that some mechanism needs to escape from React's render loop while retaining its tree-like structure. New members to the team will have to be introduced to the idea that an actor (object that acts on data) does not need to be the same entity as the React component itself. We end up taking a lot of concepts from other domains such as game engine, Rust's ownership/borrowing
What we ultimately need are that's not fulfilled by many state management libraries out there:
- Instead of global vs local, we need management of scope, referability, and intuitive dependency injection.
- Instead of state, we need management of actors.
- Management of lifetime, ownership, and borrowing (concept taken from Rust)
- A differentiator between actor, function, and data (which entity should or should not do stuffs)
- Two-way communication channel
- A consistent code semantic that makes sense to describe it all because TypeScript/JavaScript does not cut it.
This doesn’t work, the most sound comparison web developers can usually do is to jquery (speaking out of previous thread experiences, lots of them). I also see all this movement as digging more steps into the same pit they’re in, in hope that it would make a ladder out somehow. For two decades of building various business UIs on various platforms I’ve never felt that it was hard or error-prone at the level which could bother anyone to fix once and for all with some funny legs above head technique. But now it suddenly (well, gradually) became a thing everyone’s suffocating without.
I still find the whole subject so frustrating. Being a backend developer that has since out of necessity had to transfer to being a frontend-architect for a team of confused React developers, I'm struck with how the momentum of frontend development quickly leads to very complex state machines. Backend development hasn't tended to require this - you query information, perhaps you transform it, you return it. But with frontend state machines, you've got things that feel like global state sitting all over the place.
If the logged-in user is this or that and/or the component state is this or that then do this unless that, and if this part of the component changes/re-renders to be that, then refigure the entire state all over again so this or that can be adjusted if necessary...
I mean, I get it, basically any GUI is going to have matters like this. And I get that the idea is to work really hard to limit the contract of each sub-component so that it can feel pure and only be dependent on a limited number of parameters/properties coming in. After all, on the backend it's also easy for a bad programmer to write a function/method that accepts forty parameters and has horrendously complex logic inside.
But in practice, something about the product cycle and the feature cycle of React just makes it so easy to turn into a huge snarl really fast. I feel like I'm battling against the tide. Maybe it's just something specific to our team and I need everyone to just freeze until we get some better practices in place.
There are things that are "global state", at least in the sense that you're likely to care about them in a hundred disparate places. If you're writing a UI, odds are at some point you're going to want to see "current user ID", "language selection", etc.
Why can't we just say "it's global" rather than doing all sorts of plumbing to pipe these details around the code base? I know we have stuff like contexts, but it feels a strange workaround, like someone saying "I'm on a diet, no sugar... but put 12 teaspoons of honey in my tea."
I know that the standard line is that it's difficult to test globals, but maybe developing a better test mechanism is a more tractable problem than having to add extra infrastructure to reinvent globals.
I think this is a bad take on an old premise. It's not that "global state is bad", rather, "global MUTABLE state is bad". Which is kind of right but for the wrong reasons. Truth is, "mutable state" is bad and "mutable global state" is even worse. That comes from the old times of OOP when people insisted in "encapsulating" state and relied on mutation.
It's sort of a logical fallacy. How could global state be bad if reading from a database o an API is essentially that? The problem is mutation, that if global state is mutated somewhere, other places that read the same state don't know about it.
Modern state managment instead is immutable and is not even an original idea of React. Redux was basically copied from Elm's state management.
It really should be global, if it's global state. Maybe not directly in the javascript global (window) context, but part of a shared singleton, in an obvious (or at least consistent) way, or similar.
The simplest and most obvious pattern I've ever used was essentially an angularjs (1.x) directive that allowed templates to grab named references to services and directly reference that state. Mutations were also exposed on the services as plain old functions, and it all just kind of worked. Looking at a template, it was blatantly obvious where the data and methods came from. The two way binding just made it dead simple to grok what was going on once all the layers of abstraction (controllers in the AngularJS world) that might rename things or make up new clever abstractions were gone.
I've built with React a half dozen times now, and I still haven't found anything that got me close to that level of simplicity. I'd say most of my experience has been that it's at least 10x more complex with all the gymnastics you have to do to keep the data flow unidirectional.
This reminds me of a mithril project where my state management library was just a global variable. It was a breath of fresh air: simple code, snappy UI, no convoluted debugging.
Yep, Mithril's redraw system is definitely one of its strengths since there is no plumbing necessary. You're free to do whatever you want without any weird adapter libraries. You can build some decently robust state containers without dependencies or Mithril-specific code[1].
I’ll add to this, there are tough problems that we solve on the front end that the backend could solve, but backend developers run scared from.
For example: most backends will let you query arbitrary data, but they won’t alert you when that query changes.
So we solve this on the frontend (often laboriously, with spaghetti).
IMO backend developers are often unwilling to do anything beyond CRUD… and specifically they are often unwilling to model the dynamics of a domain. They want to treat the domain as frozen in a moment of time.
In general, I would love to see more backend developers be more ambitious about trying to see which jobs the frontend is doing that could be solved on the backend. I think that could distribute some of the workload more evenly, and give BE devs a taste of what that higher level of complexity looks like.
Sometimes I think backend developers just write off hard problems that they could solve because “that’s a frontend concern”. It’s an easy excuse.
That is one possible explanation, sure. But more than inherent complexity, it's also about whether there are sufficient guardrails and best practices in place to help manage that complexity.
Sometimes the business logic just sucks and you have to deal with it.
For logged in stuff in particular, it's easier IMO to fork entire pages/components completely instead of trying to make things handle both cases. You can always come back and refactor if it turns out the splitting was unnecessary, but it's really hard to go the other way.
If you have a good, generalizable alternative, maybe you should try turning it into a library. My feeling is that “database you can query” was the idea of Redux. It worked out all right, but the performance bottlenecks were kind of bad because everything that queried the store had to subscribe to every update and do some dirty check which eats up performance. The solution was to use Redux sparingly, preferring local state for things that need to be updated frequently, kind of like an L1 cache. But again, that leads to fragmentation.
I think Recoil is supposed to fix this issue but I haven’t learned it yet.
Mobx has made me finally appreciate frontend programming again. It's like people want to deal with the insanity that is redux only because it's a more pure functional style which react seems to promote.
Something I like to point out to teams when it's the case, and it's often the case, is "When is the last time anyone on the team has used time travel debugging? Never?".
I'll agree that the Redux DevTools "skip action" and "jump back to action" features are not all that commonly used in practice. I _maintain_ Redux, and I don't even use them that often.
On the other hand, the ability to see a written list of all dispatched action type names is valuable by itself. So is the ability to click one of the listed actions and see the action contents, state diff, and final state. _That_ is very powerful.
Beyond that... I now work at a company called Replay ( https://replay.io ), and we're building a true "time traveling debugger" for JS. Our app is meant to help simplify debugging scenarios by making it easy to record, reproduce and investigate your code.
The basic idea of Replay: Use our special browser to make a recording of your app, load the recording in our debugger, and you can pause at any point in the recording. In fact, you can add print statements to any line of code, and it will show you what it would have printed every time that line of code ran!
From there, you can jump to any of those print statement hits, and do typical step debugging and inspection of variables. So, it's the best of both worlds - you can use print statements and step debugging, together, at any point in time in the recording.
Additionally, because Replay records the browser's OS calls, it captures _everything_ that happens in the page. That means you can debug _any_ website or JS app, no matter what framework it uses - React, Vue, Angular, Svelte, jQuery, or vanilla JS.
I actually recently implemented a POC version of support for the Redux DevTools in our Replay debugging app, so that if you do record a Redux app (or Jotai, or Zustand, or NgRx), you can use that same Redux DevTools UI to see the action history.
So, yes, time travel debugging _is_ an amazingly powerful concept. It's just ironic that that particular aspect of Redux didn't end up getting used that much... but the Redux DevTools themselves are still valuable, and Replay is actually a far superior "time travel debugger" overall.
I actually made pretty extensive use of the redux time travelling debugger at my last job, I found it to be very convenient, especially as the application got bigger and more convoluted - not really because of development issues, but just general feature creep (features were being sold left and right by a less than stellar marketing team)
I’d say the two biggest hazards with the reactive/declarative style are cyclic dependencies in the data model and remembering history.
Tools like MobX let you write quite elegant code in the right circumstances but they are less helpful if, for example, you have a complicated set of constraints and the effects of changing one value in your state can propagate in different directions depending on what first changed to start the cascade.
This style also tends to emphasise observing the current state of the system, so if you need a history as well (for undo, syncing with other user actions via a server, etc.) then you probably have to write a whole extra layer on top, which is more work with this kind of architecture than for example if you are using a persistent data structure and reducer-style updates (as popularised by Redux within the React ecosystem).
For a project that was basically an after effect like application I needed a history, but also very fast updates, settled with mobx-state-tree which gave me a way to preserve history. Worked great for that application, no regrets.
So there are some solutions available if normal mobx is not enough.
This is a really great explanation - I love MobX in many ways but the “observing the current state of the system” definitely has its downsides, which you’ve expressed very clearly!
That's actually what converted me to Vue. I did all of the Redux stuff, but then found Mobx and enjoyed that so much more. A friend of my told me "if you like React + Mobx you'd probably like Vue" (I think he was quoting from the Vue website). He was right.
Yeah when using React (or Inferno, etc) I default to Mobx.
If you like that model, Svelte stores are very close to that as they are really reactive primitives. Honestly I think it was a terrible idea to call them stores.
SolidJS is also built on this idea of using reactive primitives. In this regard it's even better than Svelte as you get true fine grained reactivity.
Seconded, I looked at using MST a few years ago for a React Native app with frequent (30hz maybe) state updates and the CPU hit compared to plain MobX was too much
I've advocated for Redux in many commercial projects because I knew that it worked well at scale. There was occasional pushback due to the large amount of boilerplate code involved (actions, reducers, sagas, models/interfaces if using Typescript) but the team mostly settled on Redux because it was the best supported and most widely used state management framework at the time. IMO the larger pool of devs that understand a framework, the better a project's chances are of success mainly because the industry has a high turnover of developers and it's easier to find replacements to maintain the existing code.
That being said, I'm now a huge fan of react-query.
Redux is weird because it's only necessary at a certain scale, but it's the same scale where having a global shared object makes things really confusing. For example, suppose you're trying to deprecate a reducer; you need to find all consumers of the state it manages. But you can't, because there's no actual dependency between a reader of the store and the reducers. You're stuck wading through all the useSelector or connect() calls in the entire app; hopefully the reducer was managing a state branch with an easily-greppable name.
Question for you since you seem knowledgeable about React state management. When I did a lot of back-end programming in Java we frequently coded to interfaces where we could swap out back-end implementations for ORM for example if needed. There's an ongoing joke where nobody actually did that.
Is there something like that in React? My understanding is that there's a lot of coming and going of state management frameworks, and it would seem to me committing to thr API methods of a particular framework would be risky. Or am I thinking at the wrong abstraction level?
To some extent, this was the purpose of the "container/presentational" pattern that was popular for a few years [0]. By splitting your components into "containers", which were responsible for managing data fetching, and "presentational" components, which just received data as props and displayed the UI, in theory you could swap out the state layer someday without having to alter most of the display handling.
But, the community really began over-obsessing about that, and often treated it as a rule you _had_ to follow (to the point of people seeming to panic and asking for help about whether a particular component should live in a `/containers` folder, `/components`, or somewhere else).
Dan Abramov, who wrote the article that helped really popularize that approach, later updated it to say he no longer finds it very useful.
In addition, React hooks push you towards a very different approach, where each component is now responsible for calling the hooks that it relies on for data fetching. That hook may still abstract where the data actually comes from, but the calls are now part of the component itself. I talked about this change in approach in a blog post and conference talk conference talk [1] [2].
Finally, the testing approaches in the ecosystem have changed as well. Instead of "shallow rendering" components using the Enzyme library, the community has moved on towards more "integration"-style tests with React Testing Library. This does require more setup work in tests to ensure you have all the various data providers wrapping the components under tests, and real or mock data being loaded, but the tests themselves become simpler and any state library usage becomes basically irrelevant to the actual test implementation. See [3] and [4] for some thoughts on that.
Soooo... yes, you _can_ write more abstraction layers, split your components by "containers", and even add DI via React context or some other purpose-built library if you want to. You could even abstract out all the UI components you use from a particular library just in case you end up swapping date pickers or something. But as always, it's a question of whether that will actually provide a benefit, now or in the future. And in general, most React apps do not bother with those extra abstractions.
The "state management problem" appears to be an invention of React. This didn't use to be a problem with MVC. The state lives on the server. When state changes it should be persisted on the server immediately (in case the user unexpectedly closes the browser). On the client all you need is a cache.
With MVC, each page is more or less independent. Each page gets the data it needs from the server (through a client-side cache), then updates the server when something changes. No "state management problem". In contrast, ReactRouter sees the entire application as one humungous component. Therein lies the problem.
People demanded more interactive web apps, which led to more stateful UI, which led to React.
In the case of Facebook, the motivating app was Ads Manager — an enormous, highly interactive single page app for advertisers. You couldn’t build something like it with HTML + server endpoints; in the pre-web days, apps with this kind of complexity would be desktop apps.
Google Docs, Google Sheets, Maps, Spotify — these all have a huge amount of client-side state. You can’t represent that state on the server without making the UI unusably slow. The need to build these kinds of apps in a portable way led to complex state, which React tried to address (so did Angular, Ember, Backbone, etc.).
> People demanded more interactive web apps, which led to more stateful UI, which led to React.
Except they didn't. 99% of web developers work on crud apps, not Figma. The users never really cared. You can achieve "good enough" results waiting for server. I still see over fetching from server instead of updating caches in most frontend apps anyway. Things like a button optimistically updating, just show a spinner and wait for the server. Devs just want to overcomplicate things just because they don't know any better.
I'm in an organization which had some extremely old, all-server-side-PHP tools.
We managed to get one section of them retooled as responsive HTML/PHP design, and then went whole hog on React.
Every single piece we replace with React seems slower and more complex. Tools that were "render it on the server as a finished page" has been replaced with "do it as an API that returns a ball of JSON and the client will render it." Most of these tasks are classic CRUD. So in the old version, you'd click a button or link and the page would reload in 5 seconds. Now, it lets you stare at a spinner for 10 seconds while it repopulates the same page. But golly, we avoided that flash of blank screen!
We were sold that splitting off an API would allow alternate implementation-- power users could bypass the UI and build their own automations. Nobody has; I don't think the API even firmed enough to be worth documenting.
I think the thing I resent most, though, is the tendency of modern Javascript to be a fantasy language. It does not exist in the wild. It's not like you can even say "here's a cool trick that works in Mozilla but not IE6" like back in the old days, it's literally zero-real-browser support. You instead need to set up an entire dev toolchain of stuff like Babel and Webpack before you can even get to Hello World. This seems like such a loss, given that the web was such an accessible platform-- get your $2 per month shared hosting, start writing a single file in PHP or raw HTML, and you can actually see useful stuff on your screen.
> In the case of Facebook, the motivating app was Ads Manager
Google created Flutter and its native language Dart in the process of developing AdWords. I don't know if it was the 'motivating' application for Flutter+Dart, but the first public exposure of Flutter happened less than two years before mobile AdWords was implemented with it.
I guess selling ads it a pretty complicated business if you need to create frameworks and programming languages to pull it off.
As the joke goes, there are two hard problems in software:
1. Naming things.
2. Cache invalidation.
3. Off-by-one errors.
Caches are tricky beasts. First, you need to update them when the server-side state changes (for example, two people editing a single Google doc). Second, writing state changes to the server and waiting for confirmation is too slow for many kinds of interactions. In these cases, it's typical to optimistically write local changes to a local store, and then try to sync those changes with server asynchronously. But if synchronization fails, then the client needs to report that.
And finally, there's the problem of partially loaded state, where the rest is loaded asynchronously on demand. That should be simple, but without some kind of framework, it also tends to have lots of subtle state-machine and null-reference bugs.
So, no, cache does not always stay simple in large applications. (Unless the app is read only, and you're OK showing stale data.)
If you are building Google doc editor by all means, design a complex state management system, it is worth it. But then don't bring that complexity into typical business applications.
It’s all true, but you missed the part where react and/or redux are talking to the server to manage the cache. Because if they aren’t, it’s unclear why tf they are holding the mic.
The “M” in that MVC is “Model”, which is the same idea as “state management”. It’s an object that stores state and notifies listeners when it changes. You can think of these libraries as ways to implement the “Model”
concern in the application.
But from your use of MVC, I think you are referring to server-side MVC, where the “V” is a rendered HTML template? I’m not sure how to square the desktop software version of MVC with your assertion that state should only exist on the server.
My Cocoa desktop apps ran fine 10 years ago without a server. The React application I work on today has many bits of local state it needs to track like “what is selected?”, “how wide is the sidebar?”, and “should this menu be open?”. I don’t think the server should be involved in such matters.
This is true until your customers complain that your UI is super slow. You realize they’re trying to use your app from a cell phone with poor service. So you have the genius idea to add optimistic updates in JS. Now you have all the problems from the article since you need to update all the components everywhere on screen that share state.
> Now you have all the problems from the article since you need to update all the components everywhere on screen that share state.
If anyone can field a question about react-query here, this seems like one of the exact problems I thought it solved when I started using it.
I do enjoy using it, but requesting the same data with the same key+queryFn from multiple, unrelated components still generates regular requests at the configured interval (even if I'd want/expect those components to share the response and only make that request at whatever rate satisfies the shortest configured interval)
React-Query is very aggressive about refreshing the cache by default, including whenever the browser receives focus, on a set schedule, and automatically retrying failed queries multiple times, these can all be disabled. If any queries use the same key they should share data, but I haven't experimented with it enough to learn all its intricacies.
Sure, all the other settings work fine (refresh on focus, failure retry, etc.), I just would have hoped to limit the requests made when the same data is requested in many different places.
I was similarly surprised at how 'chatty' react-query was by default, but changing these defaults quieted it down. Great library though, and I understand the argument for these defaults.
How do changes to that cache get displayed? Your cache emits events that each UI element has to listen to? Congrats you’re using redux!
(It would perhaps be more accurate to say that React folks had to reinvent these patterns, but the problems were definitely present in the postback era)
It's not even necessarily the same information, but small pieces of one bigger piece that need to be updated once some of the other smaller pieces change
Yeah this. The classic is unread state in a mail app. Unread state is usually displayed both as bold state per message in a message list, and as a count in the folder list. These need to stay in sync as the user reads mail (and sometimes marks read state explicitly). Two views of the same underlying state.
Sure, you can come up with an example where some synchronization is needed, but how often do you have this requirement? In most apps it is rare, which means that it can be solved by using a couple of extra lines of code to update the screen, as opposed to adopting a complex state management system with events firing and so on. Simple solutions for simple problems.
I used to think the same thing until I considered that single page app development is really just a reinvention of thick clients.
When you look at an SPA as a thick client state management is a natural thing as it was in Java swing and WPF and Windows forms and other stacks beyond my knowledge
React wasn't the first complex frontend framework, arguably people were having these state management problems ever since people started doing more stuff like jQuery UI. It's not tied to SPAs either, you can have state management problems for a single URL as long as it has any form of state change (like opening a modal).
For anyone struggling with the complexity and boilerplate of Redux, at my day job we added Redux Toolkit (the newly recommended add-on by the Redux project itself!) and it solves most of the verbosity issues! We use it to share all our state logic over mobile and web and it works excellently now. I'd actually recommend Redux w/ Redux-Toolkit, for the first time ever.
There's a library called rematch that's been implementing most of these ideas for a long time, but sadly it didn't get popular enough and it seems to be somewhat lacking maintenance. It was such a pleasure to use (in comparison) that I just can't believe how people ever work with those massive switch statements.
One of the reasons we created Redux Toolkit was to eliminate the need to write those switch statements :) (also action type strings, action creators, object spreads, having to split logic for one feature across multiple files, and all the other "boilerplate" concerns that users rightfully complained about over the years.)
I used Redux and loved it until I moved to typescript. Then there was a terrible amount of boilerplate and magic. So I wrote my own 75 line alternative that does a bare minimum. It’s basically just a useContext wrapper. And I haven’t looked back since.
Hi, I'm a Redux maintainer. FWIW, we specifically designed our official Redux Toolkit package to not only eliminate the general concerns about Redux "boilerplate" [0] [1], but also work great with TS. With our recommended RTK+TS usage patterns, a typical "slice reducer" file only needs to define a type for the reducer's state, and then define a case reducer as `(state, action: PayloadAction<MyData>`) [2], and that's it.
We've put a _lot_ of work into making sure that our library TS types minimize the amount of types that you have to write in your own app code.
Also, one of the reasons we now teach the React-Redux hooks API as default is that it's drastically easier to use the hooks with TS than the legacy `connect` API.
If you haven't had a chance to see what "modern Redux" looks like, I'd suggest going through our docs tutorials to see how we want people to learn and use Redux today [3]
RTK is what made redux usable for me. I can't imagine using redux without it. ^ Mark is also very active and responsive in the Reactiflux discord and has directly helped me and countless others clear any hurdles with its use
It may have had some value before RTK came out, but a lot of the opinions and approaches shown in its docs lead you to write _wayyyy_ too much code. For example, we specifically recommend _against_ writing TS unions for action object types [0].
RTK completely obsoletes `typesafe-actions`, and the TS usage patterns that we teach today should result in a pretty minimal set of types that you need to write in your own code.
For a small example see the RTK+TS template for Create-React-App [1]. If you want to see what a real app codebase can look like, the client app for my day job at Replay.io is OSS [2]. It's admittedly a somewhat messy codebase due to its long evolution and legacy (started as the FF DevTools codebase, copy-pasted, and we've been slowly migrating to RTK+TS and modernizing it), but files like [3] show how I would write a real slice reducer with RTK+TS.
Context and Redux are somewhat different tools and context doesn't necessarily solve the same problems as Redux. This article by the maintainer of Redux (acemarke) goes over why (looks like he replied to you as well) [0]. Have you tried Redux Toolkit as well? It cleans up a lot of the complexity of Redux and works well with TypeScript [1].
Basically I’m not saying redux is bad. Just that after years of using it for production software I concluded it’s still overkill for my needs.
IIRC Redux is also just an abstraction on top of Context. Fundamentally it gives you pseudo-global access to application state by being able to interact with it anywhere in the component tree below the context manager.
> Redux is also just an abstraction on top of Context
No, this is a very common but incorrect misunderstanding of how Redux works.
It's true that React-Redux does use context internally... but only to pass down the Redux store instance, _not_ the current state value.
Also, because Redux itself is separate from React, there's a lot of things you can do with it that are completely different than what Context does. _One_ bit of overlap is that both can be used to access state across the component tree, but Redux does much more than that.
I've been using Pullstate for medium-sized apps for a couple years and it's fantastic: uses Immer under the hood (so it's all immutable even if it look as it wasn't), it's API and mental model are vulgarly simple, and my impression is that it's quite performant. I also like that the store is disentangled from the component tree, which allows for plenty of flexibility. Totally recommended!
Using pullstate in production for a number of apps now; seconding this recommendation. Almost all of the complexity / mental overhead / boilerplate mentioned in this thread mostly goes away with pullstate.
(Sorry, I edited my comment and removed the game reference and optimization question before I saw your comment.)
I mean, sure but the DOM is not even a factor compared to the actual WebGL rendering going on. The DOM is only used for UI, menus and things like that. So the bottleneck would still not be React or how I handle state.
For me using xstate was a game changer. Pulling logic out into a state machine gives you so much clarity over your application logic. Also nice to have your business logic defined with something that is framework agnostic.
Once you see the light of state machine based UI's, it's immediately obvious and you never want to go back. The problem comes in convincing others to deal with the boilerplate. It's worth it, but it never really sinks in until you use it.
Yes! An explicit state machine is my desire for the UI development since the Delphi days of 1990s. It makes things so much more observable and sane.
Redux nudges you to build that state machine by hand, in the form of the reducers folder, around the centralized state. While elucidating, it's still a lot of boilerplate (which you can sort of factor out), and it's still not one clearly laid out entity.
Fun fact: one of the XState devs did a proof-of-concept showing how to use XState state machines as Redux reducers and integrate the side effects handling as a middleware:
Yeah, if you've got any particular use cases or ideas for how you'd like to use them, please put up a discussion thread in the RTK repo and let's talk!
I've had this same experience as well! Wrapping your head around the concepts at first can be a little tedious but it makes application logic and behavior far more predictable. The ability to visualize them is the icing on the cake as well and is a nice extra tool that eliminates the need for arrows and boxes in a sketchbook or a Google Drawing.
Most of these new client side state management libraries are incompatible with any form of SSR. If you see the documentation for Jotai/Zustand/Valtio, the solution is to avoid using with Next.js at all, or fall back to hacks using context+provider at which point the state manager becomes effectively redundant.
Isn’t SSR only really relevant when you need to optimize for SEO (which only applies to a subset of apps)? Seems like it doesn’t confer any other significant benefit.
I'm sorry people are downvoting, because you are correct.
SSR (by which I mean Next.js) is the most over-invested in JS tech of all time. It introduces a bunch of crummy DX which never pays for itself from a business standpoint.
SSR is only potentially useful for landing pages. In which case you should be using Wordpress or something like Wordpress so you aren't wasting dev resources on something a marketing team should be doing. (If your argument is that SSR is necessary for speed, I'm pretty sure exporting your Wordpress site to static HTML + CSS and using Cloudflare could more than make up for the difference.)
The one asterisk I'll add to this is that SSR in the form of the Remix framework may justify its own existence as it removes the need for an explicit API layer, which lets you skip a ton of boilerplate code. This is a big win. The fact that Remix uses SSR is just an implementation detail -- it's the DX that's actually valuable (although SSR fetishists will probably still like it, too.)
I don’t. Because it’s not worth the hassle, as explained in my comment.
If I did want to avoid it I could send html for a generic loading state in my base html template, which does not require ssr.
If you’re using Wordpress or static site generators for your public facing pages you won’t have the flash, and if your user is invested enough to have made it past the landing page the flash will be a non issue anywhere else.
Good. Hybrid SSR for everything is the most overengineered bullshit fad I've seen in front end development. The sooner noobs stop cargo culting Next JS the better.
Curious layman here (no web dev experience). I thought the main selling point of reactive programming was that it abstracted away the need to manage state. What problems, then, does a state management library solve in a reactive framework?
Reactive programming provides the capability to react to changes in state. You still need to hold that state somewhere, mutate the state from your reactive code, and control the lifetime/scope of that state. Managing state becomes more of a concern with reactive programming, but it also becomes very explicit; this is something that traditional MVC approaches try to hide, to the detriment of anyone trying to understand the data path of your application.
ELI5 why Excel can update all of the sheets from all the different data sources without overloading its user with “state management” thing and React can not?
I was negative in my deleted comment, but watched the video anyway (I remember skimming through it back in 2019).
It answers my question only tangentially, because it doesn’t touch state management per se. The key point was, what’s the fuss about React SM and why it must be explicit in it. The video basically says that it is a nonsense which may and should be avoided. I agree, and so still don’t get what ggp is talking about.
Most state management libraries help you deal with the problem of sharing state between multiple components. In modern React, most components are just functions, so this boils down to passing state through deep call stacks.
If you have a component A, which renders component B, which in turn renders component C, without a state management library you'd have to pass state from A to B to C (this is often called prop drilling). The longer the call stack, the more irritating this becomes.
State management libraries allow component C and component A to modify, and subscribe to a shared state store. Redux et al. are effectively the reactive version of global variables.
React already has a mechanism for this (Context). It's not the primary reason to use a state management library.
Any discussion that doesn't revolve around a difference between the two is akin to suggesting re-implementing a standard library call for literally no reason.
The commenter I was replying to wasn't inquiring about why people choose third-party state management libraries in lieu of the standard context api. In fact, he's not even a web developer.
He seemed to be wondering why you need a state management solution in the first place. If he had asked why people prefer Zustand, or Redux, or Recoil over React's built in context api then I would have replied accordingly.
I was largely expounding upon the linked article's first list item under the heading "The problems global state management libraries need to solve". The first item in that list begins with:
> Ability to read stored state from anywhere in the component tree. This is the most basic function of a state management library. It allows developers to persist their state in memory, and avoid the issues prop drilling has at scale.
I would argue that the primary function of state management libraries is still sharing state between components. Third-party libraries might expose a more performant, simpler, or more intuitive api, but that's still their primary purpose. Analogously, if a non-compiler developer asked why programming languages need memory management, you wouldn't delve into the differences between reference counting and tracing garbage collection.
As we know from computer science, there are two difficult problems: naming things and cache invalidation. State management is inherently difficult in web apps (I think it's inherently difficult for any complex application). They are effectively distributed systems with local caches and remote server data that need to be synced correctly in order to manage a user application at scale. That all has to align with how the presentation logic is built. It has to deal with the constraints of the web platform, etc.
Or you just manage state in the server. With https://inertiajs.com/ you can even afford to not to declare an API and still manage the state back there.
>Or you just manage state in the server. With https://inertiajs.com/ you can even afford to not to declare an API and still manage the state back there.
I'd like to share my approach to React state management, because after a decent amount of industry experience I've stumbled onto a solution I find to be very excellent.
Firstly, I use the open source library Pullstate[1], which I find to be as effective as any alternative but _far, far_ simpler to understand and use.
All my components are functional components. For any state that _only_ that component needs, of course I simply use the useState hook.
When things need to be shared among multiple components, I create a Pullstate store - for instance, for an app I'm working on now I have a UiStateStore like so:
type UiStateStore = {
isSidebarOpen: boolean;
// etc
};
export const uiStateStore = new Store<UiStateStore>({
isSidebarOpen: false,
});
Then, in the components that need to know if the sidebar is open, I import this store and use its state hook _the exact same as the normal useState hook_, like so:
And automatically, any components using uiStateStore.useState and watching the isSidebarOpen property will get updated, exactly the same as the normal useState hook - just shared.
It's so dead simple and has made complex app-building so much easier for me.
By comparison, Redux in my experience has a harder learning curve with a lot of unnecessary pieces and boilerplate - it seems crazy to me that people think it's a great solution and it feels like people have convinced themselves all those pieces are "necessary", where in my experience they are anything but.
The one caveat is that if I have a component with many handlers, e.g. onClick, onMouseMove, onContextMenu, onMouseLeave, etc (and in some cases I do), components can get bloated. I haven't found a fix to that yet. But that's more an inherent issue with react than anything to do with state management.
You might like the atomic state management libraries mentioned in the article, they simplify things to another level because they sort of remove the concept of a store entirely. For example in Recoil I would do this:
This is because UI programming is inherently extremely complex, especially compared to something like a stateless API tier running in AWS. It’s stateful software deployed to countless different runtimes on hardware you don’t control. Instead of a smattering of API routes handling well-structured semantic datatypes like `POST /burgers?pickles=false`, input comes in the form of arbitrary UI events from various input devices like pens, keyboards, mice, etc that must be interpreted to have a semantic meaning. Often there are hundreds of API routes (event handling elements) on screen at once in a complex application. UI engineers are perpetually frustrated by this complexity, and so some small percentage look for better ways to handle these issues.
Of course it’s best to avoid UI programming entirely, but in many domains it’s necessary.
There are definitely examples of UI programming that is more complex than certain APIs that largely perform basic CRUD operations, but I'd strongly disagree either is inherently more complex than the other, it's just that human behaviours and preferences are messy and unpredictable, which means conceptually "simple and elegant" UIs are often not what users actually enjoy using, and this inevitably has impacts at the code level. APIs don't have to designed to deal with human eccentricities as they're written for developers, and we aren't like normal people.
I find the big difference for me when building an API vs working on a UI is the testing.
I think backend APIs tend to be much easier to test than UIs.
For the backend, it's mostly Request in Response out, check they look good and the side effects (database, caches, queues, etc.) are performed etc.
For the UI you have a lot of async stuff going on and writing non-flakey tests is tricky and verbose with the current tooling - using react testing library which is better than enzyme, but still harder than testing a Django endpoint.
Absolutely - realistically you never can automate UI tests fully, as there's no way of describing all the things humans "expect" to experience while using a UI.
Even when you can, the effort involved in creating and maintaining the tests and confirming that the failures are genuine often doesn't justify the benefits.
Then you've never written APIs that have to do super complex calculations or process huge amounts of data efficiently and reliably.
By far the most complex code I've had to deal with has been "back-end engine" type code, even if it was technically part of a desktop application (but wasn't dealing with user interactions).
My point isn't that "all UI is more complex than all API", but that the constraints and nature of UI tend towards complexity (especially incidental complexity) at a higher rate than API programming. As you observed in a previous post, UI engineers struggle to use techniques like end-to-end testing that API engineers use with relative ease. Why is that? My argument is that engineers working in the API space more completely control their problem space in general. Some examples:
- Want to rigorously specify the interface of an application? In API space, we can use an IDL like Protobuf to write a contract of exact input and output types. In UI space, the best we have is plain-english text language called Gherkin.
- Want to record all inputs and outputs made to application to verify them against new version? With network APIs, we can often record all app-level IO in production using network capture; often with negligible performance impact. To do best possible job for deterministic replay, we can actually hook into our CPU if we try hard. UI? Sorry, again we're running on a customer device, unlikely we can apply these technique. At best we may be able to capture some UI change traces on a few devices with spare bandwidth and CPU.
- Having performance troubles? In API space, feel free to buy more hardware, or optimize more tightly to the existing homogenous platform. Oh, maybe application takes a few minutes to boot up and "warm up"; this is fine, we'll use blue-green deploy and replay a bit of the read-only traffic we captured earlier... In UI space? We probably need to consider 2-3 versions of ~5 different operating systems, running on myriad heterogenous hardware, to say nothing of browser issues; where every few weeks Google releases a new runtime with the potential of performance regressions. Slow to boot? Say goodbye to users; we need the app to paint in 1.7 seconds and be interactive in 3.8 seconds, on demand at any time of day.
- Have a problem with teams stepping on each others toes? In API space, you can try to deploy two services (not saying this is good idea, but plenty of people see it as solution to this problem); end users won't know the difference. Try to deploy two separate apps in UI land? End users will be sad.
- Need to handle large amount of data in API? We have low latency access to storage APIs that scale to petabytes. In a UI? We'll need to build an API first to mediate streaming access to that data and store it for us, since we can probably only store a few hundred megabytes locally before we run out of disk. In both API & UI we need to add a local cache, but how do we test local cache provides good experience? This takes us back to testing topic... where API seems to be easier.
An argument from anecdotal experience isn't worth much in this territory; I can easily same the same thing in reverse: the most complex code I've had to deal with is threading the very narrow constraints of contentEditable programming for Notion's rich text editor. Now let's add a dash of "no true scotsman" as well: You must have never written any such complex UI! See? I think this is an unsatisfying approach to the issue (and not one I make in earnest).
I don't disagree with anything you've written there, but I still disagree that UI code is typically or inherently more complex (or harder to write well) than backend code. I doubt it's anything you can even prove one way or another.
But while the nature of inputs and outputs for UI code tends to be nebulous and somewhat poorly defined, it's hard to argue the range of possible inputs compares with code that has to process massive datasets.
Bullshit. In 1995 people were creating desktop UIs order of magnitude more complex than today's anemic webapps, without any of the braindamage-inducing stuff that's happening in web UI development.
Win 95 was a toy, NT was getting there. Apps were being ported quickly to NT4.
I do remember using Maya on Irix in '97 or so and it was already pretty amazing. It was built off earlier applications like power animimator, which was started in '88:
Excel is quite complex, but the Excel of 2022 is far more complex than the Excel of 1995, and part of it exists on the web itself, so Excel '22 would be an invalidation of your argument that '95 apps were more complex (as well as other modern versions of the other apps).
One major difference in 1995 applications is that almost none of them were collaborative or synced with a remote server. They were all isolated applications that worked entirely in a local context. They could be programmed to a single target platform on perhaps one to three screen resolutions (640x480, 800x600 and 1024x768). They could all work under the assumption that one style of input was being used (mouse and/or keyboard). They all could use simple built-in dropdown context menus. They could all render to a canvas context graphics with much of the heavy lifting assumed by the operating system itself.
We certainly had network programming in the 90s. It was just that apps didn't use it unless needed. SGI res was typically 1280x1024 and Wacom tablets were common in studio/professional settings.
Apps didn't use it because it wasn't practical or in high demand yet. People were just getting used to checking email and most people were only intermittently online via dial up. Collaborative apps would take another 8 or 9 years to really start to take off.
We had T-1 internet to every desk in ‘94 and I used dozens of networking apps from archie to veronica. Probably used CU SeeMe before a browser. Though Mosaic dropped around that time.
Advanced collaboration still in the future, this era was more like chat and file transfer. I think you could mark up Word docs from a network drive at some point.
Ultimately, successful products are determined by customers and by product managers and their executives, and it turns out there is no one-size-fits-all approach to even basic UI programming because everyone has an opinion.
Not to mention, as an example touch became a widespread new UI paradigm in the last 10-20 years.
simple problems easily becomes complex in mainstream contexts.. due to bazaar like soil. NASA had to solve one immensely hard problem but they owned the context. Vertical integration if you will.
I disagree, I think UI programming is inherently quite simple. Especially with a paradigm like immediate-mode GUIs in my opinion. I believe the complexity comes from the fact that the DOM is a poor API for creating user interfaces, which ultimately inspired developers to create frameworks that work in a more “immediate-mode” style way.
1. There is no standard GUI library, as per article. A GUI can be as arbitrarily complex as you wish, and every different frontend system is a unique GUI system deployed in JavaScript, usually using HTML for the view drawing primitives* and events for input. The complexities of GUI library design bubble up to developers who are implementing their own tweaks or combinations of a GUI library, usually with a huge amount of “needless” variation. Application developers should ideally never have to be making choices about internals of a GUI system, yet the core of most articles comparing frontend frameworks is discussing GUI internals.
2. There is no golden standard for tooling. Everyone has their pet variations on how to deploy to JavaScript, CSS, and HTML.
3. Back end choice. Huge variation.
As a developer we get APIs on the edges, but we make our own spaghetti to join everything how we wish because there is not one or two standard library/framework choices, and we have the power to do what we will. I developed my own 100% custom component framework because I could write one that suited us far better than what was available at the time (OSS or commercial). Browser variation used to be a huge driver for complexity, but is far less so now.
At least in AI, what happened to lisp was being replaced by Python. What do you presume happens to JS in this case? WASM frameworks are likely to have the same problems. Stuff like Phoenix Liveview can be rather standard but limited in scope.
It makes me smile that elsewhere in this thread there's people saying "I've been using <library> and state management is now super easy." when you know in two months people will be saying that library is over-engineered and someone will suggest to try the new panacea that will solve all of your problems, for real this time.
Thanks so much for the link! I tried the original and bounced off the style. It seemed like something that might be fun and useful if I invested enough effort in reading it, but I just bounced off.
But with the translation, I just devoured and appreciated it.
Has React become too big for people to not consider other alternatives? I used to be a big React advocate but realised that it is getting too bloated with every new release. Have been looking into Svelte and it is turning out to be a breath of fresh air compared to whatever React is culminating to be.
The problem with redux is that it's a simple, extremely powerful tool for creating a CQRS architecture on the front end but very few people treat it that way. Instead they bolt on things like rtk and add yet another layer of abstraction over their project. In those cases it's almost always better to just use something small and simple like Zustand, which the article called out.
For bigger projects, custom middleware is where the magic is. If you don't understand redux well enough to write your own middleware (it's not very complex, it's just under documented for some reason) then it's better to use one of the other, simpler abstractions instead of adding one abstraction (redux) then piling on more abstractions to make it usable.
Pretty sure you and I debated "vanilla Redux" vs RTK in a thread a couple years ago, but I'll link my most recent explanations of why RTK is the right way to use Redux today:
Also, note that RTK has a new "listener" middleware that simplifies the process of "run this code when some action is dispatched". You can certainly still write a completely custom middleware if you _want_ to, but the listener middleware handles that work for you.
I get it, you're _really_ invested in RTK but man, I just find it to be the right answer to the wrong question.
Since we last talked I have worked on projects where rtk was used because the devs didn't really grok redux and so far that's the best use case I've run across. That said, now when I run across situations like that I tend to guide clients to simpler state management solutions instead of piling on another layer of abstraction.
Have you considered something like "selector based side effects"? https://github.com/mikew/redux-easy-mode#selector-based-side.... The difference is most side effects are based around when an action is dispatched, but I wanted to know when things _change_.
It's possible to do the same thing with a react component + useEffect, but I thought a pure-redux solution would better fit the spirit of Redux.
Yes, you _can_ actually do that with the listener middleware! We specifically designed that to be possible.
When you add a listener entry, there are four options to specify how the middleware knows when to run that listener: 1) `type`: an action type string; 2) `actionCreator`: an RTK action creator function like `todoAdded`; 3) `matcher`: an `(action: AnyAction): action is MyAction => boolean` type guard; and 4) `predicate`: an `(action, currState, prevState) => boolean` function.
In all cases, the middleware loops over all entries every time an action is dispatched, and checks to see if any entry is interested based on those comparisons. For the first three, it's just "does this action type match".
_But_ the `predicate` callback receives `currState` and `prevState` as arguments. That means that you can write checks to see if "this state field has changed due to the action":
FWIW, skimming the repo you linked (which I assume is your package), I'm pretty sure RTK either does the same things already (creating actions and reducers) or has equivalent solutions (listeners for "selector effects", `createAsyncThunk` for the "async middleware", RTK Query for data fetching).
I've often found those that complain about these buzzwords are the people who do not actually do any frontend engineering. There are buzzwords in every language and every library. It's not an indictment of complexity (which exists outside of any one language or library), it is simply the terms of the trade of that particular technology.
I don't know why but front-end terminology always makes me cringe. I know it's useful jargon. I don't know why I have a visceral distaste for them. Maybe because it feels like taking a small, simple thing and making it seem like it's something more.
But you're right. I haven't done much front-end at large scales. Only at one employer for a short while.
> Maybe because it feels like taking a small, simple thing and making it seem like it's something more.
Perhaps it makes more sense to treat frontend engineering as thick client desktop development of yore. Websites are no longer small, simple things, they are now the primary apps that many people use (through the broswer and especially through Electron), so there needs to be sufficient tooling around managing that complexity.
I get where you're coming from and while I still fucking loathe the term "isomorphic" in my web development, I feel that an application of basic engineering practices go a long way in development of even modestly-complex websites.
I used to get worked up about front end devs calling themselves "engineers" but after 12 years in the trade, I think my dream team of web devs consists of more industrial engineers than CS grads.
I understand all of these words and they are important to my daily workflow because I do serious frontend development. I'm not trying to offend you but your comment simply indicates that you do no serious frontend development.
Zustand is a lot more popular than the comments or the article implies. I see it quite heavily used amongst Netflix engineers.
That being said, prop drilling was made more of an issue than it really is, especially considering the boilerplate needed for state management libraries like Redux.
But if there does need to be a global store, I usually reach for zustand as the API is probably the easiest out of the ones mentioned.
For anyone who has not had the pleasure of working with these simpler “atom-based” state management libraries, I would implore you to try Jotai (https://jotai.org/), which is mentioned in the article.
Jotai’s atomic model and ease of use has made writing complex React applications far more joyful for me.
I didn't get the memo apparently. I still think Redux+Thunks+Selectors+Sagas is great and that combination solves all my problems. Boilerplate isn't negligible but relatively tame. There's a straightforward way how to model state, and I don't have to put too much logic (especially async) into components.
Yeah I've always thought the complaints about boilerplate were misguided. Whatever the cost in boilerplate implementation was more than made up for by having predictable, deterministic UI output. That being said, I think hooks did open up the state management design space quite a bit and classic redux style state management doesn't quite line up with it. Still totally valid as an architecture, though, in my opinion.
Eh, as maintainer, I'd say the complaints about "boilerplate" were generally pretty accurate :)
As I've talked about in a couple posts and presentations [0] [1], there's a distinction between "inherent" and "incidental" complexity. Dispatching actions and using reducers is "inherent" - it's part of Redux, and you can't remove that because then it's not Redux. But things like writing `const ADD_TODO = "ADD_TODO"`, object spreads, etc, aren't _necessary_ for using Redux - they're inherent complexity.
Which is why we wrote Redux Toolkit to fix all that incidental complexity :)
Redux still won't ever be as few lines of code as various other libs, but writing Redux code today is much simpler than it was previously... and yes, the _benefits_ of Redux (predictable code, debugging, separation of state updates, middleware) are still entirely valuable.
I'm pretty sad this is the point we are at with React frontend development, the library is becoming much larger than it's original scope and it's getting bloated while not solving the essential problems that happen. In my opinion the best solution we have right now for frontend JS development is to not use JS or JSX at all and instead use a DSL such as Svelte, this way the compiler abstracts all complication and there is no application bundle size bloat. Obviously we have Preact (which solves react-dom bloat), MobX (which solves state management), etc. which are all great tools but the problem is that every tech stack is different and the more the core technology can do without getting more bloated the easier it is to introduce new developers to a codebase and keep up with changes.
But does it solve the problem of growing state which never gets GC'd when components which used to use it are gone? Does it solve the problem of only redrawing the required minimum (beside the normal VDOM approach)?
I'm asking as someone not knowledgeable enough about Svelte.
> Does it solve the problem of only redrawing the required minimum (beside the normal VDOM approach)?
Yes, in fact I think it's actually pretty hard _not_ to redraw the bare minimum LOL. If you want to know more about Svelte (even if you're not looking to develop with it) I HIGHLY recommend listening to this presentation called "Rethinking reactivity" by Rich Harris (the creator) https://youtu.be/AdNJ3fydeao
> But does it solve the problem of growing state which never gets GC'd when components which used to use it are gone?
I think the answer to the question is yes. Although the problem I'm speaking mostly about state management is the source of truth problem. Svelte provides a global store to store data, and data stored on local components are just _variables_ (no useState or hooks or anything complicated) where the Svelte compiler handles everything.
Something else I'm really eyeing right now is SolidJS which takes a very similar approach to Svelte (compiler instead of library) for frontend development but provides an API that's very familiar to React developers so there's not much of a learning curve (although Svelte has a very easy learning curve too).
It riffs on the old, simple APIs of React but uses pure HTML, CSS, and JavaScript w/o any trickery (I'm also hardcore about not changing the component API so WYSIWYG).
The bonus is that it's a part of a full-stack framework (the UI framework has a Node.js counterpart), so wiring up a full app is near-effortless.
Looks pretty interesting, I'm always kind of bothered about the lack of (true) full-stack frameworks for the NodeJS ecosystem that handles accounts, database, frontend, API, etc. like we have for Python with Django or C# with .NET.
How is React not solving essential problems? Suspense solves a pretty essential issue. Also Svelte can quickly increase in bundle size and if you have enough components, it can become bigger than the equivalent in other frameworks: https://github.com/yyx990803/vue-svelte-size-analysis.
The first version of React was released on 2013, it took almost 10 years for Suspense to exist (we _just_ got it now with React 18), that's what I'm talking about. Even functional components and hooks took a lot of time from them get and implement the idea after they tried to use ES classes and made everything much harder to manage. Context also isn't perfect, I like it but the redraw performance is not amazing and doesn't scale at all to bigger applications.
This is an interesting comparison I haven't seen before, I wonder if it's true for a complete application using some lib for state management, routing, etc. and if this isn't just a kind of cherry picked example. Thanks for showing this though.
I’ve been using redux ever since it came out and a home rolled flux architecture before that. I gotta say I’ve been using zustand lately and it feels so effortless compared to other solutions. I couldn’t imagine going back to redux. Although redux slices feel a little bit closer.
Feedback for the author (who seems to have also made the submission): The content is good, but there are too many basic typos and grammatical mistakes in the article, making it really hard to follow at times. Not sure if it was written in another language in the first place. Please run your article through some spellchecker and grammar checker the next time, not to mention finding somebody to proofread it.
I do find redux super helpful for undo/redo (as mentioned in the article) and have not found a similar library for another state management solution that can do quite what I need there.
I've used Redux but I've never heard of any of these alternatives. I have no doubt they're popular, but what is it about frontend that makes everyone reinvent the wheel every five years? Everything from the tooling to the tiny details somehow expires and gets recreated in a similar-but-not-similar-enough way that keeps the ecosystem in a constant state of flux.
Is it the lack of platform API support? Is it the community trying to make everyone a library developer? What's wrong with frontend?
I think it's because redux is quite painful to use with very modern apps. Most of the time you need something like `redux-saga` or `redux-thunk` to deal with async side effects. I'm not sure which is the most popular today, but sagas are based on generators and trying to use those with typescript is very very painful and the underlying issue [1] is marked as a design limitation in TS itself.
In addition, if you want to have your app load as smaller chunks rather than a single large bundle, you need to be careful to ensure that things work even all the backing reducers aren't yet loaded.
You need redux saga because api interactions can be complex.
I need to update a remote resource. But wait! Sometimes that can fail. I need to capture the failure logic and make appropriate ui changes. There are 4 types of errors and they require doing some library logic to figure out what to display.
But wait! I want to wait 250ms before triggering any state updates, or else the ui transitions will feel buggy.
Oh, and product now wants to make sure we capture some random third party tracking event in between specific state changes. The ui MUST NOT change until the tracking call succeeds.
This and more is trivial in redux saga, and you can write tests around it.
We specifically moved off thunks because the capricious whims of changing network calls with logic to support it is so much easier in redux saga. Stuff just works. You can handle every edge case.
And now there are multiple deployed production apps with real users, so it’s almost certainly never going to be replaced. The cost would be enormous.
Yeah, I'm not questioning whether you _can_ do things with sagas, and there's definitely cases where their capabilities are valuable.
But as I put it in my "Evolution" talk: "Sagas are like a chainsaw. Really powerful, and really dangerous. If you actually _need_ that power, great! But most of the time you don't actually _need_ to use a chainsaw on a daily basis."
As the sibling comment said, a lot of times sagas really do end up as spaghetti code, largely because they are so event-driven, and with all the use of generator functions that can make debugging hard. (Ironically, the original Flux Architecture was created to _avoid_ the problems of Backbone-style event triggers causing events to ricochet around the app without a way to understand how things would update in response, and sagas can end up recreating that problem with Redux apps.)
I was called in to rescue a redux-saga based project that had gone badly off the rails. It was one of the worst codebases I've ever had to work with in many years of coding. Stack traces were useless and debugging with anything higher level than log statements was impossible.
Apparently you don't need to debug if you're a good programmer. This explains why many modern tech stacks (JS, Scala, Kotlin, ...) have a horrible debugging experience.
Saga is so heavy and overly complex and you don't really get much out of it if you understand how middleware works. There's really no reason to add a huge dependency to your project.
You can do all of that pretty easily with a vanilla redux.
In my experience the experience with Redux Toolkit is awesome, it's essentially an opinionated way to use Redux for modern web applications and I believe they really hit the mark when it comes to reducing boilerplate and the need for extra deps. Although I'm still not a big fan and prefer to use built-in tools of React to manage state when possible (now that we have Context, Suspense, etc.)
> In addition, if you want to have your app load as smaller chunks rather than a single large bundle, you need to be careful to ensure that things work even all the backing reducers aren't yet loaded.
In the decade old application we use redux saga in at work, a large portion of our 7MB minified/gzipped main chunk is redux handlers. It'd be nice if there was a relatively simple way to not synchronously load all the reducers up front.
There have been some different community packages for helping with that process, but some of them seem to have become outdated (only worked with React-Redux v5, etc). I did see a new one at https://github.com/fostyfost/redux-eggs that seemed like it had potential, but I haven't had a chance to try any of them myself.
I also once saw someone play around with the idea of using React's still-not-technically-final Suspense support to help ensure that a lazy-loaded component that relies on a code-split reducer doesn't actually get rendered until that reducer's state is available. Don't have the link handy atm, but if someone wants it ping me and I can go figure out where that was described.
Also, the new RTK "listener" middleware was specifically designed to replace almost all saga usages, and you can dynamically add more listeners at runtime via dispatch an `addListener()` action:
More of this. Middleware is simpler to grok than reducers and provides a clean, universal solution to async. You don’t need sagas, just dispatch events at meaningful points in time, like when a request is initiated and then another action when it completes. Pretty sure Dan Abramov taught this approach years ago in an egghead tutorial, if it takes hearing it from the horse’s mouth to get you to consider not just throwing more libraries at it
I considered something like this last night as I stay up to 6am working on a new UI library. My conclusion was in part because it's easy. The goal is well defined. I got the first widget working thought I'd call it a day (a night) but then it was so much fun to just implement the next widget mostly because very little design iteration is needed. The problem being solved is well known so each thing to tick off my UI lib was only a few minutes. Next thing I know it's 6am.
I think the same is true of 3D libraries. The real work of making an app that does 3D is not the 3D engine, it's everything else (UX or GameDev). The 3D part has well known goals and solutions so it feels like you're making a ton of progress, ie, it's fun to make. Much easier than deciding harder things like which features your app should have and how they should work.
Great insight, this makes a lot of sense. And on top of this, it SEEMS impressive. You made a whole framework! Wow! Plus there’s a chance of widespread adoption—-so, well-defined goals, fun to work on, seems impressive, potentially very high payoff. No wonder there are so many.
It's the case in the backend and devops world too. But like for Redux you probably chose some decent libraries and tools at the beginning and sticked to it. If you start looking around you will see there are alternatives to everything you use, and even maybe that most of the industry moved to those alternatives in the past 5 years.
How many ways are there to build an API? In Python: Falcon, Flask, Django, FastAPI and so on. Just as many choices in Java, Go, Node, <insert your favorite language>.
"Necessity is the mother of invention" comes to play here. The reality is that the community hypes certain tools, but in practice, they tend to have gotchas buried far beyond the surface level demos and documentation. The problem with that is that you only figure that out after committing to those tools and using them. This leads to tool abandonment, or in some cases, developers taking a swing at their own version. What they come up with is more often than not a rehash of the old ideas but lacking any "why" or long-term vision.
The other one is employability by obscurity. An old grifter trick is to make something far more complicated than it needs to be as a means to guarantee employment (both on the tool developer side and the end-user side). For the tool developer, the more they can twist and turn their tool (introducing novelty and potentially confusion), the more sought-after their services will be. For the end-developer, they can hold a "monopoly on intelligence" and become difficult to replace in a company because they're the only one that understands that thing. Couple this with the conference talk circuit where you see the same people constantly pitching some new-fangled widget every year and you realize the goal isn't to solve the problem, it's to get paid to look like you're solving the problem.
Another problem is inexperience. A developer might have just enough experience to feel confident at the code-level, but they lack the practical experience to let them know why a certain pattern is incorrect. Assuming that they never get that practical experience, they will continue to iterate the tool into an utter mess or deprecation.
I can assure you nobody at my workplaces cares what tool I'm using. I'm the only developer on my team. I am still looking for better state management solutions.
This is the age-old question. You're using Redux now, but I'm sure some JQuery/Angular/Knockout/etc dev said the same thing about what you're using back then.
The front-end is crazy and I don't think there's an obvious answer of why it's such a developmental disaster. It's easy to say it's run by script kiddies or the barrier to entry is too easy, but there are a lot of smart devs working on the top libraries. The culture just ended up this way. Whatever is driving it probably will never stop though. Enjoy the ride. lol
Frontend end is crazy because many UIs that do useful things are inherently quite complex. UIs, by definition, are how people interact with software to solve problems and accomplish goals. The problems we solve and the goals we achieve with software are continually evolving and expanding as more and more software is created.
The people-software interfaces for many of the commonly used pieces of software will always be complex as we will always demand a lot of our software, up to the limit that can be provided by the available tools for building these apps. As our capabilities for building better software grow, so too will the demands of the users.
If you aren't buying this argument, sit down and map out every single possible state and every possible event (user interactions, etc) of a moderately-sized app that you use regularly. Or even just one page of that app. There's a lot going on. It feels simpler than it is when we are using it because well-designed applications become invisible to the users, especially as we become familiar with them. They "just work".
TL;DR Reality is messy and complicated. And so is the software we build as well.
Agree. I am working on a side-project and using XState for local state management, and Jotai (with Immer) to send that state to various components in my app. It is working really well and mitigating unnecessary rerenders.
More moving parts than I might like, but they play together nicely and I haven't hit a single wall yet.
I'm just tired of the redundant reinvention of new "terms" for literally everything in JS. I don't want to know what an "atom" or a "proxy" or a "thunk" is. These are meaningless abstractions that simplify down to a store and a callback. Stop inventing terms to make yourself feel smart.
FWIW, the term "thunk" is a long-standing CS term that long predates Redux [0] [1] [2].
In fact, my first job back in 2008 involved a C++-based emulator/VM framework, and the devs used "thunk" to refer to jumping from the original program binary out to altered/replacement code written as C++ to add additional behavior or replace functionality.
"Proxy" is also a long-standing term as well that describes wrapping or replacing functionality of a system, which is why it's used for HTTP servers and why it got used for a new JS capability in the ES2015 language spec.
I find it interesting that across a decade and countless frameworks from jQuery, meteor, angular, and now react the core problem is still and somehow progressively worse than ever...state.
I find it strange this is still such a hard problem to solve in an environment with a slew of options for key value global, local, and remote storage
It's easier to solve it than to agree on what is the solution and to keep it. The more alternatives to redux there are, the higher the chance you get thrown into projects with some lesser known framework, maybe not even used to its best.
I remember there being about 5 layers of abstraction to using Redux. Every time you change something have to update 5 or 6 files. It really gave me Angular 1 vibes.
One thing I learnt from all these JS libraries and Node is that nothing is for free. If it seems too easy, then something gonna bite you in the ass down the line.
I’ve not seen overmindjs mentioned on hacker news. We use it in our React app and my students who wrote the code liked it a lot… maybe others have advanced enough by now to be on par with or better than overmindjs.
It's annoying that they now reinvent yet another old thing (proxy based dependency tracking). This seems to be a pattern in the React community. These libraries are great but they offer nothing new comparing to mobx, which started even before redux became ridiculously popular.
I read the whole article, and I found it quite interesting and it has a lot of good references that I've encountered in the past (for example Zombie Children etc...)
However I find it very difficult to read: there's so much text and not many example to illustrate.
This is one reason I like to use ClojureScript + Reagent for React(Native) development. Clojure atoms make a perfect in app db, that can hold anything, have good performance, and lots of "batteries included" functionality for reading and writing nested data.
I find the whole state management situation pretty frustrating. It seems like everybody agrees that you should only introduce tools like Redux and Vuex when you actually need it, but what I observe is that nearly everybody reflexively reaches for these tools regardless of whether they need them. I see single page applications with 5 pieces of state, fully engineered with Redux.
Then the outcome is that about 50% of the beauty of reactive development is lost because the whole point was that your UI representation became isomorphic with your state. In doing that a whole class of bugs went away. Then we inject state management and suddenly all those bugs are back again, living in layers and layers of boilerplate code generated to satisfy the patterns required by the state management code.
tldr; the whole reason I started using a reactive UI framework was to eliminate complexity. When you bring it back in any form then you destroyed the main value proposition.
Absolutely great article. One thing I always see missing is why front-end applications can't use sessionStorage or localStorage? Why is this generally frowned upon?
What these state management solutions usually attempt to address is reactivity.
When the <UserLoginModal /> finishes its login process and sends a bunch of data about the newly logged-in user to the global store, I'd like for some N other components in the tree to consume that new information. State management solutions help relay that new information down the tree to the components that need it.
Local storage and session storage can persist data, but have no way of informing components that something has changed without implementing a setState() and listener/callback system, at which point you've recreated yet another state management system.
Most apps actually do use localStorage as a persist layer. But it's not a replacement for state libraries because it's just a place to store strings, and that's it. A state management library is so much more than a scratchpad.
I'd assume because it's often abused? There are perfectly legitimate reasons to use either and I've been technical lead on React projects where the decision was made to use them. In at least one case it was a known "shortcut" as we didn't have time to properly develop server-side persistence and local storage was "good enough" for 95% of use cases. And session storage is needed to allow page refreshes without cookies.
The state that remains is often simple enough that plain old React state management with useState/useReducer is sufficient. A lot of state can be local, and local state is easy to handle with the built-in tools of React. And global state is only really an issue if it changes often. For mostly static state like information about the current user, theme or UI settings and similar kinds of state using React Context works perfectly fine.
Of course this depends heavily on the kind of web application you write, if your application is closer to Photoshop in the browser than a simple CRUD app you probably can make good use of more complex state management libraries.