I’ve done a presentation at JSConf JP 2019! This is the first JSConf in Japan so I’m happy to have a talk there! Thank you, @jsconfjp!!

My presentation is “Make it declarative with React”.

https://jsconf.jp/2019/talk/toru-kobayashi

The agenda is here.

Benefits of Declarative Programming for UI

Custom renderer of React

Live Demo!

You can find my slide and demos in this repository!

My presentation wasn’t useful for your React applications. But I hope that you enjoyed!

My presentation is built by hiroppy/fusuma. You can write your presentation with MDX. Its presentation mode is also great! Thank you @about_hiroppy!

In this talk, I’ve created a custom renderer, of which implementation is straightforward. You can define a file structure using the following.

const React = require('react');

const { ReactFS } = require('@koba04/react-fs'); const targetDir = "test-react-fs-project"; ReactFS.render(

<>

<file name="README.md">

# Title

</file>

<directory name="src">

<file name="index.js">

console.log("Hello");

</file>

</directory>

</>,

targetDir

);

You can see more details on the following repository.

I had a speaker note because this was my first conference talk in English. So you can see what I’ve presented at the conference through the speaker note :).

Creating a custom renderer is easier than you think. So I’m happy if you thought that you want to try to create a custom renderer after reading this.

How to create a custom renderer

Let’s take a look at how to create a custom renderer. Here is almost everything of react-fs .

HostConfig

HostConfig is a configuration for your custom renderer. You should implement logic to connect React and an environment for your renderer.

The following implementation is quite simple. So you would understand what the HostConfig does.

import {

Instance,

HostContext,

Props,

Container,

TextInstance,

Type,

UpdatePayload

} from "./fs-renderer-types";

import path from "path";

import { writeFileSync, existsSync, mkdirSync, renameSync } from "fs"; const HOST_CONTEXT: HostContext = {}; export const getPublicInstance = (instance: Instance) => {

const { rootContainerInstance, ...rest } = instance;

return rest;

};

export const getRootHostContext = (): HostContext => HOST_CONTEXT;

export const getChildHostContext = () => HOST_CONTEXT; export const prepareForCommit = () => {};

export const resetAfterCommit = () => {};

export const createInstance = (

type: string,

props: Props,

rootContainerInstance: Container

): Instance => ({ type, props, rootContainerInstance });

export const createTextInstance = (

text: string,

rootContainerInstance: Container

): TextInstance => ({ text, rootContainerInstance });

export const appendInitialChild = (

parentInstance: Instance,

child: Instance | TextInstance

) => {

child.parent = parentInstance;

};

export const finalizeInitialChildren = () => true;

export const prepareUpdate = () => ({});

export const shouldSetTextContent = () => false;

export const shouldDeprioritizeSubtree = () => false; export const appendChildToContainer = () => {}; const buildParentPath = (instance: Instance | TextInstance) => {

const names = [];

let current = instance.parent;

while (current) {

names.push(current.props.name);

current = current.parent;

} return path.join(instance.rootContainerInstance.rootPath, ...names.reverse());

}; export const commitMount = (

instance: Instance,

type: Type,

newProps: Props

) => {

const parentPath = buildParentPath(instance);

const targetPath = path.join(parentPath, newProps.name); if (!existsSync(parentPath)) {

mkdirSync(parentPath, { recursive: true });

} if (type === "file") {

writeFileSync(targetPath, newProps.children);

} else if (type === "directory" && !existsSync(targetPath)) {

mkdirSync(targetPath);

}

}; export const commitUpdate = (

instance: Instance,

updatePayload: UpdatePayload,

type: Type,

oldProps: Props,

newProps: Props

) => {

if (oldProps.name !== newProps.name) {

instance.props = newProps;

renameSync(

path.join(buildParentPath(instance), oldProps.name),

path.join(buildParentPath(instance), newProps.name)

);

}

}; export const commitTextUpdate = (

textInstance: TextInstance,

oldText: string,

newText: string

) => {

if (oldText !== newText) {

textInstance.text = newText;

writeFileSync(buildParentPath(textInstance), newText);

}

};

export const removeChild = () => {};

export const appendChild = (

parentInstance: Instance,

child: Instance | TextInstance

) => {

child.parent = parentInstance;

}; export const scheduleDeferredCallback = () => {};

export const cancelDeferredCallback = () => {};

export const setTimeout = global.setTimeout;

export const clearTimeout = global.clearTimeout;

export const noTimeout = {};

export const now = () => Date.now(); export const isPrimaryRenderer = true;

export const supportsMutation = true;

export const supportsPersistence = false;

export const supportsHydration = false;

https://github.com/koba04/react-fs/blob/master/src/fs-renderer-host-config.ts

Creating a renderer

This file creates a renderer from a HostConfig and a type definition.

import Reconciler from "react-reconciler";

import * as HostConfig from "./fs-renderer-host-config";

import {

Type,

Props,

Instance,

TextInstance,

HydratableInstance,

PublicInstance,

HostContext,

UpdatePayload,

ChildSet,

TimeoutHandle,

NoTimeout,

Container

} from "./fs-renderer-types"; // eslint-disable-next-line new-cap

export const FSRenderer = Reconciler<

Type,

Props,

Container,

Instance,

TextInstance,

HydratableInstance,

PublicInstance,

HostContext,

UpdatePayload,

ChildSet,

TimeoutHandle,

NoTimeout

>(HostConfig);

https://github.com/koba04/react-fs/blob/master/src/fs-renderer.ts

Exposing public APIs from your custom renderer.

This file is the entry point for react-fs . You have to create a renderer and container to render ReactElement.

import React from "react";

import { FSRenderer } from "./fs-renderer";

import { Container } from "./fs-renderer-types";

import ReactReconciler from "react-reconciler";

import { rmdirSync } from "fs"; type RootContainer = {

fiberRoot?: ReactReconciler.FiberRoot;

container: Container;

}; const rootContainerMap = new Map<string, RootContainer>(); export const ReactFS = {

render(element: React.ReactNode, rootPath: string) {

let rootContainer = rootContainerMap.get(rootPath);

if (!rootContainer) {

rootContainer = {

container: {

rootPath

}

};

rootContainerMap.set(rootPath, rootContainer);

} // First, we remove the root to clean up.

// TODO: support hydration

rmdirSync(rootPath, { recursive: true }); rootContainer.fiberRoot = FSRenderer.createContainer(

rootContainer.container,

false,

false

);

FSRenderer.updateContainer(

element,

rootContainer.fiberRoot,

null,

() => {}

);

}

};

https://github.com/koba04/react-fs/blob/master/src/index.ts

That’s it! It’s straightforward, isn’t it!

A Custom renderer also supports other cases like Persistence mode and Hydration, but it’s a good start point to create their custom renderer!

Let’s make anything declarative :).