Skip to main content
Coding

How To Create A Semantic UI React Functional Table Component

Gloomhaven Ledger Home Page Screenshot

It took me quite a while to figure out how I wanted to setup the table of scenarios in my Gloomhaven Ledger app that I’ve been developing. If you’re just figuring out Semantic UI React (SUIR) you’ll find it helpful to follow along with the steps I took to build my functional table component. Make sure to reference the SUIR documentation on tables as we’re walking through the steps.

Step 1.

The first thing to do is build out the very basic functional component. Let’s import React, the scenario data, and the table component from Semantic UI React. We then create a functional component called ScenarioTable that returns a div which will hold our table. Finally let’s make sure to export the default table. The scenario data is available as a github gist.

//import, create, and export a base functional component setup
import React from "react";
import scenarios from "./scenarios.json";
import { Table } from "semantic-ui-react";
function ScenarioTable() {
return (
<div className="scenario__table">
<Table>
{/* it's not much but it's a start! */}
</Table>
</div>
);
}
export default ScenarioTable;

Step 2.

Now that we have the basic functional component we can build out the 3 sections of the table. It’s header, body, and footer.

//build out the table structure
// 1. add the header
// 2. add the body
// 3. add the footer
import React from "react";
import scenarios from "./scenarios.json";
import { Table } from "semantic-ui-react";
function ScenarioTable() {
return (
<div className="scenario__table">
<Table>
<Table.Header>
{/*table title and column names */}
</Table.Header>
<Table.Body>
{/* table data here */}
</Table.Body>
<Table.Footer>
{/* table controls here */}
</Table.Footer>
</Table>
</div>
);
}
export default ScenarioTable;

Step 3.

Now it’s time to add the header section to our table. We want the first row to span all the columns and display the table title. Then we want a second row to list each of the column names. Notice here that we’re using icons and text for column names! So we make sure to import Icon from Semantic UI React.

//create the table title and column names
//we're using both icons and text for column names!
//don't forget to import Icon from SUIR
import React from "react";
import scenarios from "./scenarios.json";
import { Table, Icon } from "semantic-ui-react";
function ScenarioTable() {
return (
<div className="scenario__table">
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="5" textAlign="center">
<h1>Scenarios</h1>
</Table.HeaderCell>
</Table.Row>
<Table.Row textAlign="center">
<Table.HeaderCell>Id</Table.HeaderCell>
<Table.HeaderCell>
<Icon name="location arrow" />
</Table.HeaderCell>
<Table.HeaderCell>
<Icon name="unlock" />
</Table.HeaderCell>
<Table.HeaderCell>
<Icon name="check" />
</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{/* table data here */}
</Table.Body>
<Table.Footer>
{/* table controls here */}
</Table.Footer>
</Table>
</div>
);
}
export default ScenarioTable;

Step 4.

This is where things start getting more complex. We want to pull the scenario data from the backend to fill the table’s rows (though for this example we’ll be pulling from the imported json file). In the table body we’re calling the renderScenarioRows function and passing it our scenario data. The renderScenarioRows function takes the array of scenarios and maps over the data. For each scenario we’re creating and returning a table row. You’ll notice that these rows contain checkboxes that we’ve also imported from Semantic UI React. These checkboxes are managed by two useState hooks which track if the scenario is unlocked and if it’s completed.

//render the rows of data, including checkboxes
//iterate over the array of scenarios
//for each scenario return 1 table row of data
//import useState from react
import React, {useState} from "react";
import scenarios from "./scenarios.json";
import { Table, Icon, Checkbox } from "semantic-ui-react";
function renderScenarioRows(scenarios) {
return scenarios.map((scenario, index) => {
let [unlocked, toggleUnlocked] = useState(false);
let [completed, toggleCompleted] = useState(false);
return (
<Table.Row key={scenario.id} positive={completed}>
<Table.Cell textAlign="center">
{scenario.id}.
</Table.Cell>
<Table.Cell>
{scenario.coords}
</Table.Cell>
<Table.Cell>
{/* checkbox for if the scenario is unlocked */}
{/* can't be unchecked if the scenario has already been marked completed */}
<Checkbox
onChange={() => toggleUnlocked(!unlocked)}
checked={unlocked}
disabled={completed}
/>
</Table.Cell>
<Table.Cell>
{/* checkbox for if the scenario is completed */}
{/* can' be checked if it's not unlocked */}
<Checkbox
onChange={() => toggleCompleted(!completed)}
checked={completed}
disabled={!unlocked}
/>
</Table.Cell>
<Table.Cell>
{scenario.name}
</Table.Cell>
</Table.Row>
);
});
};
function ScenarioTable() {
return (
<div className="scenario__table">
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="5" textAlign="center">
<h1>Scenarios</h1>
</Table.HeaderCell>
</Table.Row>
<Table.Row textAlign="center">
<Table.HeaderCell>Id</Table.HeaderCell>
<Table.HeaderCell>
<Icon name="location arrow" />
</Table.HeaderCell>
<Table.HeaderCell>
<Icon name="unlock" />
</Table.HeaderCell>
<Table.HeaderCell>
<Icon name="check" />
</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{renderScenarioRows(scenarios)}
</Table.Body>
<Table.Footer>
{/* table controls here */}
</Table.Footer>
</Table>
</div>
);
}
export default ScenarioTable;

Step 5.

Once we’ve finished step 4 we pretty much have a finished table, but it’s quite long since we have lots of data. So let’s paginate the table data. Let’s refactor our renderScenarioPages function to take in a scenarioPage variable to track the page that we are viewing. To do that we need to create a variable to track the current scenarioPage and we also need the variable maxScenarioPage to track the last page of scenario data based on displaying 15 rows per page. With those two variables we can set up a two button menu in the footer which when clicked either increases or decreases the page that we’re on. That’s taken care of in the changePage function. Notice that we’re making sure that users cannot navigate beyond the first or last page in both the changePage method and by disabling the buttons.

//we have lots of rows of data - so why don't we paginate the table data!
//render the rows of data, including checkboxes which we need to import
//iterate over the array of scenarios
//for each scenario return 1 table row of data
//to change through the pages of data we need to..
//track the current page
//track the last page
//add a menu of two buttons, to change the page number
//add a function that takes care of changing and updating the page number
import React, {useState} from "react";
import scenarios from "./scenarios.json";
import { Table, Icon, Checkbox, Menu } from "semantic-ui-react";
function renderScenarioRows(scenarios, scenarioPage) {
// lets diplay 15 rows of data on each page
let startingIndex = scenarioPage * 15 - 15;
let endingIndex = scenarioPage * 15 - 1;
return scenarios.map((scenario, index) => {
let [unlocked, toggleUnlocked] = useState(false);
let [completed, toggleCompleted] = useState(false);
if (index >= startingIndex && index <= endingIndex) {
return (
<Table.Row key={scenario.id} positive={completed}>
<Table.Cell textAlign="center">
{scenario.id}.
</Table.Cell>
<Table.Cell>
{scenario.coords}
</Table.Cell>
<Table.Cell>
{/* checkbox for if the scenario is unlocked */}
{/* can't be unchecked if the scenario has already been marked completed */}
<Checkbox
onChange={() => toggleUnlocked(!unlocked)}
checked={unlocked}
disabled={completed}
/>
</Table.Cell>
<Table.Cell>
{/* checkbox for if the scenario is completed */}
{/* can' be checked if it's not unlocked */}
<Checkbox
onChange={() => toggleCompleted(!completed)}
checked={completed}
disabled={!unlocked}
/>
</Table.Cell>
<Table.Cell>
{scenario.name}
</Table.Cell>
</Table.Row>
);
};
return null;
});
};
function ScenarioTable() {
const [scenarioPage, changeScenarioPage] = useState(1);
const maxScenarioPage = Math.ceil(scenarios.length / 15);
const changePage = (value) => {
let newPage = scenarioPage + value;
if (newPage < 1) {
newPage = 1;
} else if (newPage > maxScenarioPage) {
newPage = maxScenarioPage;
}
changeScenarioPage(newPage);
};
return (
<div className="scenario__table">
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="5" textAlign="center">
<h1>Scenarios</h1>
</Table.HeaderCell>
</Table.Row>
<Table.Row textAlign="center">
<Table.HeaderCell>Id</Table.HeaderCell>
<Table.HeaderCell>
<Icon name="location arrow" />
</Table.HeaderCell>
<Table.HeaderCell>
<Icon name="unlock" />
</Table.HeaderCell>
<Table.HeaderCell>
<Icon name="check" />
</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{renderScenarioRows(scenarios, scenarioPage)}
</Table.Body>
<Table.Footer>
<Table.Row textAlign="center">
<Table.HeaderCell colSpan="5">
<Menu pagination>
<Menu.Item
as="a"
icon
onClick={() => changePage(-1)}
disabled={scenarioPage === 1}
>
<Icon name="chevron left" />
</Menu.Item>
<Menu.Item
as="a"
icon
onClick={() => changePage(1)}
disabled={scenarioPage === maxScenarioPage}
>
<Icon name="chevron right" />
</Menu.Item>
</Menu>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
</div>
);
}
export default ScenarioTable;

Step 6.

Finally we can browse through the Semantic UI React docs to add some styling changes and address spacing. Let’s make our table striped, unstackable, small, very compact, and give it an orange accent color. Then we give our header cells a specified width which will cause each column to be that width. And last but not least a small quality of life improvement. Our users want to be surprised by scenario names as they unlock scenarios so if the scenario is not unlocked the name is hidden by a grey bar.

//let's add some styling touches and address spacing
//and lets hide the names of scenarios that are not unlocked yet
import React, {useState} from "react";
import scenarios from "./scenarios.json";
import { Table, Icon, Checkbox, Menu } from "semantic-ui-react";
function renderScenarioRows(scenarios, scenarioPage) {
// lets diplay 15 rows of data on each page
let startingIndex = scenarioPage * 15 - 15;
let endingIndex = scenarioPage * 15 - 1;
return scenarios.map((scenario, index) => {
let [unlocked, toggleUnlocked] = useState(false);
let [completed, toggleCompleted] = useState(false);
if (index >= startingIndex && index <= endingIndex) {
return (
<Table.Row key={scenario.id} positive={completed}>
<Table.Cell textAlign="center">
{scenario.id}.
</Table.Cell>
<Table.Cell>
{scenario.coords}
</Table.Cell>
<Table.Cell>
{/* checkbox for if the scenario is unlocked */}
{/* can't be unchecked if the scenario has already been marked completed */}
<Checkbox
onChange={() => toggleUnlocked(!unlocked)}
checked={unlocked}
disabled={completed}
/>
</Table.Cell>
<Table.Cell>
{/* checkbox for if the scenario is completed */}
{/* can' be checked if it's not unlocked */}
<Checkbox
onChange={() => toggleCompleted(!completed)}
checked={completed}
disabled={!unlocked}
/>
</Table.Cell>
<Table.Cell>
{/* if this scenario is unlocked, then display it's name */}
{/* if not we're hiding it */}
{unlocked ? (
scenario.name
) : (
<p style={{ color: "transparent", backgroundColor: "lightgrey" }}>
{scenario.name}
</p>
)}
</Table.Cell>
</Table.Row>
);
};
return null;
});
};
function ScenarioTable() {
const [scenarioPage, changeScenarioPage] = useState(1);
const maxScenarioPage = Math.ceil(scenarios.length / 15);
const changePage = (value) => {
let newPage = scenarioPage + value;
if (newPage < 1) {
newPage = 1;
} else if (newPage > maxScenarioPage) {
newPage = maxScenarioPage;
}
changeScenarioPage(newPage);
};
return (
<div className="scenario__table">
<Table striped unstackable size="small" compact="very" color="orange">
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="5" textAlign="center">
<h1>Scenarios</h1>
</Table.HeaderCell>
</Table.Row>
<Table.Row textAlign="center">
<Table.HeaderCell width="2">Id</Table.HeaderCell>
<Table.HeaderCell width="2">
<Icon name="location arrow" />
</Table.HeaderCell>
<Table.HeaderCell width="1">
<Icon name="unlock" />
</Table.HeaderCell>
<Table.HeaderCell width="1">
<Icon name="check" />
</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{renderScenarioRows(scenarios, scenarioPage)}
</Table.Body>
<Table.Footer>
<Table.Row textAlign="center">
<Table.HeaderCell colSpan="5">
<Menu pagination>
<Menu.Item
as="a"
icon
onClick={() => changePage(-1)}
disabled={scenarioPage === 1}
>
<Icon name="chevron left" />
</Menu.Item>
<Menu.Item
as="a"
icon
onClick={() => changePage(1)}
disabled={scenarioPage === maxScenarioPage}
>
<Icon name="chevron right" />
</Menu.Item>
</Menu>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
</div>
);
}
export default ScenarioTable;

And there you have it! A react functional table component with Semantic UI React. Check it out the final component on codesandbox. I hope seeing the steps of this component being built helped you get a better understanding of Semantic UI React. Have you built any other fun or cool tables in Semantic UI React? Hit me up and let me see them!

mkirby

I'm a perpetual learner, constant explorer and a Full stack web developer with 10 years experience leading fast paced entertainment industry productions. I enjoy building things and the process of discovering how to create them.