Working with React in Drafts

Since the launch of Drafts 5, I've been using the app as a makeshift JavaScript IDE on iOS while learning functional programming. This isn't necessarily what Drafts was built for, but as its capabilities have continued to expand, I've found it to be quite competent.

Recently, I decided to start learning ReactJS as well. Considering Drafts was already my JavaScript sandbox of choice on iOS, I wanted to see if this could be a viable platform for experimenting with React.

The setup below is not for the faint of heart - there's a fair bit of manual configuration involved. As such, there may be some things I've overlooked in posting to the shared action library. If you attempt to replicate this setup and find anything missing to make this work as expected, please let me know and I will try to post an update.

Additionally, as I’m just getting started with React, I make no guarantees as to the robustness of this setup. 😃

Overview

Once your environment is set up, you will begin with the React Apps workspace. This will show all drafts with the react tag. Each of these drafts represents a React app.

In this environment, a React app is essentially a call to a custom runReact() function. The function accepts a tag (used for identifying related JSX and CSS content), a name, and any relevant data that should be embedded in the app at launch (useful if you have content stored or processed in Drafts that you want to make available in your React app).

When the function runs, it will find all JSX drafts with the app tag (more on this later), transpile them with Babel, and then combine them together. Next, it will then combine all CSS drafts with the app tag. Finally, the data object will be converted into variable assignments with each attribute becoming a named variable accessible to the React code.

Once all the processing is completed, a temporary draft is created (using a custom react_html.txt template) and then presented through an in-app browser via the HTMLPreview scripting object. You can then interact with the app in a web browser like you would with any other React app.

Getting Started

Before starting, you should ensure you have a Drafts Pro subscription. This setup relies on the Workspaces feature along with several custom actions that you may need to modify if your environment is different than mine.

Installing Actions

Two separate actions need to be installed to support this React environment.

First, install the Keyboard - React - Apps action group. This provides the initial actions for creating a new React app, or working on an existing app.

For the second action group, you have a choice. You can either install a completely self-contained action group that has supporting React files embedded (Keyboard - React - Editing (React embedded)), or you can install a slightly simpler, smaller version (Keyboard - React - Editing) that depends on supporting files being available in Drafts’ script library folder, and loaded with both Global.require() and FileManager.readString()

The former is a quick and easy way to get started, but is more difficult to maintain if/when you want to update to newer versions of React/Babel. I use (and recommend) the second version. For this version, you will need to add the following files to your iCloud Drive/Drafts/Library/Scripts/ directory (as named below):

  1. babel.min.js
  2. react.min.js
  3. react-dom.min.js

You can put these files in place by using the Files app, or for the more adventurous, you can use something like Working Copy to sync a git repository with these files into the right location, making it easier to apply updates in the future. Here is my Drafts utilities repo that I use to make JavaScript libraries available within Drafts.

Custom Template

Next, set up a custom template named react_html.txt in the iCloud Drive/Drafts/Library/Templates/ directory with the following contents:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>[[name]]</title>
    <style>
      [[css]]
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/javascript">

    [[imports]]

    [[data]]

    [[reactApp]]

    </script>
  </body>
</html>

This template will be used to render the React app - a div with the ID "root" exists to contain the app. It also contains placeholders for the combined CSS content, relevant imports to support the app, and our embedded data we want to make available to the app.

Custom Workspace

Create a custom Workspace named "React Apps" - this name is referenced from the Stop Working on App action. If you want to change the name of the workspace, you will need to update that action.

The workspace should be configured to only include drafts with the tag "react", and should select the Keyboard - React - Apps as the default keyboard/action group.

reactEditorWorkspaceConfig

Working With React Apps

Now that everything is set up, you can start working with React Apps. When you switch to the React Apps workspace, both your action list and custom keyboard show two actions:

  1. New React App
  2. Work on App

Creating a New React App

When you tap the first button to create a new React app, you will be prompted for two pieces of input. The first is a name for your React app. The second is a unique tag for your app. This tag will be used to identify all the drafts related to a single app. By default, the tab will start with app- but this is not required.

Once you tap Create App, a new draft will be created with some default code for your app. This boilerplate code will be enough to get you started. If you would like to make some specific data available to your app, you can collect and process it here, and then pass it in as part of the data object.

Here is an example where I collect all drafts with a movie-json tag, parse them as JSON objects, and save the array of objects into a variable named movies. When the React app is run, it will have access to a variable named movies.

// Sample Movie Database

(() => {

  const movies = Draft.query('', 'all', ['movie-json'], [], 'name')
    .map(d => {
      const body = d.processTemplate('[[body]]');
      let obj = JSON.parse(body);
      obj['id'] = d.uuid;
      return obj;
    });
  
  const
    tag = 'app-movie-sample',
    name = 'Sample Movie Database',
    data = {movies};
  
  runReact(tag, name, data);
  
})();

If you want to follow along and use this example, these Drafts links will create a couple sample movie drafts in the format expected by this code:

Working on an App

If you have one or more drafts (React apps) in your workspace, you can select one of them and then tap Work on App. When doing so, the action executed will:

  1. Determine the unique app tag for the app 1
  2. Create and apply a new workspace filtered to this tag
  3. Set the keyboard and action list to Keyboard - React - Editing 2
  4. Save a local configuration file with the unique app tag, as well as the UUID of the main React app draft

If this is a new app, the only change you will notice is the change to the custom keyboard, which now includes 4 buttons. Excluding the keyboard switcher, the buttons from left to right are:

  1. Stop Working on App
  2. Run App
  3. New CSS
  4. New JSX

Stop Working on App will exit out of working on an app (by clearing the saved configuration) and return to the main “React Apps” workspace.

Run App will execute the main React app code, regardless of which draft is currently being viewed (e.g. CSS or JSX drafts). This is accomplished by loading the draft with the UUID in the saved configuration file and running its contents through eval().

New CSS and New JSX create new CSS and JSX drafts with the appropriate tags (css and jsx) so they are correctly processed when running the React app. It will also tag them with the unique app tag so these drafts are considered part of the current app.

Here are a couple example files (CSS and JSX) to go along with the Movie Database app.

Example CSS

/* Movie Database - CSS */

body {
  font-family: -apple-system;
}

.movie {
  border: 1px solid #808080;
  margin: .5em 0;
  padding: .5em; 
  background: #EEEEEE;
}

ul.movieList li {
  list-style-type: none;
  width: 100%;
  max-width: 225px;
  border-radius: 5px;
  float:left;
  margin: .5em;
}

ul.movieList {
  padding-left: 0px; 
}

Example JSX

// Movie Database - JSX

class Movie extends React.Component {
  render(){
    let movie = this.props.movie;
    return (
      <li className="movie">
        <strong>Title: </strong>
        <span className="title">{movie.title}</span>
        <br />
        <strong>Rating: </strong>
        <span className="rating">{movie.rating}</span>
        <br />
        <strong>Actions: </strong>
        <MovieActions id={movie.id} />
        <br />
      </li>
    );
  }
}

class MovieActions extends React.Component {
  render(){
    return <a href="#">Loan</a>;
  }
}

class MovieList extends React.Component {
  render(){
    let movies = this.props.movies;
    let rows = movies.map(m => <Movie movie={m} key={m.id} />);
    return (
      <ul className="movieList">
        {rows}
      </ul>
    );
  }
}

class MoviesSummary extends React.Component {
  render(){
    let movies = this.props.movies;
    return (
      <div className="moviesSummary">
        <strong>Total: </strong>
        <span className="total">{movies.length}</span>
      </div>
    );
  }
}

class App extends React.Component {
  render(){
    return (
      <div>
        <MoviesSummary movies={this.props.movies} />
        <MovieList movies={this.props.movies} />
      </div>
    );
  }
}

ReactDOM.render(
 <App movies={movies} />,
  document.getElementById('root')
);

Putting it All Together

If everything was set up correctly, and you have some movie drafts tagged with movie-json, you should get something like this once you run the app:

reactEditorSampleApp

Going Forward

While this was a fun experiment, I'm not sure how useful it will be going forward. I've found that trying to debug React code without browser plugins is a bit of a challenge. While coding mistakes that break at the transpiling step will bubble up as an error, mistakes within the React code result in a blank preview with no additional details as to the cause of the failure.

Nonetheless, I'm going to keep playing around with this setup to see how far I can take it. At a minimum, I'm going to try and build out the Movie Database example I started to solve a personal need and see where this goes along the way.


  1. The first tag that is not react is used as the app tag.

  2. Or the (React Embedded) version if the simpler version isn’t found.