One pattern you might like to try in this kind of code is writing your heavy imperative logic in classes (not React class components! just normal classes), and then using Hooks very lightly to "connect" and "disconnect" things.
See here for some examples of how you can approach writing the same code in a few different ways -- maybe one of these styles will fit you better: https://react.dev/learn/reusing-logic-with-custom-hooks#ther...
One example of this pattern is Searchkit [0] which performs most of its logic inside a singleton Searchkit class which is instantiated and passed as a prop to the root React component. A bonus is that it's easier to implement bindings for Angular, Svelte, etc. since they can rely mostly on the class and just need to call the appropriate methods exposed on its interface. For example, it looks like Searchkit now suggests using InstantSearch (react-instantsearch-dom) [1] from Algolia, i.e. an entirely different maintainer, and it creates the bindings with a `Client(new SearchKit(...))` adapter [2] around the class (see the code on the home page at [0]).
There are downsides to this - you do need to think a lot more about code architecture before implementing the code, and you can get into a really messy/unmaintainable situation if you aren't careful. And you'll need to consider what state your "singleton" is tied to - is it really one per page, one per component, etc? And depending on the answer, the implications may be that this pattern isn't a great idea. But for certain use cases, it can work well.
The core premise of this pattern is separating business and UI logic, which is not exactly a new idea.
[0] https://www.searchkit.co/
[1] https://github.com/algolia/instantsearch
[2] https://github.com/searchkit/searchkit/blob/main/packages/se...
Oh wow didn't expect to hear form you! Yes that's what I'm in the middle of right now actually to enable online multiplayer: everything regarding the game is in a (class-based) state machine in pure JS that can run frontend or backend and be interacted with via a websocket client (this actually helps with a11y too, navigating a board game via aria-labels was a pain in the butt), and the UI (still WIP) will be far thinner. Perhaps I'll give hooks a try again for that.
Yes!!
React apps should, in general, contain much less react-specific code than they tend to.
All of the “tanstack” components seems to be going this route which I’m really excited about. I tend to really like that stack and roll most other things myself.
Should make it easier to switch frameworks in the future if the need arises.
For what it's worth, we do think there is also a lot of value in building a deep vertical integration in tooling so that it can take advantage of React's unique features. A little bit about this here: https://react.dev/learn/start-a-new-react-project#bleeding-e...
I wont dispute the benefits to vertical integration, but I still think there is benefit into keeping things modular as well. I've been working on an application for the past five years that's stayed afloat partially because keeping things small and not becoming over reliant on any one library.
Regarding the link, I most likely wouldn't consider routing and bundling to be in that list. Sever integration currently doesn't apply to me, since our app is one of the cases where CSR makes more sense.
I created this library that would allow you to put all your state logic into classes: https://github.com/baron816/use-structure
I'm going through this process right now and would love to find examples of a larger open-source codebases doing this, in case anybody has references.
i've seen advice on the webs for useReducer instead of a lot of useState's...which feels a lot closer to the class-component this.setState().
is there a good reason not to uniformly rely on useReducer everywhere in a codebase?
Because you have to write more code. If all you have is a single variable that gets updated with no complex logic then writing a reducer seems excessively boilerplatey.