Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Software tooling & tips

Mastering React data flow: integrating with DOM-mutating plugins

Discover how to handle React's data flow when using plugins that mutate the DOM. Learn practical tips and avoid common pitfalls with this guide.
Dean Radcliffe
|
January 31, 2016
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Early on in one’s ReactJS days, you learn of one-way data flow and commit this mantra to memory:

The UI is a (pure) function of the state

When you adhere to this 100%, you no longer have to ask the question “What is my source of truth?” You know that state—consisting of props, and an optional overlay layer called state—is turned into DOM through a method called render that returns VirtualDOM. React then applies that new VDOM to the real DOM in clever and minimal ways. As long as React is 100% in charge of that DOM underneath that mounted component, that’s all you need to know.

But DOM mutations can occur in many ways. You could be using a jQuery plugin or Chrome Extension that mutates the DOM without React knowing. Or, simply, a user may choose an option from a <select> , and now that DOM is in a state React is not aware of.

Those who caution to “never mutate the DOM underneath React”, miss out on having a backup plan if they are not able to Reactify All The Things. Personally, I find the utility of existing jQuery plugins too compelling to opt to rewrite each one I use immediately—so I set out to detail a hybrid option.

Act 1: Setting the stage

Our investigation starts with the following scenario: A nested list is created as an object, then given to React, via props. Next, render turns this state into DOM as a series of lists and list-items with classes that the Nestable jQuery plugin wants. Lastly, we want to initialize the Nestable plugin to make the lists drag-and-drop and reorderable.

We decide that after mounting our component, we’ll initialize the Nestable plugin on the rendered output. Keep in mind that—for this jQuery plugin—its source of truth is what’s in the DOM, and it gives you a method you can call to get an object representation of that DOM—but we’ll talk about that later.

componentDidMount: function() {
  $('.dd').nestable()
}

Here’s how this works:

 

It appears to work fine from the user’s point of view, but notice that after we drag a node around—the React Inspector is not aware of the change. This breaks our sync, and leads to confusion. Let’s try and remedy that.

Act 2: Closing the loop

“No problem,” you say. You’re cool as a cucumber, knowing that in a situation like this, we need an event handler to feed the current value of the tree back into React. You even know to avoid state except in container components, and use props as much as possible. So you look up the Nestable plugin’s docs, and write the following:

componentDidMount: function() {
  var $dd = $('.dd').nestable().on('change', () => {
    this.props.onChange(this.getCurrentTree())
  })
},
getCurrentTree: function() {
  return $('.dd').nestable('serialize');
}

Yeah! You are feeding the tree (as given by nestable’s serialize method), into a function (which you accept via props), and that function can push the props back down to the component. Your feel great, with your code looking like this:

const container = document.getElementById("nestable");

const nestableChanged = (newTree) => {
  ReactDOM.render(
    <Nestable data={newTree} onChange={nestableChanged} />,
    container
  )
}

// on first load
ReactDOM.render(
  <Nestable data={exampleData()} onChange={nestableChanged} />,
  container
)

But when you start to test its behavior now, you get a sinking feeling, your pulse races, and your blood pressure shoots up a couple dozen points. Something is not right.

Act 3: Something’s rotten in the state of the DOM

Unfortunately, we have two very anomalous behaviors, indicated by the animations below:

Error 1: Tucking one subtree under another causes the subsequent subtree to vanish!

 

Error 2: Dragging a node out to the root adds it twice!

 

While these errors appear unrelated on the surface, they are essentially the same kind of error caused by the phase of the React lifecycle called Reconciliation.

Act 4: Reconciling with the past

Remember that React has an internal model of state, and when we update props or state, render is invoked again to return a new VDOM. This VDOM gets compared to the current state, as well as the current DOM, and changes flow to the real DOM.

You can make some adjustments to this part of the process in the React LifeCycle method shouldComponentUpdate:

shouldComponentUpdate: function(nextProps, nextState) {
  // well, should it update? return false if not
}

Let’s now explain what happened in the first error above from the point of view of React, which has to apply changes to the DOM:

  1.  Looking at the difference between the old and new VDOM, it appears that the second child of the root has been deleted
  2.  React deletes the second child of the root as it should, but due to our user and plugin’s changes, that node has already been moved out of the way, and React’s deletion applies to the wrong node

You can reason out the second error for yourself, based on the same logic of trying to propogate an update without realizing that it’s already been taken care of.

The nodes use key, which is a best practice for being able to let React identify them in a list, using something other than their position, but it’s not enough, because in fact we’re at an edge-case of React’s diffing algorithm:

In the current implementation, you can express the fact that a sub-tree has been moved amongst its siblings, but you cannot tell that it has moved somewhere else.

Since we’ve written our React component to fully be determined from props, what’s safe to do—and what we really want to do—is clobber the old DOM and then re-render. Don’t try using jQuery’s .empty() either… Trust me, dragons that way lie, in the form of:

Invariant Violation: processUpdates(): Unable to find child 1 of element. This probably means the DOM was unexpectedly mutated

That’s not helpful. So let’s look at what works.

Act 5: Denouement

It’s actually been right under our nose all along. Not a lifecycle method, not a configuration option, but the same way we got React markup into the DOM from the beginning. We mounted React to the DOM initially, so we can completely clear up its state by unmounting.

const container = document.getElementById("nestable");

const nestableChanged = (newTree) => {
  ReactDOM.unmountComponentAtNode(container) // <- This!
  ReactDOM.render(
    <Nestable data={newTree} onChange={nestableChanged} />,
    container
  )
}

While this may seem crude, it actually ensures that any state the user sees is generated from React’s render method. In other words:

The UI is a (pure) function of state

We’re simply allowing that to be untrue momentarily while the user interacts with us, and we’re taking the performance hit of clobbering more DOM than we theoretically need to, in order to preserve logical correctness that is easy to reason about, and to use a plugin that makes the grievous mistake of not being compatible with every JS framework that wasn’t even out yet when it was written.

Reconciled and happy

Our problem has two parts: (1) not knowing how to correctly blow away the DOM under React, and (2) not knowing that our particular plugin comes up against edge cases inherent in React.

But now we’re empowered. If we are sticking to props instead of state, and find ourselves in a corner where React’s VDOM reconciliation fails to clear out old nodes correctly, we can unmount and remount and then we’ll know that we are back in sync after every render cycle.

The React team knows this needs to be done sometimes, and speaks to it:

Unfortunately not everything around you is built using React. At the root of your tree you still have to write some plumbing code to connect the outer world into React.

Problems like these will arise from time to time, but in the end I think the React model is the cleanest I’ve seen. I have to give a nod to some of its predecessors, though: KnockoutJS view models and reactive-coffee are libraries I’ve used in the past that follow these principles, and I’ve been using or contributing to these since 2010. Flowing truth from objects to DOM is definitely the way to stay happy.

You just need to be aware of a few edge cases, and sometimes learn some of the implementation details of the framework you’re working with.

p.s. Thanks to Gordon Kristan, from Sprout Social whose presentation on this topic at Chicago React inspired this post.

p.p.s. Play with a JSBin for this article

Related Insights

🔗
The nine best recommendations in the new React docs
🔗
Finding the right React component in the MUI design system
🔗
How to create custom linting rules in React Testing Library

Explore our insights

See all insights
Leadership
Leadership
Leadership
Why we coach the system, not just the team

Slow delivery isn’t usually about your people—it’s about your system. Shifting focus to incremental improvements in the system helps change not just processes but behaviors for lasting change.

by
Doc Norton
Developers
Developers
Developers
Developer QA checklist for feature releases

Quality Assurance is a mindset integrated throughout development to catch issues early, build user trust, and reduce maintenance costs. These recommended procedures for dev teams without dedicated QA roles establish collective responsibility for ensuring feature stability, functionality, and usability before release.

by
Lee Quarella
Developers
Developers
Developers
From engineer to consultant: The powerful shift from inward to outward focus

What transforms a skilled software engineer into an exceptional consultant? Approach new codebases with respect rather than judgment, embrace constraints as creative boundaries, and prioritize client needs over personal preferences.

by
Dave Mosher
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
No items found.
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.