Skip to content
This repository has been archived by the owner on Jun 3, 2022. It is now read-only.

Make React architecture modular + unidirectional #12

Open
appsforartists opened this issue Oct 25, 2016 · 0 comments
Open

Make React architecture modular + unidirectional #12

appsforartists opened this issue Oct 25, 2016 · 0 comments

Comments

@appsforartists
Copy link
Member

appsforartists commented Oct 25, 2016

I'm leaving my architecture notes from #8 in this issue, but it could probably be broken into smaller issues.

In React, components should be small/single responsibility and only affect themselves (through configuration, their children).

For instance, DOMController currently does a bunch of things:

  • Draws the UI scaffolding
  • Determines which route to show
  • Toggles the visibility of its parent element
  • Sets up keybindings

These could be broken into separate elements:

class RemixerRouter extends React.Component {
  state = {
    showOptions: false,
  }

  // Use an arrow function to get correct `this` binding
  toggleOptions = () => {
    this.setState(
      {
        showOptions: !this.state.showOptions,
      }
    );
  }

  render() {
    return (
      <RemixerPanel
        toggleOptions = { this.toggleOptions }
      >
        {
          this.state.showOptions
            ? <Options />
            : <VariableConfigurator />
        }
      </RemixerPanel>
    )
  }
}

function RemixerPanel(props) {
  return (
    <div className = "rmx-card-wide mdl-card mdl-shadow--6dp">
      <div className = "mdl-card__title">
        <h2 className = "mdl-card__title-text">
          Remixer
        </h2>
      </div>

      <div className = "mdl-card__menu">
        <button 
          className = "mdl-button mdl-button--icon mdl-js-button mdl-js-ripple-effect"
          onClick = { props.toggleOptions }
        >
            <i className = "material-icons">
              menu
            </i>
        </button>
      </div>

      <div className = "mdl-card__supporting-text mdl-card__actions mdl-card--border">
        { children }
      </div>
    </div>
  );
}

function Cloak(props) {
  return props.hide
    ? null
    : props.children;
}

I'm going to move the global event listening out of a component and into the top level. This is a common pattern in React: to have all your shared state at the top-level, passed down via props.

let state = {
  showRemixer: false,
};

function toggleVisibility() {
  state = Object.assign(
    {}
    state,
    {
      showRemixer: !state.showRemixer
    }
  );

  rerender();
}

function rerender() {
  ReactDOM.render(
    <Cloak
      hide = { !state.showRemixer }
    >
      <RemixerRouter/>
    </Cloak>,
    container
  );
}

rerender();

function onKeyDown(event) {
  switch (event.keyCode) {
    case KeyCode.ESC:
      toggleVisibility();
      break;
  }
}

function onMessage(event) {
  switch (event.data) {
    case MessageType.TOGGLE_VISIBILITY:
      toggleVisibility();
      break;
  }
}

document.addEventListener(KeyboardEventType.DOWN, onKeyDown);
Messaging.register(onMessage);

For such a simple example, this is probably fine. However, as the app grows, you might find you want more order. That's where Redux comes in. It's this same pattern, but organized:

Redux is conceptually really simple. You have a high-level state atom (effectively just a tree of data you want to keep track of). Yours might be {showRemixer: true, showOptions: false}. Then, you have reducers that operate on that state. You might have this:

export function toggleVisibility(oldState = {showRemixer: false}, action) {
  switch (action.type) {
    case MessageType.TOGGLE_VISIBILITY:
      return (
        {
          ...oldState,
          showRemixer: !oldState.showRemixer
        }
      );
  }
}

The messages you are currently sending are called "actions" in Redux parlance. Every time it receives one, it calls the reducer to get an updated state. It then passes that updated state into your component tree (Cloak in our example above) to trigger a rerender. As you can see, this is the same thing I've done in the top-level of the example, just a bit more structured.

Imagine you wanted to add a keybinding to toggle the options. You could do so with a reducer that looked like this:

export function toggleOptions(oldState = {showOptions: false}, action) {
  switch (action.type) {
    case MessageType.TOGGLE_OPTIONS:
      return (
        {
          ...oldState,
          showOptions: !oldState.showOptions
        }
      );
  }
}

Your router simplifies to just this:

function RemixerRouter(props) {
  return (
    <RemixerPanel>
      {
        props.showOptions
          ? <Options />
          : <VariableConfigurator />
      }
    </RemixerPanel>
  );
}

RemixerPanel uses the toggleOptionsAction to handle clicks on the menu button, and your key listener calls it whenever someone hits the ? key. Redux would pass showOptions into RemixerRouter as a prop.

Redux is a 7K dependency. Since this is a developer tool, that's probably not an impactful number. For cleanliness/maintainability, it may be worth adopting.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant