I have three components in my project, App, DataTable and Table.
The App will render a DataTable that contains the Table component. I just called DataTable with data and columns props in it.
// data sample
const tmpData = [
{
name: "Can",
age: 4,
}, {
name: "David",
age: 44,
}, {
name: "Sara",
age: 14,
}, {
name: "Hani",
age: 24,
}
]
// main columns array
const tmpColumns = [
{
title: "Name",
accessor: "name",
}, {
title: "Age",
accessor: "age",
}
]
function App() {
return (
<div className="App" style={{background: "#f0f0f0", padding: "1rem"}}>
<div>App Component:</div>
<DataTable data={tmpData} columns={tmpColumns}/>
</div>
);
}
DataTable component is just for handling selection and filter actions on my table. So, it could manipulate the data and the columns. the Table will render in it and here is the code of DataTable.
function DataTable(props) {
const [data, setData] = useState(props.data)
const [columns, setColumns] = useState(props.columns)
const [selection, setSelection] = useState([])
useEffect(() => {
// add select columns after component mount
handleColumnChange()
}, [])
// listen to selection change
useEffect(() => {
// selection change log, It change on each select.
console.log(selection);
}, [selection])
function handleRowSelect(rowName) {
const keyIndex = selection.indexOf(rowName);
console.log(selection, rowName, keyIndex);
if (keyIndex === -1)
setSelection(preSelection => ([...preSelection, ...[rowName]]))
else
setSelection(preSelection => preSelection.filter(sl => sl !== rowName))
}
function handleColumnChange() {
// add select column if not added already
if (!columns.find(col => col.accessor === 'select')) {
setColumns([
...[{
title: "Select",
accessor: "select",
// this method will execute to render checkbox on Select table
Cell: function (row) {
return <input type="checkbox"
onChange={() => handleRowSelect(row.name, selection)}
checked={selection.includes(row.name)}/>
},
}],
...columns,
])
}
}
return (
<div className="DataTable" style={{background: "#e0e0e0", padding: "1rem"}}>
<div>Data Table:</div>
<Table {...{data, columns}}/>
</div>
)
}
Table component will render columns and suitable data for them. For each column in columns array we have an item to access data (accessor) or an executable method to return custom data (Cell) and here is its code.
function Table(props) {
const [data, setData] = useState(props.data)
return (
<div className="Table" style={{background: "#d5d5d5", padding: "1rem"}}>
<div>Table</div>
<table>
<tbody>
<tr>
{props.columns.map((th, key) => (
<th key={key}>{th.title}</th>
))}
</tr>
{/* generating data rows */}
{data.map((dr, key) => (
<tr key={key}>
{columns.map((col, index) => (
<td key={index}>
{
// the "Cell" method has high priority than "accessor" selector
(col.Cell && typeof col.Cell === "function") ?
col.Cell(dr) :
dr[col.accessor]
}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
As you saw above, to handle row selection I manipulate the columns in the DataTable component by adding a new column at first index of my columns array. Everything works fine for now. But, when I try to select a row, the Call methods of the select column, could not access the selection array state of my DataTable component. and it's my problem!
Actually, on each select, the selection array must update and target checkbox must check. Apparently, there is a copy of the selection that changes (or maybe not changes).
You also could check the whole project on CodeSandbox
I have some fixes to your code, check it out on CodeSandbox.
The idea is that you don't need to put the columns to state, instead you just need to get the columns with the selection box.
I also added a simple optimization to it by implementing React.useMemo to memoized the calculated columns. It will only be re-calculated when the props.columns and selection state changes.
Hope this helps! Happy coding! :)
Ok, your tmpData passed from App to DataTable is read-only. So by design you will not see any change your data along the way.
There're couple of ways to get it working, mostly having something to do to allow your DataTable to pass the change back to App if that happens.
Step 1, you could add one prop called onRowClick on the DataTable,
<DataTable data={tmpData} onRowClick={row => { console.log(row) } />
Step 2, you need to allow your tmpData to change after the event. You are using hooks, so we can
const [tmpData, setTmpData] = useState([sampleData])
return (
<DataTable data={tmpData} onRowClick={row => {
// use setTmpData to adjust the tmpData to get a new instance of tmpData
} />
)
Of course for things with this complexity, we normally use useReducer instead of useState, since there's definitely other things that you want to do other than selecting the row :)
Related
We are using MUI DataGrid in our React based project.
At the moment I am trying to save/persist state of columns after toggling visibility of some columns with DataGrid's toolbar column menu, as currently after re-render it is back to default column setup.
I would like to know how could I access state of DataGrid/visibility state of columns in DataGrid so I can adjust/save it/reuse it later?
So far I meddled a bit with a apiRef, but all I got from apiRef.current was empty object. I am adding below some basic codeSandbox example to show how I tried to access it.
https://codesandbox.io/s/datagridprodemo-material-demo-forked-189j9?file=/demo.js
Maybe there is better/different approach, or I just need to create the state somehow. We would like to persist the state of the columns as user preference possibly in a future so this is vital for that to happen.
All suggestions are welcome and I thank you beforehand.
Fortunately, the DataGrid API provides the columnVisibilityModel and onColumnVisibilityChange props.
See this code sandbox for a simple example of controlling the columnVisibilityModel: https://codesandbox.io/s/mui-datagrid-oncolumnvisibilitychange-savestate-u1opzc?file=/src/App.tsx:1960-1984
Here is the code for a simple implementation. Your initial state may vary. Also, note that I could not figure out how to get DataGridPro to call onColumnVisibilityChange unless columnVisibilityModel was initially undefined. Bug, or my mistake, I am uncertain.
import "./styles.css";
import React from "react";
import {
DataGrid,
GridRowsProp,
GridColDef,
GridCallbackDetails,
MuiEvent,
GridColumnVisibilityModel,
GridColumnVisibilityChangeParams
} from "#mui/x-data-grid";
import { Button } from "#mui/material";
const rows: GridRowsProp = [
{ id: 1, col1: "Hello", col2: "World" },
{ id: 2, col1: "DataGridPro", col2: "is Awesome" },
{ id: 3, col1: "MUI", col2: "is Amazing" }
];
const columns: GridColDef[] = [
{ field: "col1", headerName: "Column 1", width: 150 },
{ field: "col2", headerName: "Column 2", width: 150 }
];
const initialVisibilityModel = { col1: true, col2: true };
export default function App() {
// it is strange, but in order for DataGridPro to call onColumnVisibilityChange, columnVisibilityModel must be undefined initially
const [
currentGridColumnVisibilityModel,
setCurrentGridColumnVisibilityModel
] = React.useState<GridColumnVisibilityModel | undefined>(undefined);
const [mySavedValue, setMySavedValue] = React.useState<
GridColumnVisibilityModel | undefined
>(undefined);
const onColumnVisibilityChange = React.useCallback(
(
params: GridColumnVisibilityChangeParams,
event: MuiEvent<{}>,
details: GridCallbackDetails
): void => {
console.log("params", params);
setCurrentGridColumnVisibilityModel((s) => ({
// per the DataGridPro strangeness, we must marry in initial state only the first update
...(s ? s : initialVisibilityModel),
[params.field]: params.isVisible
}));
},
[]
);
const saveACopyOfGridState = () => {
setMySavedValue(currentGridColumnVisibilityModel || initialVisibilityModel);
};
const loadSavedCopyOfGridState = () => {
setCurrentGridColumnVisibilityModel(mySavedValue || initialVisibilityModel);
};
const currentVisibilityAsText =
`${Object.keys(currentGridColumnVisibilityModel ?? {}).map(
(key) => `{${key}:${currentGridColumnVisibilityModel?.[key]}}`
)}` || "empty";
const savedVisibilityAsText =
`${Object.keys(mySavedValue ?? {}).map(
(key) => `{${key}:${mySavedValue?.[key]}}`
)}` || "empty";
return (
<div style={{ height: 300, width: "100%" }}>
<DataGrid
rows={rows}
columns={columns}
columnVisibilityModel={currentGridColumnVisibilityModel}
onColumnVisibilityChange={onColumnVisibilityChange}
/>
<div>
<Button onClick={saveACopyOfGridState} variant="contained">
SAVE CURRENT COLUMN VISIBILITY STATE
</Button>
<Button
onClick={loadSavedCopyOfGridState}
variant="contained"
color="warning"
>
LOAD SAVED COLUMN VISIBILITY STATE
</Button>
</div>
<p>Current filter state: {currentVisibilityAsText}</p>
<p>Saved filter state: {savedVisibilityAsText}</p>
</div>
);
}
I made a hook to persist column settings to localStorage. It uses callbacks on the API ref object. Usage:
function MyGrid() {
const apiRef = useGridApiRef()
usePersistColumnSettings(apiRef, 'customers-grid')
return <DataGrid apiRef={apiRef} />
}
I have a list of students and I display them on the table. There are two buttons that indicate by which value should I sort the list (name or birthdate). When the button is clicked and I sort the list, the list itself is getting sorted, but it's not updating if I don't assign the list to the new list using Object.assign(newList, oldList) and then update it by passing to the update state function updateList(newList). Here's my code:
const students= [
{
name: "John Done",
year: 1,
birthdate: "2020-01-24",
},
{
name: "Another Done",
year: 3,
birthdate: "2002-02-20",
},
{
name: "Jack London",
year: 2,
birthdate: "1800-01-04",
},
{
name: "Text Name",
year: 3,
birthdate: "1990-02-24",
},
{
name: "Name",
year: 2,
birthdate: "2005-04-01",
},
];
ReactDOM.render(<App students={students} />, document.getElementById('root'));
function App({ students }) {
const [studentsList, setStudentsList] = useState(students);
const sortByYear = () => {
// let sortedstudents = [];
// Object.assign(sortedStudents, studentsList);
// sorteStudents.sort((a, b) => b.year - a.year);
// console.log(sorteStudents);
// setStudentsList(sortedStudents);
studentsList.sort((a,b) => b.year - a.year));
setStudentsList(studentsList);
};
const sortByDates = () => {
// let sortedStudents = [];
// Object.assign(sortedStudents, studentsList);
// sortedStudents.sort((a, b) => new Date(b.birthdate) - new Date(a.birthdate));
// console.log(sortedStudents);
// setStudentsList(sortedStudents);
studentsList.sort((a, b) => new Date(b.birthdate) - new Date(a.birthdate));
setStudentsList(studentsList);
};
return (
<div className="App">
<div>
<label>
Sort By
</label>
<button
onClick={() => sortByYear()}
>
Year
</button>
<button
onClick={() => sortByDates()}
>
Most Old
</button>
</div>
<Students students={studentsList} />
</div>
);
}
Students component
function Students({ students }) {
return (
<div>
<table>
<thead>
<tr>
<th>Name</th>
<th>Year</th>
<th>Date of birth</th>
</tr>
</thead>
<tbody>
{students.map((student, index) => (
<tr key={index}>
<td>{student.name}</td>
<td>{student.year.toString()}</td>
<td>{student.birthdate}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
So here in this way even though the students' list is getting sorted, the state is not updating, but if I assign the initial list to the new one and then sort it and then update the state it is working.
WORKS
let sortedStudents = [];
Object.assign(sortedStudents, studentsList);
sortedStudents.sort((a, b) => new Date(b.birthdate) - new Date(a.birthdate));
//console.log(sortedStudents);
setStudentsList(sortedStudents)
DOES NOT WORK
studentsList.sort((a, b) => new Date(b.birthdate) - new Date(a.birthdate));
setStudentsList(studentsList);
So the question is why do I need to assign my studentsList to the new array, specifically by using Object.assign() so that setStudentsList() would update the component's state? I've just started learning React so it's really confusing me the way how these states actually work.
Similar posts I've found
React Functional Component props does not update
React functional components props value not updating
Re render functional component on props change
React: Passing down props to functional components
As Brain mentioned:
React determines if it should re-render or not based on the equality of previous props to the next props, and previous state to the next state. By mutating the original state array, the previous studentList has referential equality with the updated studentList and react will not detect that it needs to rerender.
So in order to change the list and to React detect the changes, the value of the array needs to be used (mutated), instead of its reference.
As Bergi hinted: alternative that copies the array by value
I am using creatable select to allow the user to input a new option that is not in the dropdown list. But after looking I cannot seem to see a way of setting the maximum input to 50 characters for input before creating in the creatable select.
I have looked at taking the new entry and if over 50 characters deleting it however this seems a long way round and would like to use something that is shorter on the actual input when the user selects create.
import CreatableSelect from 'react-select/creatable';
const cars = [
{ label: "audi", value: 1 },
{ label: "bmw", value: 2 },
{ label: "ford", value: 3 },
{ label: "VW", value: 4 },
];
const selectOption = () => (
<div className="app">
<div className="container">
<CreatableSelect
options={cars}
placeholder={"check and enter new car name here"}
isClearable
onChange={(opt, meta) => console.log(opt, meta)}
/>
</div>
</div>
);
export default selectOption
I am hoping there is a max input option i just dont know. Thank you for taking the time to look at this/help, very much appreciated.
Simply put you can use the <CreatableSelect/> s onCreateOption prop. You will have to maintain your options and value in component state. Then put your control logic inside handleCreateOption function ( for onCreateOption prop ). Your handleCreateOption will be something like this. ( look at codesandbox for full code )
handleCreateOption = inputValues => {
if (inputValue.length < 50) {
const newOption = createOption(inputValue);
this.setState({
options: [...options, newOption],
value: newOption
});
}
}
codesandbox : https://codesandbox.io/embed/react-codesandboxer-example-cjbgu
If you don't want the user to be able to type more than the limit, you can add maxLength attribute to the input.
import React, { useEffect, useRef } from 'react';
const myRef = useRef(null);
useEffect(() => {
myRef.current.inputRef.style.minWidth = 'max-content';
myRef.current.inputRef.placeholder = placeholderText;
myRef.current.inputRef.maxLength = 20;
}, []);
<CreatableSelect
ref={myRef}
{...props}
options={options}
value={value}
name={name}
onChange={handleChange}
placeholder={''}
classNamePrefix="multi-select-custom"
isMulti={isMulti}
/>
I have an array of objects. i want to access class method inside object function property. How to implement this? please help me. I wrote smth like this:
render() {
tableTh = [
{name: i18n.t('ExtendedModalBar.naming'), style:{}, handleSort () {}},
...
]
...
}
I am trying to write someth like this:
class Table extends Component {
handleSort() {
}
...
render() {
}
}
My table headers are formed dynamically:
<thead>
<tr>
<th style={{ width: "50px" }}>
<Checkbox
checked={this.state.selectedAll}
onChange={() => this.selectAll()}
/>
</th>
{tableTh.map((obj, index) => {
return (
<th key={index.toString()} style={obj.style} onClick={obj.handleSort}>
{obj.name}
</th>
);
})}
</tr>
</thead>
I must implement sorting of table columns. If tables are formed statically it is easy to attach onclick to th tag. but if code was written like this, i am stuck. How to write method in the class that will be accessible from inside property of objects (array of objects) ? And I need sorting in both directions ascending and descending. My table column should change own sorting direction when I click on its table header. Any answers will be taken into acccount. Thanks in advance
The problem with having the state-changing function on the state itself is that this inherently makes it difficult to manipulate the state.
var data = [
{ name: 'a', change: /* ??? */ },
{ name: 'b', change: /* ??? */ },
{ name: 'c', change: /* ??? */ }
]
// How do we write the function ??? to change the order of data?
It is much simpler to lift the data-manipulation logic at a higher level (outside of the data itself).
In this case, we would put the data-manipulation logic on the class itself as opposed to within each item.
We can do this by leveraging the state in React.
A state is used to hold any (dynamic) presentational (rendered) data for a component. An example of dynamic (changing) presentational data would be a table that sorts its columns.
The reason we want to put presentational data in the state is because React allows us to re-trigger the presentation (by re-rendering) to always show the latest values for our data.
We can achieve this just by changing the data itself and not any presentational logic. This is why React component structure is referred to as declarative.
Anytime the state is updated, React will call the render function to obtain the component structure with the latest state changes and display those on the appropriate medium (in your case the DOM)
Here's one way to incorporate state to create a sortable table:
class Table extends React.Component {
constructor(props) {
super(props);
this.state = {
sortDirection: "asc", // we start with ascending order by default
selectedHeaderIndex: 0 // we start by sorting based on the first header (the one in position 0)
};
this.ascComparator = (row1, row2) =>
row1[this.state.selectedHeaderIndex].localeCompare(
row2[this.state.selectedHeaderIndex]
);
this.descComparator = (row1, row2) =>
row2[this.state.selectedHeaderIndex].localeCompare(
row1[this.state.selectedHeaderIndex]
);
this.flipSortDirection = () =>
this.state.sortDirection === "asc" ? "desc" : "asc";
}
render() {
const { headers, rows } = this.props.table;
const comparator =
this.state.sortDirection === "asc"
? this.ascComparator
: this.descComparator;
// sort the rows based on the selected header
const sortedRows = rows.sort(comparator);
return (
<table>
<thead>
{headers.map((header, i) => (
<th
onClick={() => {
this.setState({
// if we clicked on the already selected index, we flip the sort direction
sortDirection:
this.state.selectedHeaderIndex === i
? this.flipSortDirection()
: "asc",
selectedHeaderIndex: i
});
}}
>
{header}
</th>
))}
</thead>
<tbody>
{sortedRows.map(row => (
<tr>
{row.map(cell => (
<td>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
);
}
}
const table = {
headers: ["h1", "h2", "h3"],
rows: [["a", "9", "+"], ["b", "6", "-"], ["c", "3", "="]]
};
ReactDOM.render(<Table table={table} />, document.querySelector("#app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Using hooks the code becomes a bit more readable (click for the CodeSandbox example since SO doesn't support React 16.8 yet):
function Table({ table }) {
const { headers, rows } = table;
const [selectedHeaderIndex, setSelectedHeaderIndex] = React.useState(0); // we start by sorting based on the first header (the one in position 0)
const [sortDirection, setSortDirection] = React.useState("asc"); // we start with ascending order by default
// determine the sorting comparator based on the sorting direction
const comparator =
sortDirection === "asc"
? (row1, row2) =>
row1[selectedHeaderIndex].localeCompare(row2[selectedHeaderIndex])
: (row1, row2) =>
row2[selectedHeaderIndex].localeCompare(row1[selectedHeaderIndex]);
const flipSortDirection = () => (sortDirection === "asc" ? "desc" : "asc");
// sort the rows based on the selected header
const sortedRows = rows.sort(comparator);
return (
<table>
<thead>
{headers.map((header, i) => (
<th
onClick={() => {
setSelectedHeaderIndex(i);
setSortDirection(
selectedHeaderIndex === i ? flipSortDirection() : "asc"
);
}}
>
{header}
</th>
))}
</thead>
<tbody>
{sortedRows.map(row => (
<tr>
{row.map(cell => (
<td>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
);
}
const table = {
headers: ["h1", "h2", "h3"],
rows: [["a", "9", "+"], ["b", "6", "-"], ["c", "3", "="]]
};
ReactDOM.render(<Table table={table} />, document.querySelector("#root"));
i am planning to build a generic filter like Gbif Have.
My question is how to approach this problem.
I like to use ReactJs for this project.
What other technology i need to look into along with React and redux in order to design such a generic filter.
I try to design this filter using React and redux only.
In my approach, i try to maintain the query parameter inside the state variable of the get_data method, in which i am fetching the data from the server. As somebody click on any filter button, then i pass custom event from that filter component along with query parameter and handle this event in get_data method. In get_data method again i am saving this value in get_data state parameter and again getting the new filtered data.
Now the Problem with above approach is that as the number of parameter increases it become very difficult to maintain.
my get_data constructor look like this.
constructor(props){
super(props);
this.state={
params:{
max:10,
offset:0,
taxon:[],
sGroup:[],
classification:undefined,
userGroupList:[],
isFlagged:undefined,
speciesName:undefined,
isMediaFilter:undefined,
sort:"lastRevised",
webaddress:""
},
title:[],
groupName:[],
userGroupName:[],
view:1
}
this.props.fetchObservations(this.state.params)
this.loadMore=this.loadMore.bind(this);
};
The way i am getting data from filter component is something like this.
this is my handleInput method which fire onSelect method from one of the filter.
handleInput(value,groupName){
this.setState({
active:true
})
this.props.ClearObservationPage();
var event = new CustomEvent("sGroup-filter",{ "detail":{
sGroup:value,
groupName:groupName
}
});
document.dispatchEvent(event);
};
the way i am handling this event in my get_data component is look something like this.
sGroupFilterEventListner(e){
const params=this.state.params;
if(!params.sGroup){
params.sGroup=[];
}
console.log("params.sGroup",params.taxon)
params.sGroup.push(e.detail.sGroup)
params.sGroup=_.uniqBy(params.sGroup)
const groupName=this.state.groupName;
var titleobject={};
titleobject.sGroup=e.detail.sGroup;
titleobject.groupName=e.detail.groupName;
groupName.push(titleobject);
let newgroupname=_.uniqBy(groupName,"sGroup")
params.classification=params.classification;
let isFlagged=params.isFlagged;
let speciesName=params.speciesName;
let MediaFilter=params.isMediaFilter;
let taxonparams=params.taxon;
taxonparams= taxonparams.join(",");
let sGroupParams=params.sGroup;
sGroupParams=sGroupParams.join(",");
let userGroupParams=params.userGroupList;
userGroupParams=userGroupParams.join(",");
let newparams={
max:10,
sGroup:sGroupParams,
classification:params.classification,
offset:0,
taxon:taxonparams,
userGroupList:userGroupParams,
isFlagged:isFlagged,
speciesName:speciesName,
isMediaFilter:MediaFilter,
sort:params.sort
}
this.props.fetchObservations(newparams);
this.setState({
params:{
max:10,
sGroup:params.sGroup,
classification:params.classification,
offset:0,
taxon:params.taxon,
userGroupList:params.userGroupList,
isFlagged:isFlagged,
speciesName:speciesName,
isMediaFilter:MediaFilter,
sort:params.sort
},
groupName:newgroupname
})
}
I registered and unRegistered the sGroupFilterEventListner in my componentDidMount and componentunmount method.
Presently i am also not considering the case where if somebody type in url bar, the filter panel change automatically.
Please consider all the above scenario and suggest me a generic way to do the same. thanks.
My Current Filter Panle look like this
Here's a quick example (React only, no Redux) I whipped up with a dynamic number of filters (defined in the filters array, but naturally you can acquire that from wherever).
const filters = [
{ id: "name", title: "Name", type: "string" },
{
id: "color",
title: "Color",
type: "choice",
choices: ["blue", "orange"],
},
{
id: "height",
title: "Height",
type: "choice",
choices: ["tiny", "small", "big", "huge"],
},
{
id: "width",
title: "Width",
type: "choice",
choices: ["tiny", "small", "big", "huge"],
},
];
const filterComponents = {
string: ({ filter, onChange, value }) => (
<input
value={value || ""}
onInput={e => onChange(filter.id, e.target.value)}
/>
),
choice: ({ filter, onChange, value }) => (
<select
value={value || ""}
onInput={e => onChange(filter.id, e.target.value)}
size={1 + filter.choices.length}
>
<option value="">(none)</option>
{filter.choices.map(c => (
<option value={c} key={c}>
{c}
</option>
))}
</select>
),
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = { filters: {} };
this.onChangeFilter = this.onChangeFilter.bind(this);
}
onChangeFilter(filterId, value) {
const newFilterState = Object.assign({}, this.state.filters, {
[filterId]: value || undefined,
});
this.setState({ filters: newFilterState });
}
renderFilter(f) {
const Component = filterComponents[f.type];
return (
<div key={f.id}>
<b>{f.title}</b>
<Component
filter={f}
value={this.state.filters[f.id]}
onChange={this.onChangeFilter}
/>
</div>
);
}
render() {
return (
<table>
<tbody>
<tr>
<td>{filters.map(f => this.renderFilter(f))}</td>
<td>Filters: {JSON.stringify(this.state.filters)}</td>
</tr>
</tbody>
</table>
);
}
}
ReactDOM.render(<App />, document.querySelector("main"));
body {
font: 12pt sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<main/>
(originally on https://codepen.io/akx/pen/JyemQQ?editors=0010)
Hope this helps you along.