So in the last section we built a React reconciler and saw how it manages the lifecycle of our renderer.
In part two, we'll create a public interface to the reconciler. We will design our component API and will also build a custom version
of createElement
method.
For our example, we'll only implement one component Text
. A Text
component is the main component for adding the text to our document.
Text component, however, doesn't create specific text node. It has different semantics when compared to the DOM APIs.
We will first create a root container (remember rootContainerInstance
in our reconciler ?) for our components. This is responsible
for creating a document instance for officegen.
WordDocument.js
import officegen from 'officegen'
// This creates the document instance
class WordDocument {
constructor() {
this.doc = officegen('docx')
}
}
export default WordDocument
Now let's create our Text
component.
Text.js
class Text {
constructor(root, props) {
this.root = root;
this.props = props;
this.adder = this.root.doc.createP();
}
appendChild(child) {
// Platform specific API for appending child nodes
// Note: This will vary in different host environments. For example - In browser, you might use document.appendChild(child)
if (typeof child === 'string') {
// Add the string and render the text node
this.adder.addText(child);
}
}
}
export default Text;
Let's see what's going on here!
constructor()
In our constructor
, we initialise the root
instance and props
. We also create a reference to our doc
instance which we created earlier in WordDocument.js
. This reference is then
use to create paragraph by adding text nodes to it.
Example -
this.adder.addText(__someText__)
appendChild
This method appends the child nodes using the platform specific function for docx
i.e appendChild
. Remember we used this in our reconciler's appendInitialChild
method to check whether the
parent instance has a method called appendChild
or not !?
appendInitialChild(parentInstance, child) {
if (parentInstance.appendChild) {
parentInstance.appendChild(child);
} else {
parentInstance.document = child;
}
}
Along with appendChild
method, you can also add removeChild
method to remove child nodes. Since our host target does not provide a mutative API for removing child nodes, we are not using this method.
For this tutorial, the scope is kept limited for
Text
component. In a more practical example, you might want to validate the nesting of components too.
- Do not track the children inside an array in your class component API. Instead, directly append them using specific host API, as React provides all the valuable information about the child (which was removed or added)
This is correct
class MyComponent {
constructor(rootInstance, props) {
this.props = props
this.root = rootInstance
}
appendChild(child) {
some_platform_api.add(child)
// In browser, you may use something like: document.appendChild(child)
}
}
This is incorrect
class MyComponent {
children = []
constructor(rootInstance, props) {
this.props = props
this.root = rootInstance
}
appendChild(child) {
this.children.push(child)
}
renderChildren() {
for (let i = 0; i < this.children.length; i++) {
// do something with this.children[i]
}
}
render() {
this.renderChildren()
}
}
- If you're rendering target does not provide a mutate method like
appendChild
and instead only lets you replace the whole "scene" at once, you might want to use the "persistent" renderer mode instead. Here's an example host config for persistent renderer.
This is similar to the React.createElement()
for DOM as a target.
createElement.js
import { Text, WordDocument } from '../components/index'
/**
* Creates an element for a document
* @param {string} type Element type
* @param {Object} props Component props
* @param {Object} root Root instance
*/
function createElement(type, props, root) {
const COMPONENTS = {
ROOT: () => new WordDocument(),
TEXT: () => new Text(root, props),
default: undefined
}
return COMPONENTS[type]() || COMPONENTS.default
}
export { createElement }
I think you can easily understand what's happening inside the createElement
method. It takes an element, props, and the root instance.
Depending upon the type of element, we return an instance based on it else we return undefined
.
We're done with the part two of our tutorial. We created the API for our two components (Document
and Text
) and a createElement
method to create an element. In the next part, we will create a render method to flush everything to the host environment.