- Redux is cool but there’s so much decoupling that it gets hard to read the code. New hires would take too long to start, TS types got crazy, it was too much.
- XState is very good for state machines… but you barely ever need that level of control. Most of the time understanding it ends up being overhead. We’ve had maybe one good case for using it in about 10 years.
- React’s contexts are minimum overhead and work very well with “jump to definition” functionality and TS types. You can choose to use a reducer if you want, or skip it if it isn’t appropriate.
YMMV of course. This is just us.
I had senior developers, who have worked with RTK before, misusing simple features such as the "createSlice" API.
By contrast, I have never once sat down with a junior to walk through React's Context. I assume, they just read the docs and were good to go.
FWIW I see folks misusing / misunderstanding Context on a near-daily basis, mostly around misunderstanding what components will re-render and when. That's part of what led me to write my "Guide to React Rendering Behavior" post, to clarify when/why/how React renders:
- https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-...
I've read your blog post, and I might be missing something, but you do not seem to be highlighting any re-rendering issues specific to context. Juniors will have to learn how React works anyway. Context or not.
- Thinking that if you change a field in the context value only components that read that field will re-render
- Thinking that _only_ the components that consume the context will re-render, not realizing that normal recursive rendering behavior resumes from there
- Not realizing that updating a context requires a `setState` call in the parent that renders the `<MyContext.Provider>`, and you can easily end up in a situation where the _entire_ component tree renders just because of default recursive rendering
Can you point to some examples of the "useless nesting" and "weird constructs" you're referring to? I'm curious to see what's happening, and if there's any docs improvements we can add to help provide guidance.
> Can you point to some examples of the "useless nesting" and "weird constructs" you're referring to?
Example: Create a slice with a list of items as initial state and truncate it in a reducer.
Redux is nice, but it’s be nice to have a very minimal framework for adding a global and local states via hooks and syncing their data structures to the indexed db
We have an object graph (updated via web socket subscriptions) and then twist and turn slices of that data on the way up to the components. I hardly think about the fact that mobx is there and instead just think in terms of reshaping data for display.
Success with MobX on a project. I worked as lead picking up an existing code base - none of us had any MobX experience so came in fresh.
My employer was kind enough to offer up FrontendMasters subscriptions - I watched a MobX course there over a couple of nights - link: https://frontendmasters.com/courses/redux-mobx/
I enjoyed it! Got the job done, no scaling issues
We’re using redux (with a copious amount of selectors) and it’s the only reason I have a little bit of confidence where all our data is.
Now I could be wetting but it's hard to have these sorts of discussions on the abstract.
Can you name examples of such state?
I don't get the hesitation to use redux. I've used it on every single FE app I've worked on and never regretted it. I can make redux do exactly what I want it to do and with the power we get from memoization of selectors via reselect, there really is nothing that beats it.
I'm also convinced that most people praising react-query are hugely missing out on the level of control you get with redux. Further, coupling side-effects -- e.g. fetching data from api -- with react component lifecycles is wrought with awkward edge-cases that you don't have to deal with in redux.
Maybe I'm just not smart enough, but I've been working with Redux for a few months and I get more confused every time I look at it, despite hours of videos and documentation and tutorials. It's just not at all clear to me what it's doing behind the scenes and how the typings get transfered and what's happening during async operations.
I don't think I'd ever want to use Redux again after this experience. I'd much rather deal with performance problems than the layers of unreadable abstractions that Redux uses...
I also heavily leverage https://github.com/neurosnap/robodux to treat redux as a database.
At the end of the day, redux is an event emitter (pub/sub) with a single object that stores all of your state that multiple components need to reuse.
If you haven't seen RTK, we designed it to make Redux a lot simpler to learn and use:
- https://redux.js.org/introduction/why-rtk-is-redux-today
- https://redux.js.org/tutorials/essentials/part-2-app-structu...
- https://blog.isquaredsoftware.com/2022/06/presentations-mode...
Not that with just react contexts, you can chose to split them up instead of or in addition to employing memoization
Does memoizing the state help?
It doesn't. Updating state in a component triggers the re-rendering of all its children. This is what they are referring to. You can stop that propagating effect as usual by wrapping direct descendants in React.memo().
Be aware, that an anonymous list of components will show up as "re-rendering" in the browser tools. You can verify that this is not(!) the case by looking at the profiler instead.
Additionally, if you cannot tolerate even just one component re-rendering, you can simply use React.useSynExternalStore() instead of Context.
Context does not trigger the re-rendering of all children. Updating state does. And you prevent that propagating effect by wrapping direct descendants in React.memo().
- https://redux.js.org/tutorials/essentials/part-2-app-structu...
- https://redux.js.org/tutorials/typescript-quick-start#define...
It’s just rare enough that “prefer contexts” is a good rule. Simpler, easier to understand code, with no Redux nonsense.
React Router moved to contexts some number of versions ago...as I look into libraries for React, I see most leveraging Contexts. I've had success with it over the past year and appreciate your affirmation
Add anything you want to keep to it, store it in the window with window.app = {}
You can mix local storage in with it too if you want to write a class that checks its existence with a key.
Why must state management need a complex solution?
I’m sick of frameworks and modes where they abstract away the simplicity of javascript in favor of their way which is very economically driven.
Some state does change a lot, and for that: sure. But if you just have currently logged in user or some such (which is quite often the case): meh, it just seems like a complex way to set a global variable.
This week I spent some time removing the Vuex state management in favour of window globals and it made everything a lot simpler and the bundle size a lot smaller (the other option would be to migrate to Pinia, which is significantly smaller than Vuex, but then I'd probably have to update to the next incompatible version in 2 years, and this seemed more robust).
However, you could attach event handlers for state change monitoring & propagation.
I’m curious to know how scalable an object GP described would be for UI state (assuming it needs monitoring & propagating)
Redux state is “just an object” that does shallow comparisons to trigger re-renders
window.getState(); window.setState();
You could even use document.querySelectorAll() to go through all the elements on your page, and update them based off values in the state, whenever window.setState() is called. This is basically all front-end frameworks in a nutshell. You've invented reactive programming in the browser.
But for a simple thing built with JS that isn’t going to need to scale to a large codebase or whatever, sure, keep it simple!
For anything non-trivial you'd have to add some kind of listener or tag to every node, find said node, and then update (ideally in a performative way). Said updates cannot destroy existing state, but must patch it. Updates must also not break existing attached event listeners. The list goes on and on.
I don’t know and can’t easily find out the time complexity of things like React setState(), but would hope that for frontend code bases where there is a build step(s) and/or for typed frontend languages we could achieve better than linear (especially if N is every element on the page)
Forgo, Mithril, and Crank all require explicit commands to rerender, unlike React and friends, which need visibility into everything that could ever trigger a rerender so they can handle it automatically.
You'd think manual rerenders would be a big pain, but I find it a small price for allowing by state to just be regular ol' variables. It makes everything feel simpler, like I'm just using the language instead of expressing my business logic through the framework.
There may be times when it's worth the cost, but otherwise just hang it on the global state and move on with your life.
(Been using preact/react since 2016, angular/jquery before that)
[1]: not actually like server rendered react, since srr only happens the first page load.
The cons are:
- A fair amount of work to make changes to the state side-effect free, but the documentation has a good tutorial on how to approach this.
- It's for ClojureScript, so it's a non-starter for organizations that will not consider anything outside of the mainstream.
I have also used Elm. If I recall correctly, Elm's state management paradigm was inspired by re-frame. I found writing JSON decoders in Elm to be demoralizing and tedious. Since the creator of the language was vehemently opposed to providing any real JSON support I decided to move to ClojureScript.
Nope, it's the other way around
Designed in late 2014, it slightly pre-dates the official Elm Architecture, although thankfully we picked up foldp ideas from early Elm games
https://day8.github.io/re-frame/re-frame/#why-should-you-car...- Provided it is not an offline-first app, move as much state handling as possible out of the frontend
- use SWR or React Query for server state
- Keep pagination, filters, and other such options in your URL
- Hold state as local as possible. Only ever move state up if two or more branches of the tree need to be synchronised.
- Pass information down through props. That is what they are for. A little “prop drilling” is fine.
- Use CSS custom properties for theming
This gets you >95% of the way with very little complexity.
For the leftover pieces of global state like ‘isSidebarVisible’ I like Zustand because it is tiny both in bytes and concept.
I don’t like to use Context for state management because it litters the top-level with providers and causes unnecessary rerenders.
Avoid a single global store at all costs.
It gets out of the way in terms of boilerplate, has a good solution for avoiding unnecessary re-renders, has tooling for deep state updates without messy syntax (immer) and strikes an overall good balance between powerful and easy to use. Cons might be that it is a bit more obscure so people might have to learn it. Also the learning curve is a bit steep. YMMV
This is for commercial reasons. Apps that depend on assets are crippled over low-bandwidth or otherwise unreliable connections, and frequently break in the presence of aggressive content blockers. I won't do that to my customers.
This can scale to much greater complexity before it becomes an issue than many are willing to admit.
The app I'm building (https://inventai.xyz) is non-trivial, especially the canvas. There hasn't been any cost or downside. Quick to learn and implement.
Redux in particular was so dogmatic about it. So much boilerplate. State is ephemeral. There’s no reason to need one global store.
Check out svelte, it's amazing.
It is simple (no need for lots of boilerplate code), reasonably performant, integrates with the browser Vue dev tools nicely and is just generally a pleasure to work with, especially because Vue does a few things better than React in my eyes - most notably, the hooks.
Pinia in particular also seems to generally get out of your way and doesn't enforce a particular architecture upon you. Want to add methods to the store to interact with an API and not have to think about that in your other components, merely call a method for initializing some data from the API that will take care of the store state? Sure! Want to do the opposite and only use the store for persisting data, regardless of where it's initialized? Go ahead. Want to have your components react to changes in the store? This will work with either approach.
This is subjective, of course, but personally that combination of tools has resulted in a pretty good development experience, in my eyes something like the following is a nice stack:
- Vue 3 with Composition API (hooks)
- using SFC with <script setup> (takes care of some boilerplate)
- using Pinia for state management
- using PrimeVue or a similar component library (to have ready-made components for most needs)
For React, I have to say that MobX was nice last I checked: https://mobx.js.org/README.html though React's own Context functionality is also okay: https://reactjs.org/docs/context.htmlreact-query has hook wrappers that allow accessing data you've fetched once across multiple components.
Calling the refetch in one will update all components using that data.
It's clean and minimises the need to think about shared state
Of all of the SPAs I've worked on, 90%+ of the "state" is just server side data, pulled down via APIs, to display to the user. Other state libs don't bother to take into account common things like cache invalidation, or loading states, but it's all built in to React Query.
But while some of the UI I build is complex, I don’t build SPAs. If I did maybe this wouldn’t work as well.
Redux:
- pros: framework to place different pieces of logic, makes it predictable once everyone is onboarded; boilerplate (actions, reducers, selectors, etc) makes it easier to avoid mistakes if you're not using TS
- cons: boilerplate adds a lot of friction that's not needed if you're using TS; debugging usually requires jumping through more files
XState:
- pros: what I said for Redux but on steroids
- cons: big setup (learning-wise); feels overkill 95% of the time
Context:
- pros: simple to understand and use (you expose values and setters via useContext); minimal setup (Provider and useContext)
- cons: you're still trapped inside React, your logic is inside a big component by default
Mobx:
- pros: keeps your logic out of React by default; you can export your model to another env and debug/test/play easily
- cons: most costly to setup (learning, config) out of all the options (redux with starter toolkit reduces the setup overhead)
I mainly use Context now as I port from Redux. Feeling the pain of not having an interactive model outside of my app views and want to get my team on Mobx.
Yikes. We can be just as reductionist about the BE: it’s just a shim for accessing a database.
Most developers overengineer to the extreme their projects. I'm not saying all of them are like this, or that all applications are just simple CRUDs, but most are.
To simplify this workflow you could use something like https://github.com/andsala/svelte-persistent-store Note that repo is basically 50 lines of code to automate this stuff, which is pretty remarkable. However this still won't handle cache invalidation or syncing.
https://sveltequery.vercel.app/ should handle most use cases. This is very similar to React Query (since it's from the same devs at https://tanstack.com/).
For handling the service workers stuff for (what I assume would be a PWA) I'd just use the Vite PWA plugin which uses workbox. That's if I need a service worker at all, if I don't I'd just set the manifest and be done with it.
An alternative depending on the needs of the specific project is to just fallback on something like the Firebase or Supabase SDK which handle offline mutations and syncing for you, for simpler projects these work incredibly well.
https://speaker.app has a UI I've been prototyping w/ this approach.
https://www.npmjs.com/package/eventemitter2
https://www.npmjs.com/package/eventemitter3
https://www.npmjs.com/package/eventemitter4
https://www.npmjs.com/package/eventemitter5
https://www.npmjs.com/package/eventemitter6
https://www.npmjs.com/package/eventemitter7
https://www.npmjs.com/package/eventemitter8
https://www.npmjs.com/package/eventemitter9
For personal stuff I like recoil and jotai. They're similar in design. Super simple to start, easy to type, work with layers of complexity.
Benefits of it over mobx is data normalization with references and JSON patches which allow you sync complex state easily. Typed models are also a plus.
Drawbacks are performance (see https://github.com/mobxjs/mobx-state-tree/issues/1267).
Previously was using immer, which I loved because of immutability but moved off since classes and OOP didn't feel as natural as in mst.
If I were to pick an alternative, might try redux with normalization https://redux.js.org/usage/structuring-reducers/normalizing-....
And if I were to build a state management tool, I would prioritize a library that has - immutability - JSON patches and snapshots - data normalization with references - schemas / models compatible with zod
For multi page apps (e.g. with nextjs) I’ve never used a global state manager. Next-auth, useReducer and react query are enough until now.
Before I tried calling functionality and updating the url in response to user actions but since the state has to be exactly the same if you paste the url into the browser I got rid of the direct response.
My company is building app with quite a set of complex features and structure (e.g. stateful integration with game consoles, background custom upload protocol, varying lifetime of modules, etc).
- The state management we always come back to is closure-based objects.
- Styled as agents, rather than passive storage (e.g. redux store-like construct). Thus providing separation of concern both in type definitions and code execution scheduling (can't find better words for these :'))
- In house event utility is used by the agents to communicate outwards.
- Combine with useEffect, useState, useMemo, and our in-house event utility to "escape" React's lifecycle by default and selectively subscribe to events, thus avoiding unnecessary-rerenders problems.
- Designed to abstract its domain and expose its internal data via methods.
- Optionally be designed to allow both code reuse and object reuse. Allowing devs to share or to not share states. Common use case for object reuse is agents acting as background-service propagated via React Context. It works really well with React Context.
- Optionally self-managing via buffers and async loop. The buffer is not exposed to its consumer directly, and the async loop works on the buffer.
- Can be useful for dependency injections too.
- Well isolated and optionally injectable so that you can unit-test against it without import mocks.
A caveat, this pattern isn't as usable when written with pure JavaScript. We relies heavily TS on FP patterns supported by fp-ts and io-ts as well as IDE's auto-complete and jsdoc to unload a whole stuff off our mind.
We've sometimes evaluated common state management solution and often found them requiring us to remember more things, but it doesn't yield valuable abstraction. In fact, most state management is abstracting the wrong thing.
First of all, I would suggest minimizing it as much as possible! You can often get away with a combination of component local state and data fetching libraries like SWR (the one I use) and react-query (I've heard it's good but haven't used). Modern data fetching libraries support intelligent caching, cache invalidation, and even optimistic updates.
The vast majority of internal tools at Twitter (prev employer where we built internal tools) didn't need any state management at all outside of swr and component local state.
However there will be some cases where some sort of client side state management will be useful. In these cases I reach for jotai. It was one of the first state management solutions designed post React Hooks, so it feels much more natural than state management solutions where hooks were bolted on. Additionally, it doesn't suffer from some of the rough edges that similar libraries like Recoil have (like naming fatigue from all those string keys).
Btw, I only use a tiny subset of jotai. I don't use any of the persistence or async stuff.
So tl;dr:
1. Build your app with component local state and SWR only.
2. If this gets messy, refactor your component local state and SWR code to be abstracted away as reusable functions / hooks.
3. Only when there is a performance problem or a really convoluted component hierarchy that doesn't make much logical sense, consider adopting jotai, but only for the parts of your app state that are causing the issues.
https://github.com/prettydiff/wisdom/blob/master/state_manag...
I use it for an OS GUI here which fully loads in 200ms in Chrome (300ms Edge):
https://github.com/prettydiff/share-file-systems
When simple things, like state management, become complicated they are either over engineered or poorly planned.
Now, working with react, I have successfully avoided redux and any extra state management like yhe plague.
Good abstractions, hook factories (under-disscussed topic imho), some sparse contexts. So far it has worked out great. I recently managed to implement quite complex requirements using these disciplines on a project while some other part of the same project contains redux-hell to do some simpler things
React seems to be drowning in its own complexity these days. As another poster points out you can do global state by observing an object on ‘window’, that most react state management tools make this more complex rather than simpler is concerning.
Sometimes you really need a state machine and for me this is the go to library.
Pros:
-Visualizer. Non-tech people love the visualizer. Tech people as well really.
-Integrates easily with entities which are also implicitly state machines, like Promise.
Cons:
-Learning curve. I've already rewritten our state machine once because my original implementation was not idiomatic.
Mobx also works. Ngrx, Redux and the like just smear your logic all over the place, which is bad.
For big stuff, especially deeply nested trees, MobX is still my favorite. It has some quirks but the core premise is such a simple and ergonomic mental model to work with, even for very complex state with intermediate derivations
2. It has some minor gotchas around object cloning, proxies, memory usage, and stuff like eg. certain functions have to be pure
But in practice these aren't big barriers; once you get used to them they fade into the background. And I've made stuff with MobX that I'm not sure I could have built with any other state management system. It's also one of the best-performing ways to write React apps because it can be absolutely surgical about only re-rendering exactly the things that need to update, even moreso than the official state management system
A way to optimize that approach substantially, while keeping everything else mostly the same, would be to have a setter on the global object, and only re-render the root component when that setter has updated the state.
My suggestion is not a very optimal approach, but probably a lot better than the continuous state loop.
When working with Flutter I like provider. With React I use Redux Toolkit, but saying I like it is a stretch.
No need for complicated state on the client.
The easiest way to avoid problems with multithreaded writes to globals is to not have multithreaded writes to globals. Ensure all writes to said global only occur in one thread. One possibility is to use e.g. locks and semaphores to ensure only one thread writes at a time, but nothing can beat the amazing synchronity that is imperative programming. Try it out some time. Tell your friends.
The real secret here is that there are very few things that actually need to be globally writable. Most things that are global should be constant, so not a problem. For those which aren't, you probably have some kind of IPC message passing thing in your programming tool of choice. Use it to pass any state changes to global state to your main thread, which changes it. Be aware that race conditions may happen. If they cause you issues, make them not happen any more. Don't worry so much about life.
There will be problems with every approach. Pick one with few problems, and deal with them as they come. Multithreaded programming is inherently complex. No need to make it worse than it is.
Context: the app in a large enterprise Angular app (100+kLOC)
hypermedia as the engine of application state