0

TL/DR: React components have two kinds of code:

  1. rendering code that draws the component, which depends on certain props that affect the component's visual appearance (call them "visual props"), and
  2. event-handling code, e.g., onclick handlers, which depends on certain props that don't affect the component's visual appearance (call them "event props").

When event props change, they cause the component to re-render, even though its appearance doesn't change. The only thing changing is its future event-handling behavior.

What's best practice for removing event props to avoid unnecessary re-renders, while still allowing intelligent event handling?

Longer version

My question is subtly different from this question about how to give handlers to dumb React components; see below for explanation.

I have an application with many React components (hundreds to thousands of SVG elements; it's a CAD application).

There are many "edit modes" in this application (imagine a drawing program like Inkscape): depending on the edit mode, you might want a left-click to select an object, or drag to draw a selection outline rectangle, or do any number of different edits to the component that was clicked, depending on the edit mode.

In my original architecture, every one of these components had the current edit mode as a prop. Each component would use the mode prop to decide what to do in response to events such as clicks: different sorts of Redux actions are dispatched in response to clicks depending on the current mode. This means that every time the user switches the edit mode, every component gets re-rendered, even though none of them change visually. In a large design, it takes several seconds to re-render.

I've altered it to improve performance. Now, each component is dumber: none of them know the edit mode. But this means they don't know what to do in response to a click. In some cases, I solved this by having each dispatch a "dumber" action that says essentially "I was clicked". Middleware intercepts this action, looks up the edit mode in the Redux store, and dispatches an appropriate smart action based on the edit mode. In other cases, I simply let the component dispatch the original action (e.g., Select), even if that action may not be valid for the current edit mode, and similarly rely on the middleware to intercept and stop the action if it is invalid for the current edit mode.

This solution feels inelegant. Now, many more actions get dispatched, even though most of them are thrown away. It's also nothing like what I find in introductions/tutorials to middleware, which mostly talk about how it's good for async stuff (I don't need any of this to be asynchronous since these actions generally are not talking to the network or files) and side-effects such as logging (no side-effects here; I simply want a user interaction to trigger a normal Redux action to be dispatched).

I feel as though a better solution would be to access the Redux store as a global variable within event handling code. I know this is emphatically not safe to do with rendering code, since it breaks the rule "React views should be a deterministic function of their props and state". But it feels safer to do with event-handling code.

I realize it's common with "very dumb" React components to pass click handlers in as a prop (e.g., this stackoverflow answer), but I don't see this as a solution. If handler has the edit mode encoded in it as a bound value, then the handler itself needs to change when the edit mode changes, which, since the handler is a prop, requires re-rendering the component. So I think this issue I'm describing is orthogonal to whether the handler is passed into the component as a prop, or written specifically for the component.

So to summarize, there's three options I see:

  1. Pass all data required for intelligent event handling as props. (causes unnecessary re-renders)

  2. Have React components dispatch actions "promiscuously", and rely on middleware (which has access to the Redux store) to stop and/or transform the action if necessary. (As I implemented it, is harder to understand, and puts lots of unrelated application logic in one place, where it feels like it doesn't belong. Also makes for a messier Redux history of actions, making it harder to debug using Redux DevTools, and is not a pattern I've seen in any documentation/tutorial on Redux middleware.)

  3. Allow event handler code (unlike rendering code) to access the Redux store as a global variable, to make intelligent decisions about what action to dispatch. (Seems okay, but scares me to use global variables in this way, and I'm worried that it could cause a problem I'm not seeing.)

Is there a fourth option I'm missing?

Dave Doty
  • 285
  • 1
  • 9

1 Answers1

0

I have an idea for how to solve this in a way that feels close to the Redux spirit. (Though I still lean towards accessing global variables in event handlers to solve the problem.)

Redux has some notion of "action creators", which is a function that returns an action object. This always seemed like an unnecessary layer of abstraction to me. But perhaps a similar idea can be used here. (I use Dart, not Javascript, so the code below is Dart; hopefully the answer makes sense.)

The idea is to have a new type of action in called ActionCreator<A extends Action> (subtype of Action). An ActionCreator<A> is an object with a method of type

A create(AppState state)

In other words, it takes the whole AppState and returns an Action. This lets it do the necessary data lookups. As an object, it can contain fields that describe data gathered from the code (usually View event handler code) that instantiated it. For example, it could reference a Selectable to select. create() returns either null or some special value to indicate that the action should be thrown away.

For example, if we have a click handler, we'd dispatch an ActionCreator

class Select {
  final Item item_clicked;

  Select(this.item_clicked);
}

class ClickedAction implements ActionCreator<Select> {
  final Item item_clicked;

  ClickedAction(this.item_clicked);

  Select create(AppState state) =>
    state.ui_state.select_mode_is_on ? Select(this.item_clicked) : null;
}

// ...

onClick = (event) {
  props.dispatch(ClickedAction(props.item));
}

And in middleware, once we have access to the full state, this can be turned into a concrete action, but only if it's legal. But the nice thing is that the next piece of code is generic and handles any such ActionCreator, so I wouldn't have to remember to keep editing this code whenever I create a new Action that needs to be "conditionally dispatched".

action_creator_middleware(Store<AppState> store, action, NextDispatcher next) {
  if (action is ActionCreator) {
    var maybe_action = action.create(store.state);
    if (maybe_action != null) {
      dispatch(maybe_action);
    }
  } else {
    next(action);
  }
}

The disadvantage of this is that it's still dispatching many more actions than we really need; most will get thrown away. It's a "cleaner" implementation of what I need, but I still think that for asynchronous event handlers, access the Redux store as a global variable is probably perfectly fine. I don't see in that any of the problems one would expect if the view code went outside of its React props and accessed global variables.

Dave Doty
  • 285
  • 1
  • 9