-
Notifications
You must be signed in to change notification settings - Fork 44
A React Tutorial for p5 Programmers
This guide is a work in progress. Feedback is welcome!
This is a React tutorial for people who are already familiar with p5.js. A basic conceptual familiarity with the Document Object Model is also highly recommended: if you're unfamiliar with it, consider reading p5's Beyond The Canvas tutorial.
This tutorial is a riff on p5's Get Started tutorial, because it's both familiar and fun.
Traditionally, most dynamic web applications are written by directly manipulating the DOM, attaching event listeners to it, and so forth. While this is fine for simple applications, as a program grows it can also quickly lead to very complex and error-prone code if one isn't really careful.
React is an alternative to directly manipulating the DOM. While it does offer some escape hatches to interact with third-party libraries or certain cutting-edge browser features, it largely serves as a layer of abstraction between your code and the page's actual DOM.
It does this largely by providing an abstraction called a React Component to encapsulate the functionality of a visible part of the page into a self-documenting, reusable building block. For instance, at the time of this writing, p5.js-widget contains the following components:
-
Editor
is the CodeMirror-powered widget that allows the user to tinker with p5 code. -
Preview
is the area that presents the user with their running sketch. -
Toolbar
is the area that provides controls to play and stop the user's sketch. -
App
brings all these components together and facilitates communication between them.
I decided to use React for p5.js-widget for a few reasons:
-
Like any good tool, it provides (what I believe to be) an incredible amount of leverage for a minimal amount of cognitive overhead. Unlike a lot of its contemporaries, the surface area of its API is fairly small, which makes it relatively quick to learn. Every feature of its design solves multiple problems I've had in the past when it comes to designing reliable and maintainable user interfaces.
-
It encourages best practices without mandating them. For example, it highly recommends writing pure functions without exploding if you decide not to follow its advice. Its documentation constantly nudges you towards understanding the benefits of immutability without absolutely requiring that you use immutable data structures. It encourages being explicit about how data flows through your app without forcing you to follow its conventions. It nudges you to have stateless components. All of this makes it easier to learn, because it allows you to engage with it at whatever level you're comfortable with, and then improve from there.
-
Its documentation constantly invites one to peek under the hood and learn how it works. This helps demystify things and allows me to better intuit where my bugs may be coming from when they inevitably occur (thanks to the law of leaky abstractions).
-
It has very good debugging messages: when it notices you doing something that it thinks might lead to problems, it logs a message to the console and often includes a link to learn more.
-
Since I started using it in 2014, it's been the only front-end library that hasn't either (A) left me in "analysis paralysis" mode as I fret over how to architect my app, or (B) resulted in lots of spaghetti code that makes me wish that I could just rewrite the whole thing from scratch. Thanks to the aforementioned best practices, I feel like the React-based apps I've written have consistently been reasonably maintainable, testable, self-documenting, and modular.
The following passage from the Why React? page on React's website helped me out when I was first learning it, so I'll pass it on here:
React challenges a lot of conventional wisdom, and at first glance some of the ideas may seem crazy. Give it five minutes while reading this guide; those crazy ideas have worked for building thousands of components both inside and outside of Facebook and Instagram.
Start out with the following HTML file:
<!DOCTYPE html>
<meta charset="utf-8">
<title>React Sketch</title>
<div id="sketch"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
<script src="react-sketch.js"></script>
And the following JS file:
// react-sketch.js
var Sketch = React.createClass({
render: function() {
return React.createElement('svg', {
width: '640',
height: '480'
}, React.createElement('circle', {
cx: '50',
cy: '50',
r: '40',
stroke: 'black',
fill: 'white'
}));
}
});
ReactDOM.render(
React.createElement(Sketch),
document.getElementById('sketch')
);
Loading this page should give you a stationary circle:
Let's examine the code you just wrote.
In the HTML, you're loading two libraries, React and ReactDOM. There actually used to be just one library, and the distinction between the two isn't actually completely obvious. You're welcome to read an explanation on StackOverflow, but it's totally fine if you skip it.
In the JS, you're doing a number of things:
-
You're calling a function called
React.createClass()
and assigning its return value to a variable calledSketch
. Here you're creating something called a React component, and it's the fundamental building block of React-powered programs. Components are nicely self-contained chunks of functionality that you can hook together to build awesome things. -
You're calling a function called
ReactDOM.render()
and passing a DOM node as its second argument. This basically "injects" your React program into the web page.
The Sketch
component has only one method called render()
, and it's actually a little bit like the draw()
method of a p5 sketch, with a few notable exceptions:
-
By default, it's not called 60 times per second. In fact, it's only called whenever the state of the component changes. State is just data which uniquely determines how the component should display itself--more on that later.
-
It generally doesn't call many functions itself, but rather returns a React Element which represents what the DOM controlled by React should look like. A React Element is not a full-fledged DOM element; more on that later, too.
Our render()
function is calling React.createElement()
, which is the primary mechanism through which React Elements are created. In our case, we're creating an <svg>
element with width
and height
attributes; the single child of this element is an SVG <circle>
element with cx
, cy
, r
, stroke
, and fill
attributes. Its HTML representation looks like this:
<svg width="640" height="480">
<circle cx="50" cy="50" r="40" stroke="black" fill="white" />
</svg>
It turns out that React rendering code is so peppered with calls to React.createElement()
that its creators made some syntactic sugar for it called JSX. In JSX, the following is literally equivalent to our Sketch
component's render()
method:
function render() {
return (
<svg width="640" height="480">
<circle cx="50" cy="50" r="40" stroke="black" fill="white"/>
</svg>
);
}
You can read more about JSX in React's documentation on JSX Syntax. JSX isn't required to use React, but it tends to make React code read more concisely.
So JSX is cool and all, but because it's not part of the JavaScript standard or anything, browser's won't understand it by default. So we'll need to use something called a transpiler to convert our JSX into JS.
We don't just want a transpiler for the JSX benefits, though; we'll also eventually want to take advantage of the newest version of JavaScript, ES2015, and it isn't fully supported across all major browsers yet. So our transpiler will help solve that problem too.
Right now we're going to use a transpiler called Babel, which works in the browser. To use it, we'll first rename react-sketch.js
to react-sketch.jsx
and then change our HTML a bit:
<!DOCTYPE html>
<meta charset="utf-8">
<title>React Sketch</title>
<div id="sketch"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
<script type="text/babel" src="react-sketch.jsx"></script>
The browser won't load our react-sketch.jsx
file itself, because it doesn't know what to do with a script of type text/babel
. Instead, Babel will find that tag, manually load our content itself, transpile it, and then tell the browser to run it. Cool! Sort of.
By now your head might be spinning a bit from all this new tooling and stuff. The field is actually moving so fast that some programmers are saying JavaScript development is crazy, which is a fair point!
But, some of these new technologies reflect really interesting ideas that can enlighten us even if we decide not to use them, so I'm going to try to spend the rest of this tutorial focusing on the "big picture" rather than the nitty-gritty details, which you can read up on elsewhere.
To make the circle move, we need to add some state to our Sketch component:
var Sketch = React.createClass({
getInitialState: function() {
return {x: 50, y: 50};
},
handleMouseMove: function(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
},
render: function() {
return (
<svg width="640" height="480" onMouseMove={this.handleMouseMove}>
<circle cx={this.state.x} cy={this.state.y} r="40"
stroke="black" fill="white"/>
</svg>
);
}
});
ReactDOM.render(
<Sketch/>,
document.getElementById('sketch')
);
In the above code, our state has two properties, x
and y
. These both start at 50
, because React automatically calls our component's getInitialState()
function when our component is first created.
Assigning to the onMouseMove
attribute in render()
is a lot like assigning to the onmousemove
attribute in HTML, only much more flexible and tied to the lifetime of our component.
So, when the user moves their mouse on our SVG:
- The browser notifies React, which then calls our event handler method,
handleMouseMove()
. - Our event handler calls
setState()
to update the state of our component. - React notices that our component's state has changed, so it calls our component's
render()
method to figure out what should change on the page.
From looking at the render()
method, you might assume that React is wiping out the existing DOM and replacing it with the one that render()
provides.
If this were the case, then React would be a lot slower than hand-crafted code that only changes the parts of the DOM that need changing.
Fortunately, this isn't the case. Open your browser's element inspector as you drag around the circle, and you will see something like this:
The purple background behind the values of cx
and cy
in the inspector reflect the fact that they're the only parts of the DOM that are changing as the circle is being dragged around: if the entire SVG was being re-rendered, the whole SVG would have a purple background.
This is happening because the React Element tree returned by render()
is a collection of lightweight JavaScript objects that represent what the DOM should look like. React also remembers what the last call to render()
returned, and it compares these two structures to determine what needs to be changed in the actual page's DOM. Then it applies only those changes.
React's lightweight representation of the DOM is sometimes called a Virtual DOM, and because most of its time is spent analyzing that--instead of looking at the real DOM, which is quite slow--rendering happens very quickly.
While this tutorial may be expanded in the future, for now this is all there is.
There's still more to learn, though! For now, I recommend starting with the Displaying Data tutorial on the React site, and following the "Next →" links at the bottom of each page from there.