Setting root React component props through container attributes

Disclaimer: for this particular problem, there is a much simpler solution than that oulined here (see way below). Regardless, I’m documenting the complicated solution, because it will certainly become useful again in the future.

I’m working on a web application that archives typeset documents and images. Once a document is archived, users are able to view an HTML conversion of the document through the browser without having to copy the original file to their local machines. The main application window has a paginated list of archived documents. The user selects the document he wants to view and it opens in a new browser tab.

The root component on the main application tab opens a new tab in a manner similar to this:

1
2
3
4
5
6
7
<button type='button' className='btn btn-default btn-sm'
onClick={function(){
var w = window.open('page.html', '_blank');
w.addEventListener('load', function() {
parent.preview(doc._id, this);
});
}}>

page.html provides the scaffolding for the HTML representation of a PDF, DOC, or some other typeset document. In conventional React style, the root component is rendered to an element of your choosing. In this case, the element looks like this:

1
2
3
4
5
<div class="container">
<div class="starter-template">
<div id="page"></div>
</div>
</div>

Here’s the problem: the main application window either needs to

  1. download the HTML representation of the requested archived document, or
  2. obtain the mongo ID of the archived document so that it can be downloaded when page.html renders the root component

Either way, the problem is the same. I couldn’t set the page content or document ID until after page.html was loaded. For illustrative simplicity, suppose I proceed by obtaining the mongo ID. In this case, I need page.html to look like this after it’s loaded:

1
2
3
4
5
<div class="container">
<div class="starter-template">
<div id="page" docId="SomeMongoObjectID"></div>
</div>
</div>

Once page.html is fully loaded, a gebo-server-style request is sent to the archiver-agent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var DocumentList = React.createClass({
// ...
preview: function(id, windoe) {
this.refs.error.getDOMNode().style.visibility = 'hidden';
var previewMsg = {
sender: this.props.clientId,
performative: 'request',
action: 'archive.view',
content: {
resource: 'pages',
id: id,
},
access_token: localStorage.getItem(name + '-key'),
};
var parent = this;
var request = perform(previewMsg, function(err, data) {
if (err) {
parent.refs.error.getDOMNode().style.visibility = 'visible';
parent.setState({ errorMessage: 'Cannot preview that document', errorDetails: err })
return;
}
// !!!
var element = windoe.document.getElementById('page');
element.setAttribute('docId', data._id);
});
},
// ...
});

These lines set #page‘s attribute in page.html:

1
2
var element = windoe.document.getElementById('page');
element.setAttribute('docId', data._id);

But wait! That’s great and all, but how do I tell the root React component to wait until the docId attribute has been set?

Behold the MutationObserver!

This code was appended to the root component’s definition, which is rendered in page.html. It’s adapted from the example provided on the Mozilla page.

1
2
3
4
5
6
7
8
9
10
11
12
var target = document.querySelector('#page');
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
var container = document.getElementById('page');
React.renderComponent(
<Page startPage={container.getAttribute('startPage')} email={container.getAttribute('email')} />, document.getElementById('page')
);
});
});
observer.observe(target, { attributes: true });

This code simply waits until the #page element’s attributes change before rendering the root component in page.html.

Epilogue

You’ll have to take my word for it, but there were very good reasons why I initially pursued this complex solution. The way things worked out, however, allowed me to simply render the root component in page.html from the application loaded in the main window like this:

1
2
3
4
5
preview: function(id, windoe) {
// ...
React.renderComponent(<Page docId={data._id} />, windoe.document.getElementById('page'));
// ...
},

Obviously, this is a much cleaner solution than the one outlined above. Nonetheless, I know that which has been documented will become useful to me again in the future.