Learning a new abstraction is easier when you strip away every other tool around it. That is especially true for React, where the development stack tends to blur the line between what React itself does and what the surrounding tooling provides. Pulling those pieces apart makes their individual responsibilities visible — and makes it harder to combine them in ways they were never meant to be used.

What follows is a minimal way to run React from a plain index.html file, adding one piece at a time. It expands on material from the free Beginner's Guide to React course, which also has a companion lesson: Create a User Interface with Vanilla JavaScript and DOM on egghead.io.

From an empty page to a rendered DOM node

Start with an ordinary HTML document.

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

The browser is forgiving enough to insert html and body for you, so even that markup is more than strictly required.

Next, create a container — the "root" node — that JavaScript can find by its id:

<html>
	<body>
		<div id="root"></div>
	</body>
</html>

Locate that node in JavaScript:

<html>
	<body>
		<div id="root"></div>
		<script type="module">
			const rootElement = document.getElementById('root')
		</script>
	</body>
</html>

Then build a DOM element and place it inside:

<html>
	<body>
		<div id="root"></div>
		<script type="module">
			const rootElement = document.getElementById('root')
			const element = document.createElement('div')
			element.textContent = 'Hello World'
			element.className = 'container'
			rootElement.append(element)
		</script>
	</body>
</html>

The page now renders "Hello World" inside a div within the root. That is the whole vanilla baseline: no framework involved.

Bringing in React and React DOM

React is a third-party library with its own JavaScript, so it needs its own script tags for the browser to load.

<html>
	<body>
		<div id="root"></div>
		<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
		<script type="module">
			const rootElement = document.getElementById('root')
			const element = document.createElement('div')
			element.textContent = 'Hello World'
			element.className = 'container'
			rootElement.append(element)
		</script>
	</body>
</html>

With React available as the global React, you can create React elements:

<html>
	<body>
		<div id="root"></div>
		<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
		<script type="module">
			const rootElement = document.getElementById('root')
			// const element = document.createElement('div')
			// element.textContent = 'Hello World'
			// element.className = 'container'
			// rootElement.append(element)

			const element = React.createElement(
				'div',
				{ className: 'container' },
				'Hello World',
			)
		</script>
	</body>
</html>

Log that element and you will find it is a plain JavaScript object, roughly like this:

{
  $$typeof: Symbol(react.element)
  key: null
  props: {className: "container", children: "Hello World"}
  ref: null
  type: "div"
  _owner: null
  _store: {validated: false}
  _self: null
  _source: null
  __proto__: Object
}

A React element on its own does not put anything on the page. Turning it into a DOM node and inserting that node into root is the job of react-dom.

<html>
	<body>
		<div id="root"></div>
		<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
		<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
		<script type="module">
			const rootElement = document.getElementById('root')
			const element = React.createElement(
				'div',
				{ className: 'container' },
				'Hello World',
			)
			ReactDOM.render(element, rootElement)
		</script>
	</body>
</html>

The result is the same output the vanilla JavaScript version produced.

Introducing JSX via Babel

Nobody actually writes React that way; the usual syntax is JSX. Browsers understand React.createElement, not JSX, so the syntax needs to be converted — the reason the Babel compiler exists.

Babel is written entirely in JavaScript and can run in the browser, so it can be added directly to the page. That is enough to let you mark a script as JSX by setting its type to text/babel:

<html>
	<body>
		<div id="root"></div>
		<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
		<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
		<script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>
		<script type="module">
			const rootElement = document.getElementById('root')
			const element = React.createElement(
				'div',
				{ className: 'container' },
				'Hello World',
			)
			ReactDOM.render(element, rootElement)
		</script>
	</body>
</html>
<html>
	<body>
		<div id="root"></div>
		<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
		<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
		<script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>
		<script type="text/babel">
			const rootElement = document.getElementById('root')
			const element = React.createElement(
				'div',
				{ className: 'container' },
				'Hello World',
			)
			ReactDOM.render(element, rootElement)
		</script>
	</body>
</html>

With the script tagged for Babel, JSX becomes available:

<html>
	<body>
		<div id="root"></div>
		<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
		<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
		<script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>
		<script type="text/babel">
			const rootElement = document.getElementById('root')
			// const element = React.createElement(
			//   'div',
			//   {className: 'container'},
			//   'Hello World',
			// )
			const element = <div className="container">Hello World</div>
			ReactDOM.render(element, rootElement)
		</script>
	</body>
</html>

The finished single file

What remains is the complete, minimal setup for running React in an index.html with no build tooling at all:

<html>
	<body>
		<div id="root"></div>
		<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
		<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
		<script src="https://unpkg.com/@babel/[email protected]/babel.js"></script>
		<script type="text/babel">
			ReactDOM.render(
				<div className="container">Hello World</div>,
				document.getElementById('root'),
			)
		</script>
	</body>
</html>

This is not a structure to build a real application on. Its value is diagnostic: it separates the pieces of the React stack and shows what each one contributes, and where the tooling ends and the library begins.