Local Addon development - introducing Redux

What do I need next?

I've recently released a first working version of the XDebugControl Local addon to control XDebug settings but I've already made a list of things I would like to implement next.
The addon was the first result of my attempt at a Local addon and you can read all about my struggle in the posts.
While I could go and implement pretty much all of them with some copy and paste "wizardry", the reason I've taken on this project, besides the need for it in my day to day work, was to learn some React through a practical and hands-down approach.
As a "PHP guy" I've enjoyed every second of it due to its clean design patterns and its component-based approach to architecture; the complexity of my addon is far from being huge but good practices are good because they work at any scale.
Since I've taken care to put in place [tests] and setup Travis CI to run them, I would like now to take the time to add another piece to the "React puzzle" that is born out of need: Redux.
Keeping in mind that I am, by no means, a "React expert" (six weeks of experience with it a the time of this post), I have developed, while working in PHP and other languages, an eye for "future spaghetti code" that I would like to avoid.
Really any code is a good candidate for "spaghetti code", mine is not really a "code-shining" gift.
The next feature I want to implement is a tab, similar to the "XDebug Controls" one, that will allow me to control other PHP settings.
In essence it will have a different title, it will not have the "XDEBUG IS ACTIVE" and button elements, and it will only sport the field list and an "Apply Settings" button; as I said before: it would be quite easy to have it done in 2 hours with some copying and pasting.
But I'd like to approach this new task with a little more structure, the similarity between the XDebug and the new PHP tab is apparent from the description and it feels like they should have a common base; like a common ancestor class.
If I start to think about a common ancestor class I start to think how the current "wiring" of components, happening in the current incarnation of the XDebugControl component as a very specific affair, can be made more general.
This is, roughly, what both views will have to share and have defined, hence, in a common ancestor class:

  • show a loading component if an asynchronous action is happening
  • show an error component if a manageable error happens
  • show a title
  • show some components; currently the "XDebug Control" tab shows the following components: the XDebug status, a button to control that status, a list of fields that includes a button to apply them
  • each component part of the view should be able to tell the application something asynchronous is waiting (commands on the underlying container) and that it should show the loading view

This last point, in particular, is currently not handled, the feeling of "unresponsive" UI the addon can convey is eased just during the first load; I'm aware of that and would like to correct that behavior.
Furthermore, components should be able to toggle the appearance of other sibling components depending on their state.
All of this sounds like an event bus based implementation where, essentially, each piece of the view (a component), listens to a common and accessible event broadcaster object to know if it should update its status, and with it probably re-render as well, or not; more on that note each component should be able to dispatch events on that event bus to allow for other components to react to a state change.

How does Redux fit?

I'll start with an example: right now when I click the button to activate or deactivate XDebug the UI "freezes" while the container runs the required commands and XDebug status is read from it, at the end of this process the XDebugControl component will update the whole view.
In the XDebugControl::render method I do this:

button = <Button text="Activate XDebug" onClick={this.activateXdebug.bind( this )}/>

Where this is the XDebugControl component itself.
The XDebugControl::activateXdebug method, then, runs this code:

activateXdebug() {
    this.container.activateXdebug()
    this.setState( {xdebugStatus: this.container.getXdebugStatus()} )
}

In the first line issues a command to the container, an operation requiring 1-3 seconds; when that is done, then, the component updates its state, after having read the new XDebug status (another 1-3 seconds operation), triggering a re-render.
I would like to show the loading view, an instance of the Loading component, while that happens and the simplest way to do it would be to modify the XDebugControl::activateXdebug method to this:

updateXdebugStatus(){
    // this can keep being synchronous
    const updatedStatus = this.container.getXdebugStatus()

    if (updatedStatus !== this.state.xdebugStatus){
        this.setState({
            xdebugStatus: updatedStatus,
            loading: false
        })
    }
}

activateXdebug() {
    // this should be asynchronous
    this.container.activateXdebug(this.updateXdebugStatus.bind(this))
    this.setState( {loading: true} )
}

I'm assuming I will modify the Container::activateXDebug method to become asynchronous and that, provided a callback, will call it when done; that being true then the view will switch to a loading one as soon as the button is clicked and show the UI again when the status has been updated and the XDebugControl::updateXDebugStatus method has updated the status.
The first lines of the XDebugControl::render method will take care of rendering a loading view, and nothing else, whenever state.loading is true:

render() {
    if(this.state.loading === true){
        return (<Loading/>)
    }

    //...
}

This seems to work out just fine and the same approach, extended to all the other components, might work.
The problem will arise in two cases though:

  1. the button, or control, that triggers the action might be nested more than one level deep; in the example above I might have the <Button/> component nested into a <StatusAndControl> component; this means the XDebugControl::activateXdebug method has to be set, bounding this to the XDebugControl instance, on each intermediate component; the example below shows it with one level of nesting but it can get messier.

    render(){
        return (
            <StatusAndControl 
                onButtonOnClick={this.activateXDebug.bind(this)}
                onButtonOffClick={this.deactivateXDebug.bind(this)}
            />          
        )
    }
    
    
  2. What if the state of the component depends on the state of another? Say that I want to show the list of XDebug setting fields as disabled if XDebug is not active; I would need to modify the XDebugControl::render method to provide the FieldList component that detail. And the same from above applies: each parent component in the component tree would need to know about, and take care of, all the possible changes triggered on children components due to a change or an action on another child component. This is manageable in a smaller project but might quickly get messy with many callbacks passed down the component tree and changes to deeply nested components code forcing changes to any possible user code.

I've approached Redux picturing an event bus and some of its method names, connect, dispatch and subscribe makes me think I'm not that far removed from the truth.

Next

I will install Redux and get started with it migrating the addon from its current architecture to a Redux based one; in doing so I will chronicle my new JavaScript struggle as it evolves.
The current version of the addon, it's working, is here.