React implemented in a Tweet!
24 Jul 2019Let’s implement the basic React API in a tweet, with React DOM & State management as a separate plugin in another tweet.
What we want, and shall get
- Top-down, one-way, state-driven rendering.
- Compose HTML primitives into higher-level components.
- Ability to write JSX to define components.
- Custom props for higher-order components.
- Event handlers that can change application state and trigger re-renders.
What we won’t get
- No virtual DOM diffing between old and new renders for efficient DOM updates, so re-renders will replace HTML content.
setState
won’t be asynchronous, but synchronous.- DOM event handlers won’t have React-ified names (like
onClick
), but default DOM names (likeonclick
). - Only necessary API surface area implemented, so yes
React.createElement
, and noReact.cloneElement
. - No React Hooks API.
Code
Here’s React defined as a global
React={Component:function(e){this.props=e},[C="createElement"]:(t,s,...a)=>t.bind?new t(s):(e=document[C](t),(()=>{for(n in s)e[n]=s[n]})(),g(a).map(t=>e[A](t.props?t.render():t.part?t:new Text(t))),e)},A="appendChild",g=(e=>(e=e||[],e.map?e.flatMap(g):[e]))
What does the above do?
- Defines a global
React
. - Defines
React.Component
, the component base class. - Defines
React.createElement
which can create both HTML primitives and custom components. React.createElement
also acceptsprops
, andchildren
which get appended to the created DOM element.- JSX essentially transpiles to
React.createElement
calls, with contents passed in aschildren
argument. Thesechildren
can be custom components, primitive HTML elements, or plain text. Code handles them all correctly.
WHAT! HOW?
Read the annotated, prettified code below for line-by-line explanation.
For now, moving on to more features!
React DOM + State management
You can bring in React DOM and React’s State management API come as a separate plugin, implemented in another tweet!
ReactDOM={render:(e,t)=>(e.h=t,t[A](e.render()))},React.Component.prototype.setState=function(e){this.state={...this.state,...e},this.h.innerHTML="",ReactDOM.render(this,this.h)};
What does the above define?
- A global
ReactDOM
. ReactDOM.render
function to trigger re-renders.React.Component#setState
to set component-level state that triggers a re-render of that component.
Demo
Let’s build React’s own Tic-tac-toe tutorial with React-in-a-tweet™.
You can fiddle with the following over at JSFiddle, or try it out right here!
Video of working example:
Usage code
Here’s how that sweet sweet demo is implemented.
Throw in a root HTML element:
<div id="root"></div>
Sprinkle some CSS to make things look right (from React tutorial):
body { font: 14px "Century Gothic", Futura, sans-serif; margin: 20px; }
ol, ul { padding-left: 30px; }
.board-row:after { clear: both; content: ""; display: table; }
.status { margin-bottom: 10px; }
.square {
background: #fff; border: 1px solid #999; float: left;
font-size: 24px; font-weight: bold; line-height: 34px;
height: 34px; margin-right: -1px; margin-top: -1px;
padding: 0; text-align: center; width: 34px;
}
.square:focus { outline: none; }
.kbd-navigation .square:focus { background: #ddd; }
.game { display: flex; flex-direction: row; }
.game-info { margin-left: 20px; }
And now the work horse, the JSX-enabled usage of React-in-a-tweet™ (also lifted from React tutorial):
function Square(props) {
return (
<button className="square" onclick={props.onClick}>
{props.value}
</button>
);
}
Returning JSX!
The way that works is: Babel transpiles this JSX to React.createElement
calls,
which are handled by React-in-a-tweet just fine.
Remember, JSX transpiles to React.createElement()
calls, and passes in the
component type as the first argument, props defined in JSX as the second
argument, and any children provided as the third argument.
For example:
<button className="square" onclick={props.onClick}>
Square value: {props.value}
<div>nested</div>
</button>
transpiles to:
React.createElement("button", {
className: "square",
onclick: props.onClick
},
"Square value: ", props.value,
React.createElement("div", null, "nested")
);
So, transpiled code includes React.createElement()
calls that contain 3 or
more arguments: type
, props
, and ...children
.
Also notice that the click handler on button
above has the DOM-defined name
onclick
, instead of the name that React gives it - onClick
.
This is because React-in-a-tweet does not implement React’s
synthetic events.
But, just like React, you can still assign functions to onclick
.
Wait, what about props.onClick
though? Why isn’t it it props.onclick
?
Well, that’s just a prop name, which can be anything you like :)
Moving on to more definitions:
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
Notice how Board
’s render
function can call this.renderSquare()
to fill in
parts of the rendered output. Works just fine.
renderSquare()
returns a custom component, Square
, not an HTML primitive.
Works fine.
Also notice how the onClick
handler defined in renderSquare()
is just an
arrow function. Works great.
But wait, what’s className
doing there? Isn’t that a React-ified HTML attribute
name?
Well, no siree.
className
is pure DOM API. Works as is.
Now let’s define the root game component itself:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
stepNumber: 0,
xIsNext: true
};
}
handleClick(i) {
console.log('handleClick', i);
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
history: history.concat([
{
squares: squares
}
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onclick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={i => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
So much code! Well, as far as React API is concerned, there are only a few interesting things going on in there.
Game
’sconstructor
sets initial state (this.state
), storinghistory
and other Tic-tac-toe-specific data.handleClick
click handler usesthis.setState()
and passes in new state.render
does conditional rendering based on whether the game has been won yet, and returns JSX for the entire game board.- Since game state is defined entirely as
Game
’s React state, the entire game board will re-render on every game state change in this example. Regardless of this example, React-in-a-tweet does allow re-rendering specific parts of a UI as long as those specific UI sections hold their own state, instead of depending on a single global state.
ReactDOM.render(<Game />, document.getElementById("root"));
console.log('✓')
React DOM API that triggers the first-render, y’all. Passing in JSX for <Game
/>
works. It’s just the React you know and love!
Finally, a helper function that calculates winner after every move:
function calculateWinner(squares) {
const lines = [[0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6]];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) return squares[a];
}
return null;
}
Wait, how does React-in-a-tweet itself work?
Glad you asked!
Here’s a line-by-line explanation of the tweet-sized source code of React-in-a-tweet:
Define React
global:
React = {
Define and implement the following API:
React.Component
, just a simple base class which stores theprops
passed to it.
Component: function(p) {
this.props = p
},
React.createElement
, the work horse of rendering, which accepts:t
, HTML type name or React component class,p
, props...c
, children to be appended to the dom element created.
[C = 'createElement']: (t, p, ...c) => (
In the case of t
being a React-in-a-tweet component class, ie, a constructor
function derived from React.Component
, we can just instantiate that function
with passed props and return the resulting React component instance.
t.bind
? new t(p)
Otherwise, t
is an HTML primitive element name, like button
, or span
. In
this case, create a new HTML element using document.createElement(t)
: (
e = document[C](t),
and assign all the props passed to it as direct properties of the HTML element.
This is how onclick
s and other event handlers get registered.
If you want to use React-like CSS-in-JS, you can modify the part below to loop over a passed
style
object, and assign its properties toe[n].style
.
(() => {
for (n in p)
e[n] = p[n]
})(),
We must also append the children
passed in as arguments into the created DOM
element.
Looping over a flattened list of children, handle the following cases for each
child element (e
):
- If
a.props
exists, then it’s aReact.Component
instance. Calla.render()
to get the corresponding instantiated HTML element. - If
a.part
exists, then it’s a regular HTML element instance already. Do nothing. - Else,
a
must be plain text. Initialize a new HTMLText
element to hold the string.
In all cases, after instantiating, append the resulting DOM element as a child
to the parent DOM element e
.
Note:
A
is just shorthand forappendChild
, as defined below.
Finally, return e
, the element created and filled in by this execution of
React.createElement()
.
g(c).map(a => (
e[A](
a.props
? a.render()
: a.part
? a
: new Text(a)
)
)),
e
)
)
},
Lastly, define g
, just a helper function that flattens elements, lists, and
list of lists into a single list, to support React.createElement
’s children
argument semantics.
g = a => (
a = a || [],
a.map ? a.flatMap(g) : [a]
),
A = 'appendChild',
And that’s it!
Understanding the React DOM + Component.setState tweet is left as an exercise to the reader.