So, I'm trying to create a simple program in react. The state is an array of objects. It prints these objects into a table. Standard table construction (, , ) etc. works fine for the initial render, but when I try to re-render I get bizarre errors I'm trying to adapt to. So, this is my code so far.
import React from "react";
import ReactDOM from "react-dom";
import { useState } from "react";
import "./style.css";
function Trains() {
const [trainsList, setTrainsList] = useState([
{ name: "Thomas", destination: "Boston" },
{ name: "Duncan", destination: "New York" },
]);
return (
<div>
<table>
<thead>name</thead>
<thead>destination</thead>
{trainsList.map((train) => (
<tbody key={train.name}>
<td>{train.name}</td>
<td>{train.destination}</td>
</tbody>
))}
</table>
<br />
Train Name: <input type="text" id="trainName" />
<br />
Destination: <input type="text" id="trainDest" />
<br />
<button
onClick={() =>
setTrainsList((prev) =>
prev.push({ name: "Dennis", destination: "Denville" })
)
}
>
Submit
</button>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Trains />);
But even changing the table lingo like that still isn't enough. I get these bizarre errors:
bundle.js:7294 Warning: validateDOMNesting(...): Text nodes cannot appear as a child of <thead>.
at thead
at table
at div
at Trains (http://localhost:3000/static/js/bundle.js:32:86)
And
bundle.js:7294 Warning: validateDOMNesting(...): <td> cannot appear as a child of <tbody>.
at td
at tbody
at table
at div
at Trains (http://localhost:3000/static/js/bundle.js:32:86)
How on Earth can I create a table or use "thead" to any effect if I can't even put any text in the headings or put tds in my table rows? How the heck do I make tables in react?
Table Rows
You've probably intended to make your table look something like that:
<table>
<thead>
<tr>
<th>name</th>
<th>destination</th>
</tr>
</thead>
<tbody>
{trainsList.map((train) => (
<tr key={index}>
<td>{train.name}</td>
<td>{train.destination}</td>
</tr>
))}
</tbody>
</table>
Note the <tr> elements to create a table rows both in the header and in the body.
Key in a map
Also, note the key attribute in the <tr> element. It is not a good practice to use an index of an array as a key, although it is much better that using a name that can be repeated.
Fix setState
Finally, the function to update state after clicking a button can be rewritten to that:
<button
onClick={() =>
setTrainsList((prev) => {
return [
...prev,
{ name: "Dennis", destination: "Denville" },
];
})
}
>
Submit
</button>
The error is in the setTrainsList method defined in the Submit button. The push method returns the new length of the array, but does not change the original array. Instead, you must use the concat method or the spread syntax to add a new element to an array in React:
<button onClick={() =>
setTrainsList((prev) => [...prev, { name: "Dennis", destination: "your butt" }])
}
>
Submit
</button>
This should resolve the error and add a new element to the train list when you click the button.
Related
I am using bootstrap-table and trying to display a table with data from a firebase realtime database.
The contents of the database render correctly, however I am also seeing a "No matching records found" message in my table.
Using the search and sort functionality also clear the table completely and a refresh is needed to restore the data.
import React, { useState, useEffect } from "react";
import firebaseDb from "../firebase";
const StarTrekTable = () => {
var [contactObjects, setObjects] = useState({});
useEffect(() => {
firebaseDb.child("unwatched").on("value", (snapshot) => {
if (snapshot.val() != null)
setObjects({
...snapshot.val(),
});
});
}, []);
return (
<>
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display -4 text-center">Star trek series</h1>
</div>
</div>
<div>
<div className="col-md-12">
<table data-toggle="table" data-pagination="true" data-search="true">
<thead className="thread-light">
<tr>
<th>Title</th>
<th>Series</th>
<th>Season</th>
<th>Episode</th>
<th>Stardate</th>
<th>Air Date</th>
</tr>
</thead>
<tbody>
{Object.keys(contactObjects).map((id) => {
return (
<tr>
<td>{contactObjects[id].title}</td>
<td>{contactObjects[id].series}</td>
<td>{contactObjects[id].season}</td>
<td>{contactObjects[id].episode}</td>
<td>{contactObjects[id].stardate}</td>
<td>{contactObjects[id].airdate}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</>
);
};
export default StarTrekTable;
Any help would be very much appreciated.
Table shows both records as expected and "No matching records found"
Image of my Firebase realtime database setup
On the first render of your component, your contactsObject is {}, which leads to <tbody> being empty on the first render. Somewhere else in your code, probably somewhere in bootstrap, the "No matching records found" is added into the source. Once your listener has found the unwatched episodes, downloaded, parsed, and added them to the <tbody> react then removes the entries it has added (none) and then inserts the new entries. Because "No matching records found" was added outside of React, it doesn't know that it is there and isn't removed. Rather than let whatever other code is adding this row in, define it yourself when you are still loading data or the table is empty.
If you aren't already using react-bootstrap, you should use it so that the original JavaScript bundled with Bootstrap doesn't fight with React for control like you've seen here.
Note: When rendering arrays of data in React, make sure to specify a key for each entry so that React can efficiently manipulate the entries on new renders.
Cleaning up some other things too, this gives:
import React, { useState, useEffect } from "react";
import firebaseDb from "../firebase";
const StarTrekTable = () => {
// - renamed variables to reflect content
// - used array instead of object
// - used const instead of var
const [episodeData, setEpisodeData] = useState([]);
const [episodesLoaded, setEpisodesLoaded] = useState(false);
useEffect(() => {
// store Reference for attaching and detaching listeners
const unwatchedRef = firebaseDb.child("unwatched");
// because you use a `.on` listener, don't forget to store
// the listener so we can detach it
const listener = unwatchedRef
.on("value", (snapshot) => {
if (!snapshot.exists()) {
// no data to show and/or unwatched is empty
// update state variables
setEpisodeData([]);
setEpisodesLoaded(true);
return;
}
// Use forEach to maintain the order of the query, using `...snapshot.val()`
// for the whole result will often lead to unexpected results.
const freshEpisodeData = [];
snapshot.forEach((childSnapshot) => {
freshEpisodeData.push({ ...childSnapshot.val(), key: childSnapshot.key });
});
// update state variables
setEpisodeData(freshEpisodeData);
setEpisodesLoaded(true);
});
// when component is detached, remove the listener
return () => unwatchedRef.off("value", listener);
}, []);
return (
<>
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display -4 text-center">Star trek series</h1>
</div>
</div>
<div>
<div className="col-md-12">
<table data-toggle="table" data-pagination="true" data-search="true">
<thead className="thread-light">
<tr>
<th>Title</th>
<th>Series</th>
<th>Season</th>
<th>Episode</th>
<th>Stardate</th>
<th>Air Date</th>
</tr>
</thead>
<tbody>
{ !episodesLoaded
? (
<tr key="data-loading">
<td colspan="6"><i>Loading...</i></td>
</tr>
)
: (episodeData.length === 0
? (
<tr key="data-empty">
<td colspan="6">No unwatched episodes available. You're all caught up!</td>
</tr>
)
: episodeData.map(({ key, title, series, season, episode, stardate, airdate }) => (
<tr key={key}>
<td>{title}</td>
<td>{series}</td>
<td>{season}</td>
<td>{episode}</td>
<td>{stardate}</td>
<td>{airdate}</td>
</tr>
)))
}
</tbody>
</table>
</div>
</div>
</>
);
};
export default StarTrekTable;
Note: For static arrays that are unlikely to change, your database structure is fine, but I would strongly advise against using numerical keys for your data (0, 2, etc.) and instead use something else like (S01E01-02, S01E03, etc.) or Firebase Push IDs. Take at this (yet still valid) blog post for the reasoning.
I have a class that listens for clicks on a map, and when a click has been detected, it is added to an array routeWayPoints:
import React, { useState } from "react";
import Tab from "./Tab";
import RouteWayPointsTable from "./RouteWayPointsTable";
import { map, removeFromTheMap, addToTheMap } from "../javascript/mapInterprater";
function RouteAnalysisTab() {
const [routeWayPoints, setRouteWayPoints] = useState([]);
function beginPlottingRoute() {
setRouteWayPoints([]);
}
map.on("click", onMapClick);
function onMapClick(event) {
if (isRouteActive) {
setRouteWayPoints(routeWayPoints.concat(event.latlng));
}
}
return (
<Tab id="analyse-route" title="Analyse route" className="data-display__tab-content">
<h3>Analyse route</h3>
{!isRouteActive && routeWayPoints.length > 0 && (
<button className="button" id="new-route-button" type="button" onClick={() => beginPlottingRoute()}>
Plot new route
</button>
)}
<div className="data-display__tab-content--route-analysis">
{!isRouteActive && routeWayPoints.length === 0 && (
<button className="data-display__button--home" id="plot-route-button" type="button" onClick={() => beginPlottingRoute()}>
Plot a route
</button>
)}
<RouteWayPointsTable routeWayPoints={routeWayPoints} />
</div>
</Tab>
);
}
export default RouteAnalysisTab;
I pass the array to the RouteWayPointTable component and attempt to loop over each item and create a row in the table using the RouteWayPointRow component.
import React from "react";
import RouteWayPointRow from "../components/RouteWayPointRow";
function RouteWayPointsTable(props) {
return (
<div id="route-analysis">
<table className="data-display__waypoint-table" id="analyse-route-waypoints">
<thead>
<tr>
<th className="data-display__waypoint-table--way-point-col">Way point</th>
<th className="data-display__waypoint-table--lat-col">Latitude</th>
<th className="data-display__waypoint-table--lng-col">Longitude</th>
<th className="data-display__waypoint-table--remove-col"></th>
</tr>
</thead>
<tbody>
{props.routeWayPoints.map((wayPoint, index) => {
return <RouteWayPointRow wayPoint={wayPoint} index={index} key={index} />;
})}
</tbody>
</table>
</div>
);
}
export default RouteWayPointsTable;
Now the rows are being displayed, but I'm observing some strange behaviour I didn't expect.
As I add points to the array, React is iterating over every state the array existed in before it renders the latest point, so the amount of time it takes to update the table is getting exponentially longer as it has to perform more loops over the array every time a new point has been added.
So my issue was that I had declared the map listener inside the component, and so a new listener was being created each time I clicked on the map which cause React to re-render the DOM
function RouteAnalysisTab() {
const [routeWayPoints, setRouteWayPoints] = useState([]);
function beginPlottingRoute() {
setRouteWayPoints([]);
}
map.on("click", onMapClick);
Probably not the most elegant solution, but removing the listener before creating it did the trick:
map.off("click");
map.on("click", onMapClick);
here is the code
<tbody>
<tr>
<td>
<Link to={{pathname: '/user',
state: {
number: content.number,
name: content.name,
age: content.age
}
}}
/>
{content.accountOwner}
</td>
<td>
{content.address}
</td>
<td>{content.profession}</td
</tr>
</tbody>
This row becomes clickable in chrome but in IE as the is put in the first column, only the first column becomes clickable and not the entire row.
Can anyone please help me to resolve this issue?
You can use .push() method to programatically navigate to that page.
If you are using the latest react-router-dom you can use the useHistory hook to be able to use the push method.
Another advantage from push() will not break your UI, since the <Link /> component could cause problems in your UI since it's another element nested.
import React from 'react';
import { useHistory } from 'react-router-dom';
export const ExampleComponent = ({ content }) => {
const history = useHistory();
function handleRowClick() {
const { number, name, age } = content;
history.push('/user', { number, name, age });
}
return (
<tbody>
<tr onClick={handleRowClick}>
<td>
{content.accountOwner}
</td>
<td>
{content.address}
</td>
<td>{content.profession}</td>
</tr>
</tbody>
)
};
I've got my parent component iterating through an array and passing each "row" from mongo to the child component.
<tbody>
{this.state.claims.map ( claim => <ClaimRow claim= { claim } /> )}
</tbody>
The data being passed looks like this:
{"_id":"5b0d5b7f035a00f06003e6b8","claimID":"123456","claimDate":"2018-05-14T00:00:00.000Z","carrier":"BCBS NJ"}
I'm attempting to access all of the fields inside "claim" but I can't figure out how to properly access the field. Since there isn't a state, I am using a pure function. I just listed the fields below since I'm not able to successfully figure this out.
const ClaimRow = ( {claim} =this.props ) => (
<div className="inline fields">
<Form.Field>
<tr>
<td> {JSON.stringify (claim)} </td>
<td>{claimID}</td>
<td>{carrier}</td>
</tr>
</Form.Field>
</div>
);
ClaimRow.propTypes = {
claim: PropTypes.string.isRequired
};
export default ClaimRow;
It is not a proper way to provide form and div tags inside . If you want to assign class name, you can assign it to the td, or give div inside . Now without these you can properly loop thru the data in the following way,
const ClaimRow = (props) => {
let claim = props.claim;
return (
<tr>
<td>{JSON.stringify (claim)} </td>
<td>{claim.claimID}</td>
<td>{claim.carrier}</td>
</tr>
);
}
export default ClaimRow;
You should access through this.props.claim,
const ClaimRow = ({claim}) => (
<tr>
<td>{JSON.stringify(claim)}</td>
<td>{claim.claimID}</td>
<td>{claim.carrier}</td>
</tr>
export default ClaimRow;
you can even say const { claim } = this.props, so you dont need to type this.props every time
Thx Rohit, the props log helped solve the problem. Invoking the component twice was causing odd behavior and highlighting it resolved it quickly.
I'm getting the following error:
Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component />
external components
import Comment from 'material-ui-icons/Comment'
import Attachment from 'material-ui-icons/Attachment'
import History from 'material-ui-icons/History'
import TrendingUp from 'material-ui-icons/TrendingUp'
this.components = {
comment: Comment,
attachment: Attachment,
history: History,
trendingup: TrendingUp
}
render:
{this.menu.map((data, index) => (
<span key={index}>
{dynamicClassName = this.state[data.value.class]}
<span
key={index}
id="historicalTestsColorBandTitle"
className={"mobileBottomBannerOptions "+dynamicClassName}
onClick={this.handleBannerOptionsClick.bind(this, data.value.type)}
>
{DynamicComponentName = this.components[data.value.icon]}
{!dynamicClassName &&
<div>
<table><tbody><tr>
<td>
<DynamicComponentName
className="mobileBottomBannerIcon"
/>
</td>
</tr></tbody></table>
</div>
}
{dynamicClassName &&
<div>
<table><tbody><tr>
<td>
<DynamicComponentName
className="mobileBottomBannerIcon"
/>
</td>
<td className="menuOptionTitle">
{data.value.name}
</td>
</tr></tbody></table>
</div>
}
</span>
</span>
))}
You can see 'DynamicComponentName' is where the component name is created. I need to use a method like this as the order of these components changes depending on the last one clicked.
The components display fine but I get the error message at the top...
Any advice on how to resolve the error message?
thankyou!
You need to move your assignment out of JSX:
render() {
const dynamicClass = this.state[data.value.class];
const DynamicComponent = this.components[data.value.icon];
return // ...
}
If you need to make your assignment inside of map, you can do that too:
this.menu.map((data, index) => {
const dynamicClass = this.state[data.value.class];
const DynamicComponent = this.components[data.value.icon];
return // ...
})