Thoughts on Web GUI Component Interactions

What is a component
A component is a combination of expected state, state logic, and view logic, that interfaces with 0 or more components. The state itself doesn’t need to be held within the component.

Notion of Multiple interface points
For a template, there generally are many points of interface, wherein the data fills the template. Let’s consider a wrapper template, that wraps a page. The wrapper includes some header, body, and footer. Now, generally, the page will fill in body content, but sometimes it is desired that the page changes something in the header or footer, like meta tags.

Can we consider both the wrapper and the page components? Yes. Could we break this into multiple components?

Let’s say the wrapper, with interfaces designated by <, looks like

1
2
3
4
5
6
7
|
|<
|
|<
|
|<
|

If we had a page component that could interact with multiple interface points, the coupling would look like

1
2
3
4
5
6
7
|
|<-|
| |
|<-|
| |
|<-|
|

If we were to break the page up according the the interfaces of the wrapper, we would, perhaps, be separating cohesive logic. For instance, the title of a page is usually both in the meta tag and in the body, and the logic for finding the title (database call for name of relevant object) and assigning it could be cohesive, in one place (the page component), or would need to be sectioned off.

So, it would appear that allowing for multiple interface points is useful in maintaining code cohesion.

issues with single point component interface
(a child mounts into a parent component at just one point. IE, the child presents only one render return which is used at the parent’s single-point-interface )

  • Doesn’t account for multi-point components, like a component that both changes the page primary content, and the page side navigation.
    • side note: the idea that this page-based reactivity can be handled by using logic within the management functions of a central store means you are moving page-component-logic outside of the component and into a central store management functions location, which would seem to go against the desire of componentisation.

issues with single central state

  • all components must have a state that conforms to something the central state can hold (ex, no forerunnerdb collection)
  • all components experience the overhead of interacting with the central state, instead of local data
  • all components must be aware of naming within the central state, both not overlapping other component data, and potentially having some naming uniqueness even among same-type components
    • this is alleviated if the central state is controlled by manager functions that know which component is attempting to change the central store, or is firing an action

issues with central state middle manager functions

  • all normally-basic operations must go through a manager, making it a longer process. Ex. arr.push 1 becomes manager.componentDataManagerFunctions.pushOnArray 1 along with the definition of componentDataManagerFunctions.pushOnArray
  • if using the dispatch pattern, every update to central state goes through all manager functions, making the computation longer. Ex dispatch(name: 'component_1 arr.push', value:1), which then requires a management function to check for that name, and execute the push

issues with react + redux

  • the above issues + required es6 transpiling

issues with purely reactive component interactions
It does not cover some standard cases. With any component, assumptions are made. These assumptions can be as simple as, “some standard tool, x, library is available”. There might also be an assumption of a standard component - like a notification/alerts component that handles displaying notifications to the user. A form-component would access the notification component through method calls. Here, the notification component is not reacting to the state of the form component, instead, the form component is telling the notification component to act according to the state of the form component.

This can be meaninglessly translated into FRP by having a standard notification stream that the form component pushes events into, but this doesn’t change the nature of it being the form component requesting to the notification component that it present some notification. Instead, the benefit in the FRP case is the indirection that would allow different notification components to subscribe. But, to make clear, the notification component is not subscribing to individual form components.

How might one component, x, interact with another, standard, component, y?

  1. x knows of y methods and calls them (expected lib “EL”)
    • requires knowledge of y
  2. y is provided as an input to x, wherein x expects y to have some standard methods (dependency injection, “DI”)
    • requires dependency injection
  3. there is some standard indirection api, (service locater). or stream to which x calls, and to which y listens (service locater, “SL”)
    • requires knowledge of abstracted y

Dependency Injection, Service Locater, Expected Lib
In all 3 cases, we have an external resource, “exre”, used by some code. DI or SL is used in the case that exre can change. It may be the case the exre never changes, and the overhead of DI or SL is not-needed. What overhead? For DI, the code outside the component must know the dependencies of the component, and must make those dependencies available to the component (which often means instantiation). This mixes the concerns, making it necessary that the outside, management, code know about something which may have no relevance to any other component, strictly for the purpose of providing the dependency to the one component that needs it. The SL puts the concern of the dependency back into the logic of the component, removing it from the management code that calls the component.

So, in what cases should these different methods be used?
One thing to consider is the nature of the programming language. In PHP, if you define some Db class, you can not override it later (this has improved with namespaces). This is important because even in cases where it is never expected that a resource/dependency will change, it may be desirable to make a mock one for testing. This can be done with EL to the extent that construction-configuration allows it (ex: using a configuration to point to a test database), to the extent the system allows it (ex: including a Db test class instead of the main Db class file), and to the extent that the language allows it (redefining the global Db class).

See (http://www.tonymarston.net/php-mysql/dependency-injection-is-evil.html#conclusion)

It would seem that EL should be used by default, service locater when the range of possible dependencies exre represents is known and can be accounted for, and dependency injection when the range is unknown.

revisiting multiple point interfaces
Regarding how an interface is used, there are 2 inclusion methods, and 2 exchange methods with each of those modes.

Inclusion methods:

  1. the page requires the parent as the wrapper (component requires a parent)
    • this can be done at the start or at the end, such that the require action may or may not include data passed in to the parent
  2. the wrapper requires the page (component includes a child)

Exchange methods:

  1. the child detects the interface available through a parent-interface-object and interacts with it
    • ex: in child code: if parent.interface.header
  2. the child provides multiple points of data, some of which may not be used, and the parent applies the parts of the child that are useful
    • ex: in parent code: if child.header

To resolve which to use, there is another notion to consider: general vs specific. The idea is, for some application, there is code specific to that application and code which is general (potentially use-able on multiple applications). With general tools, the general tool presents an api. The general tool, itself, does not normally investigate the component that uses it - instead, it is the responsibility of the component to use the general tool correctly.

As a side note here, the pattern is:

  • the more general a tool is, the less responsibility it takes for knowing about the specifics of its implementers. And, this tends to make sense, since it puts the arbitrary details of implementation in the implementation, instead of bloating the general tool with all possible implementation factors.

In the most simple cases, it is enough to provide an api with known required and optional parameters. With less simple cases, we have to account for higher order functions and complex data structures. And, in this case, like in Golang, a method for handling variability is to specify interfaces, wherein, so long as a parameter conforms to some expected structure, with some desired methods and attributes, it can be used regardless of its class.

Going back to our wrapper and page components, we can see this as the page using the general-tool wrapper component by requiring it, and providing to it expected attributes of data including “header”, “body”, “footer”.

What if the body content was dependent upon an asychronous getter? What if the body was subject to updates? Just make wrapper recallable.

Final Thoughts

  • Reactive programming does not appear to be an end-all solution for dealing with components
  • Most component frameworks fail to provide multipoint component to component interfaces
  • There is no general ideal method of interaction between components, as EL, DI, and SL all have specific cases for implementation
  • A single state central holder presents inefficiencies, inflexibilities, and the need for potentially excessive code