There's an answer the end of the article, but first, let me rant!
My Rant
So I asked ChatGPT: "How you would avoid data race in two siblings react component?" Here are its answers:
-
Lift State Up
Make sense, as you always need context (do not confuse with Context) to resolve conflicts between two actors, just like how you give understanding to two children fighting or resolve cases in courts.
-
Use a Global State Management Library
Ok? So, why the need for "global"? Can I not involve everything in the app just to resolve conflicts in a very small scope?
-
Debounce or Throttle State Updates
Never in the world will debouncing or throttling get rid of data races. Reducing, yes, eliminating, no.
-
Use an Atomic State Update Mechanism
Well, this helps a little. But it will likely lead you to implement lock-like on the same layer of abstraction as the business layer.
-
Implement State Synchronization Logic
Finally synchronization is mentioned. This point shows an example where a child component manage the parent's state in its
useEffect
despite the state being lifted up. -
Immutable State Updates
Immutability won't help preventing data races.
Then the rest of the answers doesn't help either: Custom Hooks, Shared Data Storage with React Query, Web Workers, Optimistic UI Updates, Proxy Objects, Leverage LocalStorage/SessionStorage, useReducer
The ones that make sense if you think hard enough: Proxy Objects, Web Workers, and Event Emitters
Distributed System Problem In A Single Threaded Program
The above answers has one theme: N actors conflict, each actor knows the others' state, and each is responsible to "defend" itself against unwanted user interaction, pre-emptively and locally.
If you think about it, this looks like a peer-to-peer problem. The local entity handles things based on information received by the remote peers.
In itself, it is a decent solution.
However this is a solution to a distributed system problem applied in a single threaded application!
Async and concurrency is an afterthought and a second-class
Defending JS against myself: "JS was built for scripts, not systems".
Have you ever spent your time learning redux-thunk or jotai's solution to async and then ending up with messy and chaotic temporal semantics?
Combined with concurrencies, the above tends to lead to the developer writing a lock-like on the same layer of abstraction as the business layer. I know I did one with Redux. I was dumb.
I still think that composable small constructs like generic locks is superior to big, opiniated, and undecomposable solutions, the latter traps you into a certain limit and should be optional and easily replaceable (this eerily reminds me of react-script eject
). In my experience, these small composable small constructs makes it easier to write temporal semantics of highly concurrent system (and even non-concurrent system at all).
Stuck in 1980s
...you know, when the client is just a presentation layer.
React is versatile! You can make a fat clients as well as thin clients with React.
I have beef with the most impressive "fetching" library: "TanStack Query". It is impressively easy to write application that fits its built-in timing behaviors, e.g. cached fetches and simple mutation. But once you need to add something a little more complex like a combination of a debounce and batched inputs, you need external effects and states and it changes the appearance of the code by a lot. So it limits the rich-ness of the clients.
(I know that TanStack Query is not exactly a pure state management solution per-se, but just wait)
Go to Reddit, Sort By Controversial
I observed this strange phenomenon in r/reactjs (but not r/react, which makes it more strange) where simplistic comments that suggests common libraries (and not seldom being irrelevant too) like "use zustand", "use rtk", "use TanStack Query" is heavily upvoted and those who argues just slightly not for those libraries, such as "take time to see if those libraries really help you", gets downvoted by a lot. I often wonder whether those are the work of bots or b(rainr)ots.
I'm worried that this strange downvotes/upvotes might steer people to perceive that React only fits for a portion of its potential; the limitation of accompanying libraries can hurt its reps. This is why I have beef with popular libraries overall.
State management that does things for me
...and I have those. In case you have the same problem that I listed above, I might have the library that mends it: Vaettir-React.
The app preview on the top with emojis streaming through a lazily poorly styled pipes, if you look closely, has some kind of custom concurrency locking between the input, the processor, and the output. I implemented a semi-DOD-inspired mechanism wrapped under the library, which acts like a state management. It takes less than 500 lines of code (90 lines of them are just the list of emojis that can show in the app) with the help of some locking libraries.
So, does it make your code shorter?
No, but it allows you to make your code more consistent. It also makes library adaptation easier, especially those that doesn't quite fit React's way of thinking.
You write like plain javascript inside it, that's the main point, but you have hooks that sort of tell you when to stop. For example:
- Adding debounce should be just adding several lines.
- So you can write
while (!isDestroyed()) { /* do stuffs */ }
oronDestroy(() => clearTimeout(someTimeout))
.
Is it a state management library?
Not in the classical sense.
It acts like a slightly smarter one, an agent rather than a passive store. In a way, it feels like running a server inside of your React component.
Is it only for React?
Vaettir is framework-agnostic. (Its documentation needs a bit more love tho).
Vaettir-React is its React first-class integration library.
Is it global?
Not necessarily.
But it helps you to reason about its locality. It has a hook VaettirReact.useOwned
that says "this component owns this state" in contrast to VaettirReact.use
which basically says "I'm borrowing this stuff, I just want you to listen to changes that happens inside this".
How else does it help you?
Other than its explicit lifetime (to compensate for JS' lack of destructor) and destroy hook, you can add more observables for fine-grained rendering, inside the thing, you can add absolutely anything. You can even put another React app running inside it if you want, for whatever reason, or DOOM (I should make one and register it in https://canitrundoom.org/).
How does it work with test?
Beautifully! Since Vaettir makes anything easily wrappable and instantiable, you can just spawn an instance of it (in a jest environment for example) and test it independently.
All-in-all, what I aim to do is to bring the awareness of the other, rarer side of React use cases--that its problems are not getting attention enough.