Web Log of Ross Chapman

Web Log of Ross Chapman

React inline function gotcha, but in a non-obvious way

I think a lot about Hillel Wayne’s blog post INSTRUCTIVE AND PERSUASIVE EXAMPLES: an interpolative critique of a best practice article on unit testing.

Wayne argues that “instructive” examples don’t make a reader care. In contrast, we should labor harder to craft “persuasive” examples that attempt to satisfy simian desiring machines:

  1. If your example is too simple, people brush it off.
  2. If your example is too complicated, people won’t follow. They’re also less likely - to trust it, because you complicated example might be concealing a simpler way.
  3. If your example is too abstract, people won’t see how it’s useful in practice.
  4. If your example is too concrete, people won’t see how it generalizes to their own - problems.

We see examples of #1 and #4 all of the time in technical writing – maybe because a lot of it is written for marketing purposes.

Extrapolating Wayne’s plea: let’s be careful about how we present anti-patterns and best practices when we’re trying to thought leader. Often this writing ends up being spec or framework documentation plagiarized and adorned with gifs. But without contextual complexity we may be doing a disservice to our fellow humans who dev – especially less experienced devs.

Persuasive examples are harder but the payoff is bigger. By demonstrating the why and why not there’s a better chance of putting knowledge into your reader’s brain and making it stick. This is certainly my experience. If you:

  1. Share the reasoning behind all of the small decisions getting from A to B, or determining why A or B is important
  2. Give me code that looks more like code “in the wild”
  3. Contextualize your example as either a novel approach or re-visitation

To wit. (An attempt at a persuasive example.)

Just this week I toiled on a bug with a colleague that ultimately turned out to be a case of a classic React anti-pattern where an inline function declaration caused undesirable re-renders of a Pure Component.

Now, I’ve definitely read at least a 3–4 instructive articles on the pitfalls of declaring functions inside of render(). Perf implications and unnecessary function creation, etc…. But that insight didn’t help me – or my similarly schooled – colleague come to resolution with any alacrity. We failed spectacularly to bring our academic rigor to bear because we weren’t working with sandboxed toy code from instructive examples. The root cause became obvious only after hours of thought-work finally revealed where the problem code even was.

Again, finding problematic inline functions in something like this triviality of Hillel’s lament would, of course, be much easier:

render() {
    return (
        
    )
}

But in our case, the problem function was buried in a nested component wrapped in a separate constructor function and abstracted into a separate helper in a separate file and blah dee blah…you get the point. Our journey was starting miles from render().

Our code better resembled:

// Template.js
export default (...props) => {
    // ...
    const Sidebar = connectSidebar(Sidebar);
    const Main = connectMain(Main);

    return ;
}

// Page.js
import Template from `./Template`;

export class Page extends Component {
    // ...
    render() {
        return (
            <- Template {...props} />
        )
    }
}

The bug was observed when a user clicks the “Buy on Map” button in the Sidebar. The click was getting swallowed and not transitioning the view.

So what was going on?

When we dug deeper we noticed that at the moment of click an unwanted blur event would be triggered on the form field in the <Main /> component; which would update its state. This redux-form state update would then ripple through the component tree and cause the sidebar to be re-rendered, even though none of the props passed to <Sidebar /> had changed. The result was the sidebar button getting re-rendered in the middle of the click event; which meant the click handlers of the newly rendered button were not capturing the click and able to transition to the next view! Client concurrency issues.

Inline gotcha funtimes

One hack we considered was changing the onClick handler of the sidebar button to onMouseUp – the newly rendered button could seemingly receive that event just fine in the thrash sequence (browsers are weird). But we knew our bandage likely wouldn’t last long, o we decided to troubleshoot the real issue: the unnecessary re-renders of the sidebar upon field blur.

After binary searching the code up and down <Page />, deleting chunk by chunk until the re-renders stopped, we discovered the fix to be fairly straight forward. Just move the invocation of connectSidebar and connectMain outside of the Template function context into the module context.

// Template.js
const Sidebar = connectSidebar(Sidebar);
const Main = connectMain(Main);

export default (...props) => {
  // ...
  return ;
};

After this mod, when <Layout /> was rendered, the child component passed as the prop sidebar wouldn’t be invoked – it’s already been invoked and the return value of the component has been assigned. In other words, we are not creating a new function in memory and executing it fresh every time just like all those instructive examples say!

Sigh. We likely ended up in this place by not being careful with nested connectors and their subscription boundaries. Sometimes you forget that it’s just functions all the way down. Like, declaring and assigning a component constructor inside another component constructor would not be how you’d typically compose your functions. I will humbly accept this not uncommon (read: forgivable) symptom of “bottom-up” programming. (See David Kourshid’s talk about finite state machines and the sad stories of “bottom-up”).

In sum, unlike the pristine nirvanic fields of instructive examples, we make our bed in large projects born from large organizations – cue Conway’s law. The requirements for the application accrete in fantastic ways over time. Cue McConnell’s oyster farms. The primitives you start with to satisfy embryonic requirements, like a root-level <Page /> component, may just become one large prop-drilled well. Graph hell.

This means you suddenly find yourself debugging why a click event on a sidebar button is being swallowed. You notice the divs are flashing in the Elements tab of Chrome dev tools, which means the browser is repainting the sidebar on click – re-renders!

Ok, so instructive examples won’t necessarily help you. But I’ll add an asterisk and deign they aren’t worthless either. Because I’ve read these instructives, I am able identify and classify this bug post facto as a commonly known React problem – that is, a problem with React composition not a problem inherent to React – because the instructive examples are out there contouring a problem space and a shared vocabulary. I can then use this shared language when communicating the what or what caused this… during a retrospective or or incident report. Notwithstanding, we should endeavor to be better.