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
Related
I have a basic React Table that's not rendering data when I build the object array from data returned by an API.
I'm refactoring the data from the API into it's own object, pushing the objects to an array, and then setting it to state (using useState), which the table should then render.
let tableData = []
pendingKYC.forEach(async (address) => {
let userData = await contract.retrieveOwnerInfo(address);
let refactored = {
address: address,
firstName: userData.firstName,
lastName: userData.lastName,
}
tableData.push(refactored);
});
setPendingKYCRequestsTableData(tableData);
However it's not rendering. Headers only, the rest is blank. I discovered that if I create a "dummy" array of objects containing the exact same data, and pushing that to the state instead, it works fine.
const testData = [{
address: "0x8e5acD67EFDdB1fb8c54491C8A0EB6f3CFD77ae3",
firstName: "Killua",
lastName: "Zoldyck",
},
{
address: "0x2b71400D0Eb16b0C878aa12d4cF9FbE03fFcF36f",
firstName: "Gon",
lastName: "Freecs",
}];
setPendingKYCRequestsTableData(testData);
Using console.logs all over the place and inspecting it in the Chrome console, the only difference I can see is that it appears to be in a different format.
The one that doesn't work (the first one) is a [] type of array.
The one that does work (my dummy data) is a [{...}, {...}] type of array.
What the heck is the difference? Can someone shed some light on what I might be doing wrong?
Not to do a code-dump but here is my Table component
const PendingKYCTable = ({pendingKYCRequestsTableData, approveButtonHandler}) =>{
useEffect(()=>{
console.log("PendingKYCTable parent component useEffect", pendingKYCRequestsTableData);
});
console.log("PendingKYCTable rendered", pendingKYCRequestsTableData);
const columns = React.useMemo(
() => [
{
Header: "Pending KYC Requests",
columns: [
{
Header: "Address",
accessor: "address"
},
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Last Name",
accessor: "lastName"
},
]
}
]);
const Table = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow
} = useTable({
columns,
data
});
console.log("Table component data var", data);
useEffect (() => {
console.log("Table useEffect",data);
});
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render("Header")}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
})}
</tr>
);
})}
</tbody>
</table>
);
};
return(
<div>{pendingKYCRequestsTableData && <Table columns={columns} data={pendingKYCRequestsTableData} getTrProps={approveButtonHandler}/>}</div>
);
};
export default PendingKYCTable;
and I'm invoking it from the parent component, passing in the state containing the data like this:
<div><PendingKYCTable pendingKYCRequestsTableData={pendingKYCRequestsTableData} approveButtonHandler={approveButtonHandler}/></div>
I found some advice elsewhere that was very helpful so I'm showing how it was resolved. Apparently you can't use a forEach loop with async requests. A regular for loop works just fine. I replaced my forEach loop with this:
for (var i = 0; i < pendingKYC.length; i++){
const address = pendingKYC[i];
let userData = await contract.retrieveOwnerInfo(address);
let refactored = {
address: address,
firstName: userData.firstName,
lastName: userData.lastName,
}
tableData.push(refactored);
}
I have a parent component and a child component. The child component initially renders data into a form but upon changing the data, the child component does not update.
Parent Component:
import React from 'react'
import { connect } from 'react-redux'
import styles from '../styles'
import ExpressionsForm from './expressionsForm'
class EditCondition extends React.Component {
constructor (props) {
super(props)
this.state = {
condition: null
}
this.updateExpression = this.updateExpression.bind(this)
this.changes = false
}
componentWillMount () {
let conditionid = this.props.data.id
let condition = this.props.conditions.find(c => {
return (c.id = conditionid)
})
this.setState({ condition })
}
updateExpression (e) {
let expressionid = e.currentTarget.dataset.expressionid
let field = e.currentTarget.dataset.field
let value = e.target.value
let condition = this.state.condition
let expression = condition.expressions[expressionid]
expression[field] = value
condition.expressions[expressionid] = expression
this.changes = true
this.setState({ condition })
console.log('updateExpression condition: ', condition)
}
render () {
let condition = this.state.condition
if (!this.state.condition) {
return (
<div>
The selected condition with ID "{this.props.data.id}" did not load. It
may not exist. Refresh and try again.
</div>
)
}
let groupOptions = this.props.gambitGroups.map(g => {
return (
<option value={g.id} key={'group' + g.id}>
{g.name}
</option>
)
})
console.log('RENDER editCondition: ', condition) // <-- Note: This always logs as expected
let expressionsJSX = condition.expressions.map((expression, i) => {
expression.id = i
console.log('expression: ', expression) // <-- Note: This always logs as expected
return (
<ExpressionsForm
key={'expressionsForm_' + i}
expression={expression}
deleteExpression={this.deleteExpression}
updateExpression={this.updateExpression}
updateExpressionData={this.updateExpressionData}
/>
)
})
return (
<table>
<thead>
<tr>
<th {...styles.modal.tableHeaderLeftAlign}>
Device & Data Point
</th>
<th {...styles.modal.tableHeaderLeftAlign}>Operator</th>
<th {...styles.modal.tableHeaderLeftAlign}>Value</th>
<th {...styles.modal.tableHeaderLeftAlign}>PlateValue</th>
<th {...styles.modal.tableHeaderLeftAlign}> </th>
</tr>
</thead>
<tbody>{expressionsJSX}</tbody>
</table>
)
}
}
export default connect(
(state, ownProps) => ({
user: state.user,
users: state.users,
gambitGroups: state.gambitGroups,
// deviceGroups: state.deviceGroups,
conditions: state.conditions,
reactions: state.reactions,
setEditMode: ownProps.setEditMode,
navByName: ownProps.navByName
}),
dispatch => ({
addImage: file => dispatch({ type: 'UPDATE_CONDITION_LOGO', file }),
updateCondition: condition =>
dispatch({ type: 'UPDATE_CONDITION', condition })
})
)(EditCondition)
And Child Component:
import React from 'react'
import { connect } from 'react-redux'
import styles from '../styles'
class ExpressionsForm extends React.Component {
constructor (props) {
super(props)
this.state = {}
this.updateExpression = this.updateExpression.bind(this)
}
updateExpression (e) {
this.props.updateExpression(e)
}
render () {
let expression = this.props.expression
console.log('expression: ', expression) // Note: logs initial render only.
let data = expression.data
let deviceId = data.deviceId
let dataPointIndex = data.dataPointIndex
let operator = expression.operator
let plateValue = expression.plateValue
let value = expression.value
console.log('RENDER expressionForm: ', expression) // Note: logs initial render only
let deviceOptions = this.props.devices.map((device, i) => {
return (
<option value={device.id} key={'device_' + i}>
{device.userAssignedName}
</option>
)
})
let dataPointOptions = this.props.devices[0].inputs.map((input, i) => {
return (
<option value={input.id} key={'input_' + i}>
{input.name} currentValue: {input.value}
</option>
)
})
let operatorOptions = ['==', '!=', '<=', '>=', '<', '>'].map(
(operator, i) => {
return (
<option value={operator} key={'operator_' + i}>
{operator}
</option>
)
}
)
return (
<tr>
<td>
<select
{...styles.modal.inputSexy}
style={{ marginBottom: '20px' }}
data-field='deviceid'
data-expressionid={expression.id}
value={deviceId}
onChange={this.updateExpressionData}
>
<option value=''></option>
{deviceOptions}
</select>
<select
{...styles.modal.inputSexy}
data-field='dataPointIndex'
data-expressionid={expression.id}
value={dataPointIndex}
onChange={this.updateExpressionData}
>
<option value=''></option>
{dataPointOptions}
</select>
</td>
<td>
<select
{...styles.modal.inputSexy}
style={{ width: '75px' }}
data-field='operator'
data-expressionid={expression.id}
value={operator}
onChange={this.updateExpression}
>
<option value=''></option>
{operatorOptions}
</select>
</td>
<td>
<input
{...styles.modal.inputSexy}
style={{ width: '50px' }}
data-field='value'
data-expressionid={expression.id}
value={value}
onChange={this.updateExpression}
/>
</td>
<td>
<input
{...styles.modal.inputSexy}
style={{ width: '88px' }}
data-expressionid={expression.id}
data-field='plateValue'
value={plateValue}
onChange={this.updateExpression}
/>
</td>
<td>
<i className='fa fa-close'
data-expressionid={expression.id}
onClick={this.deleteExpression}
></i>
</td>
</tr>
)
}
}
export default connect(
(state, ownProps) => ({
user: state.user,
users: state.users,
devices: state.devices,
gambitGroups: state.gambitGroups,
// deviceGroups: state.deviceGroups,
conditions: state.conditions,
reactions: state.reactions,
setEditMode: ownProps.setEditMode,
navByName: ownProps.navByName
}),
dispatch => ({
addImage: file => dispatch({ type: 'UPDATE_XXX', file })
})
)(ExpressionsForm)
I have an array of objects in the redux store called Conditions. The parent component gets an ID of one of these conditions, finds the correct condition, and loads it into state via componentWillMount to be modified by the user. The condition has an array of objects on it called expressions. Each of these expressions are passed to the child component called ExpressionsForm.
so we loop over the expressions via the map function and return the resulting JSX as expressionsJSX.
let expressionsJSX = condition.expressions.map((expression, i) => {
expression.id = i
console.log('expression: ', expression) // <-- Note: This always logs as expected
return (
<ExpressionsForm
key={'expressionsForm_' + i}
expression={expression}
deleteExpression={this.deleteExpression}
updateExpression={this.updateExpression}
updateExpressionData={this.updateExpressionData}
/>
)
})
Note that the has the expression passed to it expression={expression}
And in the child component's render you see
let expression = this.props.expression
console.log('expression: ', expression) // Note: logs initial render only.
Since this is a prop, whether it's being console.log'd or rendered into some JSX doesn't matter - when the prop changes the changes should also be re-rendered. BUT it's not doing it in this case. Why?
For example, I have 1 expression saved on 1 condition. It renders, I click into the plateValue input field of the expression - which contains a 5 by default - and attempt to add a 6 after the 5. When the parent component updates state an re-renders I see the in the console.log's that the expression's plateValue field now contains a '56'...it just doesn't render in the child component....!?
Here is an example console.log
Initial Render:
RENDER editCondition: {id: "1", group: 1, name: "Temperature >= 75F",
meta: "If >= 75F in greenhouse turn on AC until 5 degrees cooler than
75F", expressions: Array(1)} editCondition.jsx:191 expression: {data:
{…}, operator: ">=", value: "75", plateValue: "5", id: 0}
expressionsForm.jsx:39 RENDER expressionForm: {data: {…}, operator:
">=", value: "75", plateValue: "5", id: 0}
Click into plateValue field and add a '6', parent rerenders...and:
editCondition.jsx:188 RENDER editCondition: {id: "1", group: 1, name:
"Temperature >= 75F", meta: "If >= 75F in greenhouse turn on AC until
5 degrees cooler than 75F", expressions: Array(1)}
editCondition.jsx:191 expression: {data: {…}, operator: ">=", value:
"75", plateValue: "56", id: 0} editCondition.jsx:153 STATE SET!
updateExpression condition: {id: "1", group: 1, name: "Temperature >=
75F", meta: "If >= 75F in greenhouse turn on AC until 5 degrees cooler
than 75F", expressions: Array(1)}
I see a 'plateValue: "56"' in there. So why isn't it rerendering in
the child component? So confused.
I've tried componentWillReceiveProps, componentWillUpdate, et al. I can't even get these to fire off a console.log.
Something is going on that I can't figure out. I've been doing React for a long time and I'm stumped. That doesn't happen very often anymore.
Thanks in advance for your help
PS I did look at getDerivedStateFromProps - It's great that the documentation provides examples, but they don't explain what the props and state parameters actually are. The docs suck. Their explanation sucks. Their example doesn't illustrate what it actually does. I only ever use componentWillReceiveProps to know when a prop has changed, and then update state or whatever. getDerivedStateFromProps just confuses me. None the less I played around with it and couldn't get it to work either.
It looks like the same expression object is being passed in all the time.
React checks the props that a component receives for changes when deciding to render. It finds that none of the props items have changed, they are all the same objects as before, and concludes that the child component does not need to be rerendered. It will not do a deep inspection of all properties of each prop.
This also explains why a rerender can be forced by making a copy of the expression object. The copy is always a new object, thus causing a rerender, regardless if any of its content have changed or not.
You could avoid this situation as you do already, by making a copy, or by dissecting the expression object into its properties and then feeding each of those as separate props into the child.
As a final note, a copy can also be made by passing it in as expression={{...expression}}.
Updating the JSON state using setState is not happening
1.I want to change the particular quantity value in the JSON, for this I wrote an
IncrementHandler method to a tag and implemented the function but it is not updating my
main state
2.I implemented the setState method inside the function for updating the state.
import React, { Component } from 'react';
export default class App extends Component {
state = {
value: 0,
students: [
{
item: 'salad',
qty: 2,
department: 'Information Technology',
rollno: '123'
},
{
item: 'cheese',
qty: 2,
department: 'Computer Engineering',
rollno: '456'
},
{
item: 'meat',
qty: 2,
department: 'Information Technology',
rollno: '789'
}
]
};
render() {
const IncrementHandler = (type) => {
if (type === 'salad') {
this.setState((prevState) => ({
qty: this.state.students[0].qty + 1
}));
}
};
return (
<div>
<div>
<div>
<table border="2">
<tbody>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Button</th>
</tr>
{this.state.students.map((item, i) => (
<tr key={i}>
<td>{item.item}</td>
<td>{item.qty}</td>
<td onClick={() => IncrementHandler(item.item)}>click</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
}
Issues
You aren't updating the nested state in the student array. You are generating a new root state key qty.
this.setState(prevState => ({
qty : this.state.students[0].qty + 1
}))
Solution
Shallow copy the student array to create a new array reference, and shallow copy the element you want to update within the array. General note about updating nested state in react, you need to shallow copy any object you are updating since React uses shallow object equality to reconcile differences.
const IncrementHandler = item => {
this.setState(prevState => ({
students: prevState.students.map(student =>
student.item === item
? {
...student,
qty: student.qty + 1,
}
: student,
),
}));
};
Note: You should probably also declare this callback outside the render function. It will still work defined there in the render method, but it goes against convention.
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 :)
Currently, I render Material-UI's <Table>'s <TableRow> (http://www.material-ui.com/#/components/table) with an array of <TableRow>s and by using .map(). Each <TableRow> has a <TableRowColumn> that represents a first name, like so <TableRowColumn>Josh</TableRowColumn>.
But, if a user presses a button, I would like to sort the <TableRow> array alphabetically by the <TableRowColumn>'s first name. So say for example out of 10 <TableRow>s, if the array[0] had a first name Conny and array[1] had Adrian, would like the array[1] to become array[0].
What would the right approach to this? Any guidance or insight would be greatly appreciated it.
EDIT
Each row would be rendered like so with the array rows, that has objects with properties firstName and favColor:
{
rows.map((row) => {
return(
<UserRow
firstName={row.firstName}
favColor={row.favColor}
/>
)
})
}
And each row is defined like so:
const UserRow = (props) => {
const {firstName, favColor} = props
return (
<TableRow>
<TableRowColumn>{firstName}</TableRowColumn>
<TableRowColumn>{favColor}</TableRowColumn>
</TableRow>
)
}
I would sort the array before applying the map operation that will create the TableRows.
The react way of thinking is declarative. This means that on a visual level, you should provide the elements as they should be displayed. Hence they get sorted before they are passed to the view component.
For example (I couldn't use material-ui elements since the example didn't run in the stackoverflow setup. Just replace all elements of TableComponent with their material-ui alter ego.):
const data = [
{firstname: "John", lastname: "Rover", id:12},
{firstname: "Bob", lastname: "Taylor", id:24},
{firstname: "Lucy", lastname: "Heart", id:43}
]
// The table component is unaware of the data order operations
const TableComponent = ({tableData}) => <table><tbody>
{tableData.map(d=> <tr key={d.id}>
<td>{d.firstname}</td>
<td>{d.lastname}</td>
</tr>)}
</tbody></table>
// The parent component takes care of feeding the Table
// component with the data in the correct order.
class App extends React.Component {
state = { sortBy: "firstname"}
handleChange = (event) => this.setState(
{sortBy: event.target.value}
);
render () {
const {data} = this.props;
const {sortBy} = this.state;
const sortedData = data.sort((a,b) => a[sortBy]>b[sortBy]?1:-1)
return <div>
Sort by
<select value={sortBy} onChange={this.handleChange}>
<option value="firstname">First Name</option>
<option value="lastname">Last Name</option>
</select>
<h2>The table: </h2>
<TableComponent tableData={sortedData} />
</div>
}
}
ReactDOM.render(
<App data={data} />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Here's a full example of an app with a table where its rows can be sorted by clicking on the header. Comments are inline and the full example is available here
The table's state contains the rows which are sorted whenever a table column header is clicked as well as the property name of the sorted column.
import React from 'react';
import { MuiThemeProvider} from 'material-ui';
import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui';
// properties of TableHeader component
let headerProps = {
enableSelectAll: false,
displaySelectAll: false,
adjustForCheckbox: false
};
// initial set of rows, simulating data from the database
let rows = [
{firstName: "Adrian", favColor: "gold", uniqueId: 0 },
{firstName: "Alma", favColor: "green", uniqueId: 1 },
{firstName: "Conny", favColor: "black", uniqueId: 2 },
{firstName: "Jane", favColor: "blue", uniqueId: 3 }
];
// our table hader information, key is the name of the
// property to sort by when the header is clicked
let headers = [
{name: "First Name", key: "firstName"},
{name: "Favorite Color", key: "favColor"}
];
// our table component that can sort columns
class SortableTable extends React.Component {
constructor(props){
super(props);
this.state = {rows, sortBy: 'firstName'};
}
renderHeaders(){
let header= headers.map( (h) => {
return <SortableHeader
key={h.key}
name={h.name}
onClicked={()=>this.updateSortBy(h.key)}
isSortColumn={this.state.sortBy == h.key}/>
});
return <TableRow>{header}</TableRow>;
}
renderRows() {
return this.state.rows.map( (row, i) => <UserRow {...row} key={row.uniqueId}/> );
}
updateSortBy(sortBy){
// multiple clicks on the same column reverse the sort order
if( sortBy == this.state.sortBy ){
this.setState( {rows: [...this.state.rows.reverse()]} );
return;
}
let rows = [...this.state.rows];
rows.sort( (a,b) => {
if (a[sortBy] < b[sortBy])
return -1;
if(a[sortBy] > b[sortBy])
return 1;
return 0;
});
this.setState({rows, sortBy});
}
render() {
return (
<MuiThemeProvider>
<Table>
<TableHeader {...headerProps}>
{this.renderHeaders()}
</TableHeader>
<TableBody>
{this.renderRows()}
</TableBody>
</Table>
</MuiThemeProvider>
);
}
}
function SortableHeader(props){
let style = {
cursor: "pointer"
}
if(props.isSortColumn){
style.fontWeight = "bold";
style.color = "black";
}
return (
<TableHeaderColumn>
<div style={style} onClick={() => props.onClicked()}>{props.name}</div>
</TableHeaderColumn>
);
}
function UserRow(props){
return (
<TableRow>
<TableRowColumn>{props.firstName}</TableRowColumn>
<TableRowColumn>{props.favColor}</TableRowColumn>
</TableRow>
);
}
export default SortableTable;