The New React-based Brackets Project Tree

As I’ve written here previously, I’ve been working on using React within Brackets to make parts of the UI easier to manage, specifically starting with the file tree that appears in the sidebar. I’m now actively working on getting that work into shape for landing. There’s an initial pull request up, but that one will be changing to retarget master once split view lands, which may be as soon as today.

This article is primarily geared toward Brackets core developers, the few extension developers that have targeted the project tree and possibly people interested in integrating React in large, pre-existing JavaScript projects. I’ll start from the front end, jump to the parts that feed the front end and end with ProjectManager, the file that started it all.

The Most Important Thing

Perhaps the most visible aspect of this change is that I’m replacing jstree with a custom React-based tree. That change alone helps to simplify the code, but it is not the most important change of the “ProjectManager revamp”.

The most important change for this piece of work is splitting the logic from the UI. Writing tests for the behavior in ProjectManager previously, or even just making sense of what happens where, was difficult because UI events were blended in with logic for filesystem updates and integration with other parts of Brackets.

Also, I’m a big fan of Gary Bernhardt’s Boundaries talk and the idea of having a “functional core and integration shell” (Gary calls the shell an “imperative shell”). You can increase testability by pushing integration with the rest of your system out to the edges and having as much of your code be structured as “take input, create output” as possible. Breaking the UI and non-UI bits apart is one element of this.

Flux

Since I initially started looking at using React to replace the file tree in Brackets, Facebook has introduced the Flux architecture. The way React works allows developers to think differently about how they structure their applications, and Flux struck me as a generally sensible approach. The blog post Avoiding Event Chains in Single Page Applications goes into some detail about why Flux can be better than just firing events back and forth between objects.

All of that said, there are two aspects of the work I was doing that led me to only do “pretend Flux”:

  1. Brackets is not a new, greenfield project
  2. Right now, the introduction of React and “stores” is limited to this one part of Brackets

I wanted to introduce React and Flux into the Brackets codebase, make ProjectManager easier to reason about and fix bugs, but I certainly didn’t set out to change everything. In fact, the existing public API of ProjectManager hasn’t changed at all.

I have created an object that fits the ActionCreator part of the Flux picture.

But my ActionCreator talks directly to the store (ProjectModel) and there’s no Dispatcher, which is actually the central piece of Flux. It would be possible to put a Dispatcher in with little change to the rest of the code, though. It’s not clear to me at this point that filling in the rest of the Flux picture is the right approach for Brackets and I’d like to see how it evolves.

With all of that background, let’s take a quick tour of the parts of the new ProjectManager.

FileTreeView

The FileTreeView (henceforth “FTV”) is the React piece. It generates what you see and handles user interaction via a collection of React components. Though I like JSX, I opted to not use it at this point because it would take a bit more work to make Brackets deal well with JSX.

FTV generates the basically the same DOM structure with the same classes as the old jstree-based ProjectManager, allowing me to not worry about styling concerns at this point.

There are quite a few Brackets extensions that add to Brackets by extending the DOM directly, but that has never been officially supported. Providing compatibility guarantees would be just too difficult. As a general rule that now applies to the file tree, using jQuery to manipulate a DOM that React is managing is a bad idea.

So, I looked at extensions that were reaching into the DOM for the #project-files-container. Thankfully, there weren’t many. For the purposes of illustration, I modified two extensions (Brackets-File-Icons and brackets-git) to develop supportable JavaScript APIs for the kinds of UI extensions they were doing. My branch of Brackets-File-Icons uses the FileTreeView.addIconProvider function to be able to add icons to the files:

FileTreeView.addIconProvider(function (entry) {
	if (!entry.isFile) {
		return;
	}

	var ext = FileUtils.getSmartFileExtension(entry.name);
	data = fileInfo.hasOwnProperty(ext) ? fileInfo[ext] : getDefaultIcon(ext);
	var ins = React.DOM.ins({
		className: "jstree-icon file-icon", 
		style: {
			color: data.color,
			fontSize: (data.size || 16) + 'px'
		}
	}, data.icon);
	return ins;
 	});

Though extensions do not have completely unlimited power, this new API is easier to use and supportable. It’s still quite flexible, too, because the icon provider can return whatever React components it wants.

The code in my branch of brackets-git does not even need to return React components. It just needs to return classes that are going to be added to the file tree:

    FileTreeView.addClassesProvider(function (data) {
        var fullPath = data.fullPath;
        if (isIgnored(fullPath)) {
            return "git-ignored";
        } else if (isNew(fullPath)) {
            return "git-new";
        } else if (isModified(fullPath)) {
            return "git-modified";
        }
    });

FileTreeViewModel

The FileTreeViewModel (FTVM) is the “store”, in Flux terminology, that feeds the FTV. It provides a structure that maps very cleanly to what the UI needs to display so that the React components can worry strictly about presentation details and can run completely synchronously.

One interesting aspect of the FTVM is that it stores its data in an Immutable map. The immutable package was created by a Facebook developer, like React itself. It has a friendly JavaScript API for efficiently and conveniently managing persistent data structures.

By using persistent data structures, the FTV knows which parts of itself to rerender just by checking object identity. If an object doesn’t change, the view on that object doesn’t need to either. The UI portion of the file tree should be very, very fast.

I actually considered eliminating FTVM as a premature optimization and just having the FTV render its contents based on the filesystem objects. But, the FTVM is a very easily testable way to represent the data that is going to appear on screen and I think it’s a win on balance.

ProjectModel

The ProjectModel is a bit bigger picture than the FTVM. It is responsible for managing the FTVM, but it is also responsible for providing the API for the ActionCreator and managing other data APIs for the currently opened project.

In order to break the connection between the UI and non-UI parts, ProjectModel uses events and promises to communicate back with ProjectManager.

In contrast with many parts of Brackets, ProjectModel does not store the project state in module-level variables even though it is used as a singleton in practice. I opted to create a ProjectModel class and instantiate a single instance of that in ProjectManager. This allows me to write tests for ProjectModel methods without having to resort to module management trickery which would be required if the state was all kept at the module level.

ProjectManager

The file that used to contain everything is now principally responsible for gluing everything together. To repeat what I talked about earlier, the idea put forth in “Boundaries” is that you want to keep the code that integrates the parts of your system together as simple as possible, because it’s hard to write tests for code that depends on the whole system.

For the most part, ProjectManager now mediates between user interface parts and the ProjectModel. For example, the function in ProjectManager that used to create a file was also responsible for showing a dialog if that file creation failed. Now, ProjectModel is responsible for the file creation and ProjectManager will display an error dialog if that headless operation fails.

The loadProject function is still in ProjectManager and I think it could benefit from being split up into separate parts. But, there’s only so much time in the day and this project is out of time.

That’s it!

We’re shooting to land this change in Brackets 0.44. Code review is only just beginning, so feel free to add your specific code comments to the review or more general discussion in the thread on brackets-dev.