React and Node.js CMS Series: Implementing Post Lists with Advanced Table Features

If you will need a source code for this tutorial you can get it here. Following our previous article 'Building a React CMS: Fonts, SCSS Resets, and Layout Implementation' where we established our core layout structure, we're ready to enhance our Posts page with dynamic functionality. In this tutorial, we'll transform the static Posts page into an interactive data management interface. We'll build a feature-rich posts table in React and develop the corresponding Node.js backend endpoints to serve our post data efficiently. Okay, let's do it: 1. Step-by-Step: Building and Implementing Posts Table from Scratch. create a new "Table.component.jsx" file inside the "components" folder, with the "TableComponent" function that will return JSX; create "table" tag with 5 headers: post, status, language, created at, action; continue with "table body", each table row will render a small post image and post title, language, post created date, and action (small menu with the list of additional functionality like edit, change status, or preview...); implement a "TablePagination" component from MUI; const TableComponent = () => { return ( POST STATUS LANGUAGE CREATED AT ACTION {data.map((item, index) => { let imageUrl = import.meta.env.VITE_API_BASE_URL + `/posts/image/` + item.mainImage.file.name; return ( {item.title} {item.subTitle} { item?.status === 'online' ? {item?.status} : {item?.status} } {item.language} {item.created.date.day} / {item.created.date.month} / {item.created.date.year + ' ' + item.created.date.time} handleClick(event, item)} > Preview Edit { selectedItem?.status === 'online' ? 'Deactivate' : 'Activate' } Remove ); })} ); }; export default TableComponent; Something like this, unfortunately, I can not paste all styles but you can find them in the repo. We have a table but with no data, we need to configure data fetching functionality to get data from the server, that is what we will do next. import "useEffect", and "useState" hooks from "React", and main hooks from "Redux" (we will store posts in our Redux Store), also we will use our "Notification" module (we need to import actions too), and MUI components; import React, { useState, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { aPushNewNotification } from '../../../../../store/base/base.action'; import { aSetPostsList } from '../../../../../store/posts/post.action'; import { getPostsList } from '../../../../../http/services/posts.services'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import Button from '@mui/material/Button'; import SettingsIcon from '@mui/icons-material/Settings'; import TablePagination from '@mui/material/TablePagination'; create new "posts" store structure with "SET_POSTS" type, "sPostsList" selector, "aSetPostsList" action, and "posts" reducer; import POST_ACTION_TYPES from './post.types'; export const POST_INITIAL_STATE = { postsList: [], }; export const postReducer = (state = POST_INITIAL_STATE, action = {}) => { const { type, payload } = action; switch (type) { case POST_ACTION_TYPES.SET_POSTS_LIST: ret

May 7, 2025 - 11:37
 0
React and Node.js CMS Series: Implementing Post Lists with Advanced Table Features

If you will need a source code for this tutorial you can get it here.

Following our previous article 'Building a React CMS: Fonts, SCSS Resets, and Layout Implementation' where we established our core layout structure, we're ready to enhance our Posts page with dynamic functionality. In this tutorial, we'll transform the static Posts page into an interactive data management interface. We'll build a feature-rich posts table in React and develop the corresponding Node.js backend endpoints to serve our post data efficiently. Okay, let's do it:

1. Step-by-Step: Building and Implementing Posts Table from Scratch.

  • create a new "Table.component.jsx" file inside the "components" folder, with the "TableComponent" function that will return JSX;

  • create "table" tag with 5 headers: post, status, language, created at, action;

  • continue with "table body", each table row will render a small post image and post title, language, post created date, and action (small menu with the list of additional functionality like edit, change status, or preview...);

  • implement a "TablePagination" component from MUI;

const TableComponent = () => {
return (
        <div className="table-container">
            <table className="custom-table">
                <thead>
                    <tr>
                        <th>POST</th>
                        <th>STATUS</th>
                        <th>LANGUAGE</th>
                        <th>CREATED AT</th>
                        <th>ACTION</th>
                    </tr>
                </thead>
                <tbody>
                {data.map((item, index) => {
                    let imageUrl = import.meta.env.VITE_API_BASE_URL + `/posts/image/` + item.mainImage.file.name;
                    return (
                        <tr key={index}>
                            <td className="PostTableItem-cell">
                                <img src={imageUrl} alt={item.mainImage.alt} className="PostTableItem-image" />
                                <div className="PostTableItem-details">
                                    <p className="PostTableItem-title">{item.title}</p>
                                    <p className="PostTableItem-subTitle">{item.subTitle}</p>
                                </div>
                            </td>
                            <td className="status-cell">
                                {
                                    item?.status === 'online'
                                        ? <span className="status-cell-online">{item?.status}</span>
                                        : <span className="status-cell-offline">{item?.status}</span>
                                }
                            </td>
                            <td className="language-cell">{item.language}</td>
                            <td className="created-cell">
                                <span>
                                    {item.created.date.day}
                                    /
                                    {item.created.date.month}
                                    /
                                    {item.created.date.year + ' ' + item.created.date.time}
                                </span>
                            </td>
                            <td className="action-cell">
                                <Button
                                    id="basic-button"
                                    aria-controls={open ? 'basic-menu' : undefined}
                                    aria-haspopup="true"
                                    aria-expanded={open ? 'true' : undefined}
                                    onClick={(event) => handleClick(event, item)}
                                >
                                    <SettingsIcon />
                                </Button>
                                <Menu
                                    id="basic-menu"
                                    anchorEl={anchorEl}
                                    open={open}
                                    onClose={handleClose}
                                    MenuListProps={{
                                    'aria-labelledby': 'basic-button',
                                    }}
                                >
                                    <MenuItem >Preview</MenuItem>
                                    <MenuItem >Edit</MenuItem>
                                    <MenuItem >{
                                        selectedItem?.status === 'online'
                                            ? 'Deactivate'
                                            : 'Activate'
                                    }</MenuItem>
                                    <MenuItem >Remove</MenuItem>
                                </Menu>
                            </td>
                        </tr>
                    );
                })}
                </tbody>
            </table>
            <TablePagination
                className="table-container--pagination"
                component="div"
            />
        </div>
    );
};

export default TableComponent;

Something like this, unfortunately, I can not paste all styles but you can find them in the repo.

We have a table but with no data, we need to configure data fetching functionality to get data from the server, that is what we will do next.

  • import "useEffect", and "useState" hooks from "React", and main hooks from "Redux" (we will store posts in our Redux Store), also we will use our "Notification" module (we need to import actions too), and MUI components;
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { aPushNewNotification } from '../../../../../store/base/base.action';
import { aSetPostsList } from '../../../../../store/posts/post.action';
import { getPostsList } from '../../../../../http/services/posts.services';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Button from '@mui/material/Button';
import SettingsIcon from '@mui/icons-material/Settings';
import TablePagination from '@mui/material/TablePagination';
  • create new "posts" store structure with "SET_POSTS" type, "sPostsList" selector, "aSetPostsList" action, and "posts" reducer;
import POST_ACTION_TYPES from './post.types';

export const POST_INITIAL_STATE = {
    postsList: [],
};
export const postReducer = (state = POST_INITIAL_STATE, action = {}) => {
    const { type, payload } = action;
    switch (type) {
        case POST_ACTION_TYPES.SET_POSTS_LIST:
            return { ...state, postsList: payload };
        default:
            return state;
    }
};
  • inside the "services" folder, in our new "posts.services.js" file we will add the "getPostsList" function;
export const getPostsList = (data) => {
    return HTTP.get(`/posts`, data).then(({ data }) => data);
};
  • after we prepared all the structure we can start fetching data. Add a new "fetchData" function inside the "Table" component, and call this function in the "useEffect" hook, after the page is created. The "fetchData" function after receiving data will save the "posts" list into the storage;
const data = useSelector(sPostsList);
const fetchData = async () => {
    try {
        const response = await getPostsList();
        dispatch(aSetPostsList(response.posts));
    } catch (error) {
        console.error("Error fetching Posts:", error);
        dispatch(aPushNewNotification({ type: 'error', text: 'Failed to fetch Posts' }));
    }
};
useEffect(() => {
    fetchData();
}, []);

You can check that we also use "data" to render our table. It will not work now correctly until we configure the backend part (soon).

  • import our "Table" component into the newly created "PostsTable.component.jsx" file, a new functional component that will wrap our Table in an additional "div", for design purposes;
import TableComponent from './table-component/Table.component';
const PostsTable = () => {
    return (
        <div className="posts-table">
            <h2 className="posts-table__title">Posts</h2>
            <div className="posts-table__content">
                <TableComponent />
            </div>
        </div>
    );
};
export default PostsTable;
  • create a new "PostsAction.component.jsx" file, that will render the "create" and "filters" buttons, and probably the "search" field;
const PostsAction = () => {
    return (
        <div className="posts-action">
            <div className="posts-action__buttons">
                <button className="posts-action__buttons--item">
                    <AddIcon />
                </button>
                <button className="posts-action__buttons--item">
                    <FilterAltIcon />
                </button>
            </div>
            <div className="posts-action__search">
            <TextField
                label="Search"
                id="outlined-size-small"
                size="small"
                />
                <button className="posts-action__search--button">
                    <SearchIcon />
                </button>
            </div>
        </div>
    );
}
export default PostsAction;
  • the last step, is to modify our "Posts" page and import the list and action components;

Great, we can move to the backend part finally.

2. Prepare the Server Structure to Return Posts List.

Before working with the posts module, we need to update our server structure.

  • create a new "api.js" file inside the "routes" folder, which will be general API storage for different routes;
const express = require('express');
const usersRouter = require('./users/users.router');
const postsRouter = require('./posts/posts.router');
const api = express.Router();

api.use('/api/users', usersRouter);
api.use('/api/posts', postsRouter);

module.exports = api;
  • apply our API middleware function into our express server, inside the "app.js" file;
app.use(api);
  • create a new "posts" folder inside our routes and add "router" and "controller" files. Import "express" and "posts.controller" and define first our "get" route;
const express = require('express');
const postsController = require('./posts.controller');
const postsRouter = express.Router();

postsRouter.get('/', postsController.getPostsList);
module.exports = postsRouter;
  • configure a new "getPostsList" controller, that will use the "posts" model and try to get data from the database;
async function getPostsList(req, res) {
    try {
        const data = await postsModel.getPostsList();
        return res.status(200).json({
            status: 200,
            ...data
        });
    } catch (error) {
        console.error('Error getting posts list:', error);
        return res.status(500).json({
            status: 500,
            message: 'Internal server error'
        });
    }
}
  • now we should create a model file, that will get data from the "MongoDB" and return it to the client;
async function getPostsList() {
    try {
        const postsList = await posts.find();
        return { posts: postsList };
    } catch (error) {
        console.error('Error getting posts list:', error);
        throw error;
    }
}

But we do not have a post creation functionality yet, so we can simply return some hardcoded list of posts now, and that would be fine for us.

Fantastic, we need to reload the app and check the results on our "Posts" page.

Posts List Table

In this tutorial, we've transformed our CMS Posts page from a static interface to a dynamic, feature-rich data management system using React and Node.js. We've successfully implemented a comprehensive posts table that not only displays data efficiently but also sets the groundwork for advanced functionality like sorting, filtering, and pagination.

Remember that building a robust CMS is an iterative process. Each feature you add should focus on improving user experience and making content management more efficient and intuitive. The skills and patterns demonstrated in this tutorial are transferable to many types of web applications, providing you with valuable insights into modern web development practices. Stay tuned, we will continue soon.

If you need a source code for this tutorial you can get it here.

Found this post useful? ☕ A coffee-sized contribution goes a long way in keeping me inspired! Thank you)
Buy Me A Coffee

Next step: "Content Management System: Building a Post Creation System from Scratch with Node js"