-
Notifications
You must be signed in to change notification settings - Fork 1
Review of AutoCompleteDemo
We added a prototype GUI using REACT techniques (see pull #7). On a detailed review of the underlying code, I've noted several shortcoming of the basic REACT approach to using props
and state
(with related lifecycle hooks like componentWillMount()
) to wire-up a GUI:
-
Child Components can't access global state in the Parent Component - Plain old REACT assumes a top-down flow of data through the
props
data object, which is designated in the JSX code of the component'srender()
function. This is one-way, so child components (e.g. all components other than the root) need to have every API call explicitly passed to it. Not only that, but every child component of a child component furthermore has to have these references explicitly passed as well. This create a very fragile, ambiguous, needlessly repetitive, and difficult-to-refactor mechanism which means everytime you change the root API implementation, you have to update every single damn reference everywhere. It is highly error prone and difficult to follow. It also results in very verbose component declarations because all the props have to be explicitly listed, OR the props are contained in an object that HIDES the implementation. The mixing of application state with parameter passing further complicates matters. -
Maintaining global state in the Parent JSX Component leads to mixing of GUI and Application Logic - This is also messy, as REACT components use an ugly Javascript class syntax. It would be preferable if the React component did not contain application logic, but instead acted as a pure GUI component that updates application state ELSEWHERE through a single mechanism, upon which the application logic can then respond in kind.
-
It is difficult to manage modalities with handlers that are directly calling component methods - For even simple interactions that involve updating multiple components, an interactive action has to funnel the action up to a controlling component (the root component, which passes its references down through
props
as mentioned above), which then coordinates subsequent changes by calling every other child component back down the chain by updating their props OR by overriding the child component's state through theprops
andcomponentWillUpdate()
mechanisms. This leads to fragile, difficult-to-follow code that is further complicated by subtle variations in prop names. A complicated transactional interaction is even more difficult to follow, especially when calls are repurposed in different contexts and these contexts are not documented. Determining the 'mode' of the application by assuming that a particular call with a particular parameter at a particular time is error prone and difficult to modify without breaking some other operation, or it leads to duplicated code with only minor variations.
A GUI can be written so it responds to changes in data; this is the general premise of REACT and the reason for its top-down props
approach and its use of local state
at the component level. Furthermore, a GUI can operate based on well-defined sets of data and update operations. Let's break it down as a template for emerging best practices.
(1) In the Node and Edge Selection prototype, we might classify operations as follows:
operation | state | triggering event |
---|---|---|
SOURCE_SELECT | source_id | NetGraph:Node.onClick |
SOURCE_DRAG | source_id | NetGraph:Node.onDragStart/End |
FILTER_SOURCES | f_source_ids | NodeSelector:SourceLabel.onChange |
SOURCE_HILITE | h_source_id | NodeSelector:AutoSuggest.onHover |
SOURCE_SELECT | source_id | NodeSelector:AutoSuggest.onClick |
SOURCE_UPDATE | source_id, form | NodeSelector:NodeEntry.Button.onSubmit |
EDGE_SELECT | edge_id | NetGraph:Edge.onClick |
FILTER_TARGETS | f_target_ids | EdgeEntry:TargetLabel.onChange |
TARGET_SELECT | target_id | EdgeEntry:AutoSuggest.onClick |
TARGET_HILITE | h_target_id | EdgeEntry:AutoSuggest.onHover |
EDGE_UPDATE | edge_id, form | EdgeEntry:Button.onSubmit |
(2) We also have several styles of display styles for nodes and edges:
display mode | description |
---|---|
normal | regular display of node/edge |
filtered | node/edge is a member of the current AutoSuggest set matches |
highlighted | node/edge is the current AutoSugget entry under the mouse cursor in a filtered set |
selected | node/edge is the singular selection being edited |
(3) Lastly, we have several user tasks that might consist of multiple operations. There isn't a clear analogy to this in the current prototype, but the intent of having named user tasks is to create them from clearly defined atomic operations, triggers, and handlers that operate in a sequence with explicit exit/continue/retry conditions.
user task | operation(s) | linked components (ideal) |
---|---|---|
"select a node" | SOURCE_SELECT | NetGraph, NodeEntry |
...dependent mode | SOURCE_UPDATE | NodeEntry |
"search for node" | FILTER_SOURCES | NetGraph, AutoComplete, AutoSuggest |
...dependent mode | SOURCE_HILITE | NetGraph, AutoSuggest, NodeDetail |
...dependent mode | SOURCE_SELECT | NetGraph, AutoSuggest |
(4A) To maintain a top-down dataflow-driven GUI, we want to ensure that REACT responds to state changes outside of it as the main refresh mechanism. This is provided by our proposed external mechanism for maintaining (a) application state that must be rendered and (b) transactional logic that reflects the application mode/task-in-progress. REACT components maintain what local state is needed to render the application state, copying it as-is or transforming it so the normal this.props
and this.state
mechanisms in REACT work as expected. However, dataflow to child components flows from our external mechanism, not from their parent component.
(4B) Likewise, when a REACT component at any point in the component tree accepts user input, this change is passed back to our external mechanism to update application state. This will then trigger the mechanism in (4A) to refresh other components.
There are a couple of approaches I'm considering:
A COMMON STATE MANAGER can simplify the passing of parameters through props by letting components access global state from everywhere through a single common module. It is similar to REACT-REDUX without the action dispatcher stuff.
- A universal state manager called UISTATE (which is part of UNISYS) handles all application state. The contents of UISTATE are set through
SetState()
, and subscribers to UISTATE will receive callbacks should state change. - All REACT components, no matter how they are nested, can subscribe to UISTATE to receive change events. These change events are then converted into local
this.setState()
calls so the component can refresh. Child components don't need to have state passed down through props because UISTATE is calling them directly. - All REACT components can implement local change handlers that pass data to UISTATE via
UISTATE.SetState()
method, so any other subscribing components can receive the updated state. - Use of this model requires that you have adequately modeled your application GUI can refresh cleanly from just the data in the application state.
Extending this idea is an OPERATIONS MANAGER (OPSMAN) can handle both parameters as above AND eliminate the need for passing API functions through props. It is an adaption of the UNISYS messaging system, with additional properties to make it convenient to implicitly manage different kinds of events and st`ate. This is a replacement for direct function calls, analogous to the UNISYS global event system.
- A universal operation manager called OPSMAN (an extension of UNISYS) handles application state changes using an event model. An operation is defined as an action that modifies some state data (e.g. 'SOURCE_SELECT' expects to receive a
source_id
). - All REACT components, no matter how they are nested, can subscribe to OPERATIONS through OPSMAN (e.g. 'SOURCE_SELECT') and receive data that can then be used to refresh themselves.
- All REACT components, no matter where they are nested, can dispatch an OPERATION through OPSMAN. This consistes of the operation name and the data that is being changed.
An extension to OPSMAN adds support to maintaining application transaction logic. This would cover modalities, features that should be enabled or disabled, progression through a set of steps, and so forth. I haven't modeled this concept heavily, so this is a WORK IN PROGRESS.
The first approach is to automatically define and manage state variables such as "application mode" and "chains of dependent operations" in the global operations manager, so components or debuggers can easily inspect what is going on. Currently the AutoComplete demo uses the parent's state object for setting data like this.state.isEditable
and passes it down explicitly, but there is no support for transactions; the code works for simple transactions because there is not a lot of logic. It becomes a liability for operations like EDGE EDITING, resulting in interactivity that conforms to the limits of the basic REACT implementation rather than to good design.
Anyway, an extension to managing UISTATE and OPSMAN would be to extend the notion of state to include state sets, and add asynchronous callback support for operations. This is similar to code we've used for networked UNISYS.
- BROADCAST operations have no side effects. These are useful for notifications that are not critical to the application operation.
- STATEFUL operations have multiple operational values. A stateful operation named 'LEVEL' might have three different states defined '1', '2', '3', '4' that can be set to one of those values. Subscribers receive the value, and the value can also be directly polled.
- REQUEST operations return a data object asynchronously. The originaling operation requester (say, something like 'GET_FILELIST') can fire a number of other operations and wait for them to return data, process that data, and then return that aggregate data. This is probably more useful for networked operations. The state of the operation is inspectable.
- TRANSACTIONAL operations are similar to STATEFUL operations, but consist of operations that must execute one-after-the-other using the REQUEST mechanism. This can be used to make a series of operations sequential when order of operation matters. The transaction state is also inspectable.