Developing some guidelines on how to write a clean web app, using React as a basis but not letting React be the most important element because it shouldn't be IMHO. This will be in the "hot garbage" state for a while :-)
Execution Context Scheduling
The MDN Event Loop rundown is a concise description of how it schedules (1) functions to completion and (2) events fired and handled to completion.
There are essentially two execution contexts when running a Javascript program.
-
The runtime executes all immediate-executables until it's done in a synchronous fashion. It will not be interrupted at all by another process, so you can be sure that everything runs to completion without contention to resources.
-
Events run in a different execution context, the "Event Loop". When it gets a chance to run, the event queue also handles each message and runs to completion. Because this is a different execution context, access to data frame ('this') has to rely on either function closures or use of
Function.bind(context)
if use ofthis
is anticipated. In other words if you define a method as a target for an event, it will lose access to the originalthis
(the object) unless you take these measures.
When writing React code, its state management makes use of both the runtime and event loop context.
- A React.Component class has to bind its event handlers in the constructor (e.g.
this.handleClick=this.handleClick.bind(this)
) - The event handler is called by the React virtual event handler declarations (e.g.
onclick={this.handleClick}
oronclick={e=>this.handleClick(e)}
if you are included computed values from therender()
function. - The event handler will do a
this.setState({ something })
which will cause a rerender of any affected local elements that usesstate.something
in therender()
function
This works fine unless you are also responding to events from outside the React subsystem. The React way of doing this is to use props cascade from the top-most component, use React-Redux high order component wrapper, or use React context. These all kind of suck because it obscures the messaging from external systems and/or spreads it across several files.
You can add a separate pub-sub system to work with React directly if you remember that when React is in the middle of a state refresh, it is already in a deep Event Loop message and you can't fire another setState()
if some of your state logic is also sending state to your separate pub/sub system that could potentially fire and trigger some other handler in your application. The solution, I think, is to design your state system to always fire on the NEXT event loop through the use of setTimeout(()=>notify(subs))
instead of just notify(subs)
. This use of setTimeout
with delay 0 will "fire on the next event cycle", which means it won't interfere React (which will complain bitterly in development mode).
Mixed Execution Context Handling
Because we're reliant on React for triggering rerenders, we have to make sure any non-React source of events aren't screwing it up.
React Rendering as Sri Understands It
A React app is a tree of rendering nodes. A node receives props
as input from its parent, and optionally defines its own state
. Rerendering occurs under only two conditions:
- If the incoming
props
changes, then React will rerender that node, which will in turn update anyprops
it sends to its child nodes. - If the node's internal
state
changes and its rendering function is dependent on it, then it will rerun the rendering function. This in turn maybe changeprops
that are provided to its own child nodes.
Props are a way of passing data "down" through children, and State is a way for every node to manage its own local data. The big drawback of React is that it's not clear how to let children communicate state BACK to a parent or how to centralize state. In hindsight, the confusion about this in the first years of React's existence may be due to a general lack of familiarity in the general population about HOW to structure an interactive program, and a lot of bad patterns that were applied without deeper understanding spread across the Internet. It doesn't help that React seems to change its meta every half year or so.
As far as I'm concerned, the ideal is to have a clean separation between your source data, the GUI-related data, and the intermediate state used to generate the GUI-related data.
Another problem is how props or state changes are triggered. Every interactive program has some kind of input routine that (1) detects a change in a set of monitored input devices (2) routes the change data to the code that is supposed to handle it. These are usually called events and event handlers.
For React, the basic idea is that it's a UI that has data that flows from a higher-level node down to its child nodes. Any node can receieve its own events and make changes to propagate the change down to its own children, but this top-down scheme breaks the moment a child node needs to signal a change outside of itself. The solutions I've seen all bother me because of React solutions rely on "code magic" to move data around, which makes it hard to figure out how things are happening. React code is another idiomatic language built on top of Javascript and likes to use the latest ECMAscript features, which increases its challenge level. To solve the very basic problem of "how do I centralize change information in a way that the change API is reachable by everyone", the solutions are:
- Use props to pass down API objects to children who might need to access it. Good luck keeping track of that. The JS spread operator helps make the code look less messy, but it still is pretty boilerplatey and fragile.
- Use ReactRedux, which relies on a conceptual model implemented across multiple files and using React's "Higher Order Components" to merge the functionality into the Root level component and pass it down to all children who might need it. I find this messy and boilerplatey.
- Use React Context, which allows you to define a named context that you then declare down through your React tree, somehow. Also messy and difficult to trace.
There are a lot of other solutions I'm sure. I prefer not to let React have power over data and overall application state, because it sucks at this. It's great as a GUI system that lets you compose reusable component definitions as boxes inside of boxes that can be informed by its parent boxes how to conform itself, but that's not the complete solution. Letting these components manage actual data is stupid, because the data architecture and hierarchy does not match the GUI composition. You know, how like a GUI is not implemented to reflect the way a SQL database tables are laid out.
Difficulty of Understanding Events
A separate issue is how to understand events as they relate to the GUI, state, and data. This adds to the problems with having a GUI system like React manage data, and requires some advanced understanding as well to debug. At minimum you need to know:
- where an event comes from and how it is routed to our code
- the difference between synchronous and asynchronous events in terms of the execution path and memory context
- how our code receives the event and what memory context is in use
- how to visualize the cascade of events that produce the intended side effects without unintended ones.
Sri's Current Model
The basic action we want to control is how to tell React that a change has happened from OUTSIDE it. My modules instantiate React as a visual subsystem. So far:
- Instantiate the app's entire state and viewstate in an AppCore module that is imported into the Root Component, and initiate a pub-sub link between them. The initial read is all the data keys supported by the AppCore.
- The Root Component renders the basic layout of its top-level components, controlling visibility and passing data keys to its children.
- The child components manage their own view state for GUI elements, but read the view data from the AppCore module using the data keys it has received. Since AppCore is a singleton module, it can be imported everywhere.
- The child components rerender when their data keys change, so it can re-retrieve the view data at render time from AppCore.
- When a child component wants to signal a change that affects the application state, it also uses the AppCore module to write the data change. AppCore serialized and manages access to data and application state through an API that models the operations that the app can perform on data and state.
- When AppCore receives a data change through its API, it can also check to see what other derived data may change. If this affects any information referenced by a data key, then a change event is fired for that data key. In this way, the important app logic is in one place instead of scattered across a zillion React components.
- The Root Component receives the data key change and updates.
- Child components can also directly subscribe to the AppCore to receive events, so long as they subscribe on mount and unsubscribe on mount.