...
This allows for some neat new logical paths, such as returning a block of HTML code to the caller of a JavaScript function.
Table of Contents |
---|
The Gist of JSX
Assuming you have imported a component called MetadataTable, you could call Here is an example of a component written without JSX - notice that e()
is a shorthand for React.createElement()
with classic JavaScript:
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
<!-- Load Bootstrap CSS from CDN --> <script> // Without JSX, we can only use plain old JavaScript <link var nojsxContainer = document.querySelector('#react_test_nojsx'); ReactDOM.render(React.createElement(MetadataTable), nojsxContainer); </script> |
Or you could choose to use JSX to directly embed HTML/XML into your JavaScript:
Code Block | ||
---|---|---|
| ||
<script>
// With JSX, we can use a syntax that feels a bit more like Scala
// Notice how we have HTML(XML) embedded in our JavaScript
var jsxContainer = document.querySelector('#react_test_jsx');
ReactDOM.render(
<MetadataTable />,
jsxContainer
);
</script> |
Such a simple example doesn't really illustrate the power of JSX, so let's dive a bit deeper.
How JSX can Simplify Complex Components
Without JSX, the developer ends up calling React.createElement()
a lot. Like, a LOT a lot.
When things get even the slightest bit complex, the React.createElement()
syntax (and even the e()
shorthand) become very tedious to continuously repeat.
This is where JSX can truly shine - rather than a mess of e('div', {}, e('div', {}, e('div', {}, ... )))
, we can use the cleaner, more familiar syntax of <div><div><div>...</div></div></div>
Below, I have provided a few test files. To see these examples in action, simply copy them to your local machine.
You can then view them by navigating a browser to file:///path/to/example.index.html
Custom Components Written without JSX
Here is an example of a component written without JSX that can be run by your browser without any compilation necessary - notice that e()
is a shorthand for React.createElement()
:
Code Block | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
<!-- Load Bootstrap CSS from CDN -->
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<!-- Load React from CDN -->
<!-- Note: when deploying to production, replace "development.js" with "production.min.js". -->
<script src="//unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="//unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<div id="react_test">Component failed to load</div>
<script>
'use strict';
var e = window.React.createElement;
var ReactDOM = window.ReactDOM;
class MetadataTable extends React.Component {
constructor(props) {
super(props);
this.state = {
headers: [
{ key: 'name', label: 'Name' },
{ key: 'type', label: 'Type' },
{ key: 'lastUpdated', label: 'Last Updated' },
{ key: 'value', label: 'Value' },
{ key: 'additionalActions', label: 'More...' }
],
data: [
{ 'name': 0, 'value': 'hello'},
{ 'name': 1, 'value': 'react'},
{ 'name': 2, 'value': 'without'},
{ 'name': 3, 'value': 'jsx'},
]
};
}
getTableHeaders() {
var tableHeaders = [];
for(var i = 0; i < this.state.headers.length; i++) {
var header = this.state.headers[i];
var uniqueKey = 'table_header_' + header.key;
// Push table header cells into the header row
tableHeaders.push(e('th', { 'key': uniqueKey, 'width': `${100 / this.state.headers.length}%` }, `${header.label}`));
}
// Accumulate and return table header elements
return tableHeaders;
}
getTableRowData() {
var tableRows = [];
for(var i = 0; i < this.state.data.length; i++) {
var item = this.state.data[i];
var uniqueKey = 'table_row_data_' + i.toString();
var rowData = [];
// Loop over selected headers to print table row values
for(var j = 0; j < this.state.headers.length; j++) {
var header = this.state.headers[j];
var uniqueKey = 'table_' + header.key.toString() + '_' + i.toString();
// Push table cell values into a "row"
rowData.push(e('td', { 'key': uniqueKey, 'width': `${100 / this.state.headers.length}%`}, `${item[header.key]}`));
}
// Push rows of cells into the table
tableRows.push(e('tr', { 'key': uniqueKey }, rowData));
}
// Accumulate and return table row elements
return tableRows;
}
handleNewValueChange(event) {
console.debug("OnChange Event:", event);
this.setState({ 'newValue': event.target.value });
}
addNew(event) {
console.debug("OnClick Event:", event);
let data = this.state.data;
data.push({ 'name': this.state.data.length, 'value': this.state.newValue });
this.setState({ 'data': data });
this.setState({ 'newValue': '' });
}
render() {
// Re-fetch selected headers and row data
var tableHeaders = this.getTableHeaders();
var tableRows = this.getTableRowData();
// Append an additional row with a submit button to the form
tableRows.push(e('tr', { 'key': 'submitBtn' },
e('td', {},
e('input', {
'placeholder': 'Dummy text input...',
'value': this.state.newValue,
onChange: (event) => this.handleNewValueChange(event)
})
),
e('td', {},
e('button', {
'className': 'btn btn-xs btn-primary',
onClick: (event) => this.addNew(event)
}, 'Add new')
)
));
// Render the table data with React
return e('div', { 'className': 'table-responsive' },
e('table', { 'className': 'table table-condensed table-hover'},
e('thead', {},
e('tr', { 'key': 'headers' }, tableHeaders)),
e('tbody', {}, tableRows))
);
}
}
console.log("Rendering!");
var domContainer = document.querySelector('#react_test');
ReactDOM.render(e(MetadataTable), domContainer);
console.log("Rendered!");
</script>
|
With JSX
Now, here is the same component converted to use JSX syntax - note that the browser can no longer directly interpret this code, which must be compiled or "transpiled" using Babel or something similar. Since we will need to compile the JSX down to CommonJS anyways, we can safely use ES6 syntax (such as the "let" keyword or arrow functions) without worrying about browser support, since the compiler should translate these shorthands to their equivalent CommonJS representation.
Notice that we are no longer passing around the values returned from React.createElement
... in fact, we don't even explicitly call React.createElement
at all:
Code Block | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
<!-- Load Bootstrap CSS from CDN -->
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<!-- Load React from CDN -->
<!-- Note: when deploying to production, replace "development.js" with "production.min.js". -->
<script src="//unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="//unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<div id="react_test">Component failed to load</div>
<!-- STOP! The contents of the following script tag can be compiled with the debugger console, located here: https://babeljs.io/en/repl -->
<!-- See react-jsx-compiled.index.html for compiled browser-ready results -->
<script>
'use strict';
let ReactDOM = window.ReactDOM;
// TypeScript used here - feels like a more formal JavaScript
class MetadataTable extends React.Component {
constructor(props) {
super(props);
this.state = {
headers: [
{ key: 'name', label: 'Name' },
{ key: 'type', label: 'Type' },
{ key: 'lastUpdated', label: 'Last Updated' },
{ key: 'value', label: 'Value' },
{ key: 'additionalActions', label: 'More...' }
],
data: [
{ 'name': 0, 'value': 'hello'},
{ 'name': 1, 'value': 'react'},
{ 'name': 2, 'value': 'with'},
{ 'name': 3, 'value': 'jsx'},
],
newValue: ''
};
}
getTableHeaders() {
let tableHeaders = [];
for(let i = 0; i < this.state.headers.length; i++) {
let header = this.state.headers[i];
let uniqueKey = 'table_header_' + header.key;
// Push table header cells into the header row
// Notice that we no longer return an HTML element here
tableHeaders.push({ 'key': uniqueKey, 'width': (100 / this.state.headers.length).toString() + '%', 'label': header.label });
}
// Accumulate and return table header data
return tableHeaders;
}
getTableRowData() {
let tableRows = [];
for(let i = 0; i < this.state.data.length; i++) {
let item = this.state.data[i];
let uniqueKey = 'table_row_data_' + i.toString();
let rowData = [];
// Loop over selected headers to print table row values
for(let j = 0; j < this.state.headers.length; j++) {
let header = this.state.headers[j];
let uniqueKey = 'table_' + header.key.toString() + '_' + i.toString();
// Push table cell values into a "row"
// Notice that we no longer return an HTML element here
rowData.push({ 'key': uniqueKey, 'width': (100 / this.state.headers.length).toString() + '%', 'label': item[header.key] });
}
// Push the row of cell data into the table
// Notice that we no longer return an HTML element here
tableRows.push({ key: uniqueKey, 'data': rowData });
}
// Accumulate and return table row data
return tableRows;
}
handleNewValueChange(event) {
console.debug("OnChange Event:", event);
this.setState({ 'newValue': event.target.value });
}
addNew(event) {
console.debug("OnClick Event:", event);
let data = this.state.data;
data.push({ 'name': this.state.data.length, 'value': this.state.newValue });
this.setState({ 'data': data });
this.setState({ 'newValue': '' });
}
render() {
// Re-fetch selected headers and row data
let tableHeaders = this.getTableHeaders();
let tableRows = this.getTableRowData();
// Render the table data with React
// NOTE: JSX syntax is returned - feels a bit like Scala
return (
<div className='table-responsive'>
<table className='table table-condensed table-hover'>
<thead>
<tr>
{tableHeaders.map((item, i) => {
// For each column header in tableHeaders
return (<th key={item.key} width={item.width}>{item.label}</th>);
})}
</tr>
</thead>
<tbody>
{tableRows.map((row, i) => {
// For each row in tableRows
return (
<tr key={row.key}>
{row.data.map((cell, j) => {
// For each cell in the row
return (<td key={cell.key} width={cell.width}>{cell.label}</td>);
})}
</tr>
);
})}
<tr>
<td>
<input placeholder='Dummy text input...' value={this.state.newValue} onChange={this.handleNewValueChange} />
</td>
<td>
<button className='btn btn-xs btn-primary' onClick={(event) => { this.addNew(event, this.state.newValue) }}>Add new</button>
</td>
</tr>
</tbody>
</table>
</div>
);
}
}
console.log("Rendering!");
let domContainer = document.querySelector('#react_test');
ReactDOM.render(
<MetadataTable />,
domContainer
);
console.log("Rendered!");
</script>
|
Compilation Step
JSX requires us to compile down to JavaScript - to do this, we simply pass our source code through a Babel compiler.
There are many build tools / plugins which can perform this step as well, but I chose to use the Babel web debugger for this simple example.
The result of the compilation might look as follows - Note that the compiler has replaced the JSX parts in the code below with nested calls to React.createElement()
.
This compiled output can be then immediately consumed by your browser:
Code Block | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
<!-- Load Bootstrap CSS from CDN --> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <!-- Load React from CDN --> <!-- Note: when deploying to production, replace "development.js" with "production.min.js". --> <script src="//unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="//unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script> <div id="react_test">Component failed to load</div> <!-- STOP! The contents of the following script tag were compiled using the debugger console, located here: https://babeljs.io/en/repl --> <!-- See react-jsx.index.html for original compilation source --> <script> 'use strict'; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } var ReactDOM = window.ReactDOM; // TypeScript used here - feels like a more formal JavaScript var MetadataTable = function (_React$Component) { _inherits(MetadataTable, _React$Component); function MetadataTable(props) { _classCallCheck(this, MetadataTable); var _this = _possibleConstructorReturn(this, (MetadataTable.__proto__ || Object.getPrototypeOf(MetadataTable)).call(this, props)); _this.state = { headers: [{ key: 'name', label: 'Name' }, { key: 'type', label: 'Type' }, { key: 'lastUpdated', label: 'Last Updated' }, { key: 'value', label: 'Value' }, { key: 'additionalActions', label: 'More...' }], data: [{ 'name': 0, 'value': 'hello' }, { 'name': 1, 'value': 'react' }, { 'name': 2, 'value': 'with' }, { 'name': 3, 'value': 'jsx' }], newValue: '' }; return _this; } _createClass(MetadataTable, [{ key: 'getTableHeaders', value: function getTableHeaders() { var tableHeaders = []; for (var i = 0; i < this.state.headers.length; i++) { var header = this.state.headers[i]; var uniqueKey = 'table_header_' + header.key; // Push table header cells into the header row tableHeaders.push({ 'key': uniqueKey, 'width': (100 / this.state.headers.length).toString() + '%', 'label': header.label }); } // Accumulate and return table header data return tableHeaders; } }, { key: 'getTableRowData', value: function getTableRowData() { var tableRows = []; for (var i = 0; i < this.state.data.length; i++) { var item = this.state.data[i]; var uniqueKey = 'table_row_data_' + i.toString(); var rowData = []; // Loop over selected headers to print table row values for (var j = 0; j < this.state.headers.length; j++) { var header = this.state.headers[j]; var _uniqueKey = 'table_' + header.key.toString() + '_' + i.toString(); // Push table cell values into a "row" rowData.push({ 'key': _uniqueKey, 'width': (100 / this.state.headers.length).toString() + '%', 'label': item[header.key] }); } // Push the row of cell data into the table tableRows.push({ key: uniqueKey, 'data': rowData }); } // Accumulate and return table row data return tableRows; } }, { key: 'handleNewValueChange', value: function handleNewValueChange(event) { console.debug("OnChange Event:", event); this.setState({ 'newValue': event.target.value }); } }, { key: 'addNew', value: function addNew(event) { console.debug("OnClick Event:", event); var data = this.state.data; data.push({ 'name': this.state.data.length, 'value': this.state.newValue }); this.setState({ 'data': data }); this.setState({ 'newValue': '' }); } }, { key: 'render', value: function render() { var _this2 = this; // Re-fetch selected headers and row data var tableHeaders = this.getTableHeaders(); var tableRows = this.getTableRowData(); // Render the table data with React // NOTE: JSX syntax is returned - feels a bit like Scala return React.createElement( 'div', { className: 'table-responsive' }, React.createElement( 'table', { className: 'table table-condensed table-hover' }, React.createElement( 'thead', null, React.createElement( 'tr', null, tableHeaders.map(function (item, i) { rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <!-- Load React from CDN --> <!-- Note: when deploying to production, replace "development.js" with "production.min.js". --> <script src="//unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="//unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script> <div id="react_test">Component failed to load</div> <script> 'use strict'; var e = window.React.createElement; var ReactDOM = window.ReactDOM; class MetadataTable extends React.Component { constructor(props) { super(props); // For each column header in tableHeaders this.state = { headers: [ return { key: 'name', label: 'Name' }, React.createElement( { key: 'type', label: 'Type' }, 'th', { key: 'lastUpdated', label: 'Last Updated' }, { key: 'value'item.key, labelwidth: 'Value'item.width }, { key: 'additionalActions', label: 'More...' } ],item.label data: [ ); { 'name': 0, 'value': 'hello'}, { 'name': 1, 'value': 'react'},) { 'name': 2, 'value': 'without'}, ) { 'name': 3, 'value': 'jsx'} ), ] React.createElement( }; } getTableHeaders() { 'tbody', var tableHeaders = []; null, for(var i = 0; i < this.state.headers.length; i++) { var header = this.state.headers[i]; tableRows.map(function (row, i) { var uniqueKey = 'table_header_' + header.key; // For each row in tableRows // Push table header cells into the header row tableHeaders.push(e('th', { 'key': uniqueKey, 'width': `${100 / this.state.headers.length}%` }, `${header.label}`)); return React.createElement( } // Accumulate and return table header elements'tr', return tableHeaders; } getTableRowData() {{ key: row.key }, var tableRows = []; for(var i = 0; i < this.staterow.data.length; i++map(function (cell, j) { var item = this.state.data[i]; var uniqueKey// = 'table_row_data_' + i.toString(); For each cell in the row var rowData = []; // Loop over selected headers to print table row values return React.createElement( for(var j = 0; j < this.state.headers.length; j++) { var header = this.state.headers[j]; 'td', var uniqueKey = 'table_' + header.key.toString() + '_' + i.toString(); { key: cell.key, width: cell.width }, // Push table cell values into a "row" // NOTE: sketchy hack at the endcell.label to coerce to a String rowData.push(e('td', { 'key': uniqueKey, 'width': `${100 / this.state.headers.length}%`}, `${item[header.key]}`)); ); } // Push rows of cells into the table}) tableRows.push(e('tr', { 'key': uniqueKey }, rowData)); ); } // Accumulate and return table row elements }), return tableRows; } React.createElement( handleNewValueChange(event) { console.debug("OnChange Event:", event); 'tr', this.setState({ 'newValue': event.target.value }); } null, addNew(event) { consoleReact.debugcreateElement("OnClick Event:", event); let data = this.state.data; data.push({ 'name': this.state.data.length, 'valuetd': this.state.newValue }); , this.setState({ 'data': data }); this.setState({ 'newValue': '' }); null, } render() { React.createElement('input', { placeholder: 'Dummy text input...', value: this.state.newValue, onChange: this.handleNewValueChange }) // Re-fetch selected headers and row data var tableHeaders = this.getTableHeaders(); ), var tableRows = this.getTableRowData(); // Append an additional row with a submit button to the formReact.createElement( tableRows.push(e('tr', { 'key': 'submitBtn' }, e('td', {}, e('input', { null, 'placeholder': 'Dummy text input...', React.createElement( 'value': this.state.newValue, onChange: (event) => this.handleNewValueChange(event) 'button', }) { ), className: 'btn btn-xs btn-primary', onClick: function onClick(event) { e('td', {}, e('button', { _this2.addNew(event, _this2.state.newValue); 'className': 'btn btn-xs btn-primary', onClick: (event) => this.addNew(event) } }, }, 'Add new') 'Add new' ) )); // Render the table data with React) return e('div', { 'className': 'table-responsive' }, ) e('table', { 'className': 'table table-condensed table-hover'}, ) e('thead', {}, ) ) e('tr', { 'key': 'headers' }, tableHeaders)), ); } e('tbody', {}, tableRows)) }]); ); return MetadataTable; }(React.Component); } console.log("Rendering!"); var domContainer = document.querySelector('#react_test'); ReactDOM.render(eReact.createElement(MetadataTable, null), domContainer); console.log("Rendered!"); </script> |
With JSX
Now, here is the same component converted to use JSX syntax.
Notice that we are no longer passing around the values returned from React.createElement
... in fact, we don't even call explicitly call React.createElement
at all:
Code Block | |
---|---|
language | js