In this post, we will discuss how to create composable and complex components in React, using a custom table component as an example. Composability allows you to create flexible and reusable components that can be easily adapted and extended.
We will also explore how to effectively manage global state using the modern and accessible Zustand library. Lest's go!
The TaskLet's assume we have been tasked with creating a table component for displaying order data. In the BadOrderList
component, I have prepared an example of a poorly designed table component that presents order data.
Setting Up Mocks and Fetch Data MethodsTo begin with, let's create two methods that will help us work with data that will seemingly be fetched from a backend. These methods will simulate data retrieval and manipulation, providing a realistic context for our React component development.
Creating Mock DataThe first method I will create is for generating a mockup of 100 order items in an array. This method will simulate a dataset that you might typically receive from a backend service.
export const sampleStatuses = [
'Processing',
'Shipped',
'Delivered',
'Cancelled',
] as const;
export const generateMockOrders = (numberOfOrders: number) => {
const mockOrders = [];
const sampleCustomers = ['Alice', 'Bob', 'Charlie', 'Diana'];
const sampleItems = ['Item A', 'Item B', 'Item C'];
for (let i = 1; i <= numberOfOrders; i++) {
const order = {
id: i,
customer:
sampleCustomers[Math.floor(Math.random() * sampleCustomers.length)],
orderDate: new Date(
2024,
Math.floor(Math.random() * 12),
Math.floor(Math.random() * 28)
).toLocaleDateString(),
item: sampleItems[Math.floor(Math.random() * sampleItems.length)],
quantity: Math.floor(Math.random() * 5) + 1,
price: Math.floor(Math.random() * 100) + 10,
status: sampleStatuses[Math.floor(Math.random() * sampleStatuses.length)],
};
mockOrders.push(order);
}
return mockOrders;
};
// Generate a large set of mock orders
export const ordersMockData = generateMockOrders(100);
generate-mock-data
Implementing Fetch Data MethodsThe second method is a function for fetching the generated data. We will use it to retrieve data. This method simulates an API call to fetch order data, which is crucial for demonstrating how your React component interacts with asynchronous data sources, handles state changes, and updates the UI in response to data retrieval.
It encapsulates the core functionality of data fetching including filtering, searching, and pagination, which are common requirements in modern web applications.
- The
limit
parameter determines the maximum number of items to be returned in a single fetch. It's used for pagination and controls the size of each page of data. - The
offset
parameter specifies the starting point for fetching the data. In pagination, it's used to calculate the starting point of a particular page. For example, with a limit of 10, an offset of 20 would start fetching from the 21st item. - The
search
parameter allows filtering the data based on specific criteria. It can include filters like status
or item
. The function uses these filters to return only the data that matches the specified search criteria.
import { ordersMockData, sampleStatuses } from '../mocks/orders';
import { OrderItem } from '../types/orders-type';
type OrderStatus = typeof sampleStatuses[number];
type FetchOrdersProps = {
limit?: number;
offset?: number;
search?: {
status?: OrderStatus;
item?: string;
};
};
export type FetchedData = {
data: OrderItem[];
total: number;
limit: number;
offset: number;
};
export const fetchOrders = ({
limit = 10,
offset = 0,
search,
}: FetchOrdersProps): Promise<FetchedData> => {
const filteredOrders = ordersMockData.filter(
(order) =>
(search?.item
? order.item.toLowerCase().includes(search.item.toLowerCase())
: true) && (search?.status ? order.status === search.status : true)
);
const paginatedOrders = filteredOrders.slice(offset, offset + limit);
return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: paginatedOrders,
total: filteredOrders.length,
limit,
offset,
});
}, 1000);
});
};
order-fetch-method
Showcasing a Code of poorly designed componentIn our application, we will style our components using Tailwind CSS, a utility-first CSS framework. Tailwind simplifies the process of designing custom and responsive layouts. For instructions on how to install and integrate Tailwind CSS into your project, you can refer to their official installation guide here: Tailwind CSS Installation Guide. This guide provides comprehensive steps for setting up Tailwind CSS in various development environments.
Let's create a bad designed component presents list of and orders
import { useEffect, useState } from 'react';
import { fetchOrders } from '../../lib/fetchOrders';
import { sampleStatuses } from '../../mocks/orders';
type OrderItem = {
customer: string;
id: number;
item: string;
orderDate: string;
price: number;
quantity: number;
status: string;
};
type BadOrderListProps = {
limit?: number;
};
export const BadOrderList = ({ limit = 10 }: BadOrderListProps) => {
const [orders, setOrders] = useState<OrderItem[]>([]);
const [page, setPage] = useState(0);
const [total, setTotal] = useState();
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState({});
useEffect(() => {
setLoading(true);
fetchOrders({ limit, offset: page * limit, search }).then((res) => {
setOrders(res.data);
setTotal(res.total);
setLoading(false);
});
}, [page, search]);
const handleSearchChange = (e) => {
setSearch({ ...search, [e.target.name]: e.target.value });
};
const nextPage = () => setPage((prev) => prev + 1);
const prevPage = () => setPage((prev) => (prev > 0 ? prev - 1 : 0));
return (
<div className="px-8 m-auto">
<h2 className="py-8 text-3xl text-gray-900 dark:text-white">
Wrong List
</h2>
<div className="filters mb-8 flex gap-6">
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Search by items
</label>
<input
type="text"
name="item"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
value={search.item}
onChange={handleSearchChange}
/>
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Select an option
</label>
<select
name="status"
value={search.status}
onChange={handleSearchChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
>
<option value={''}>Choose a status</option>
{sampleStatuses.map((status) => {
return (
<option value={status} key={status}>
{status}
</option>
);
})}
</select>
</div>
</div>
<div className="relative overflow-x-auto">
<table className="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-6 py-3">
ID
</th>
<th scope="col" className="px-6 py-3">
Customer
</th>
<th scope="col" className="px-6 py-3">
Item
</th>
<th scope="col" className="px-6 py-3">
Order Date
</th>
<th scope="col" className="px-6 py-3">
Price
</th>
<th scope="col" className="px-6 py-3">
Status
</th>
</tr>
</thead>
<tbody>
{loading
? [...Array(limit)].map((_, rowIndex) => {
return (
<tr
key={rowIndex}
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
>
{[...Array(6)].map((_, columnIndex) => {
return (
<td
key={columnIndex}
scope="row"
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
<div className="h-4 bg-gray-200 rounded-full dark:bg-gray-700 w-auto mb-1"></div>
</td>
);
})}
</tr>
);
})
: orders.map((order) => {
return (
<tr
key={order.id}
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600"
>
<th
scope="row"
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
{order.id}
</th>
<td className="px-6 py-4">{order.customer}</td>
<td className="px-6 py-4">{order.item}</td>
<td className="px-6 py-4"> {order.orderDate}</td>
<td className="px-6 py-4">${order.price}</td>
<td className="px-6 py-4">{order.status}</td>
</tr>
);
})}
</tbody>
</table>
<div className="flex flex-col items-center mt-8">
<span className="text-sm text-gray-700 dark:text-gray-400">
Showing{' '}
<span className="font-semibold text-gray-900 dark:text-white">
{page + 1}
</span>{' '}
to{' '}
<span className="font-semibold text-gray-900 dark:text-white">
{limit}
</span>{' '}
of{' '}
<span className="font-semibold text-gray-900 dark:text-white">
{total}
</span>{' '}
Entries
</span>
<div className="inline-flex mt-2 xs:mt-0">
<button
onClick={prevPage}
className="flex items-center justify-center px-3 h-8 text-sm font-medium text-white bg-gray-800 rounded-s hover:bg-gray-900 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
<svg
className="w-3.5 h-3.5 me-2 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 5H1m0 0 4 4M1 5l4-4"
/>
</svg>
Prev
</button>
<button
onClick={nextPage}
className="flex items-center justify-center px-3 h-8 text-sm font-medium text-white bg-gray-800 border-0 border-s border-gray-700 rounded-e hover:bg-gray-900 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
Next
<svg
className="w-3.5 h-3.5 ms-2 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M1 5h12m0 0L9 1m4 4L9 9"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
);
};
bad-list-component
At first glance, it might appear that this component functions correctly. However, upon closer inspection, it reveals several technical shortcomings.
let's enumerate the errors commonly found in such a component:
Problem 1. - Lack of Component Flexibility and ReusabilityFirst and foremost, the BadOrderList
component suffers from a lack of flexibility. The hardcoded limit for pagination and the tightly coupled search functionality within the component make it less reusable. In an ideal scenario, components should be designed to be as generic and reusable as possible, allowing them to adapt to various contexts and data types.
Problem 2. - Inefficient State ManagementThe component's state management also leaves much to be desired. The states orders
, page
, total
, loading
, and search
are all managed within the component, leading to a bloated and less manageable codebase. This design fails to leverage the full potential of React's compositional model, where state management could be externalized or handled more elegantly using context or Redux-like patterns.
Problem 3. - Overcomplication in UI RenderingThe rendering logic, particularly for the loading skeletons and pagination, is more complex than necessary. This complexity can introduce bugs and makes the code harder to understand and maintain. A more streamlined approach, possibly using custom hooks or separate components, could greatly simplify the rendering logic.
Problem 4. - Poor Separation of ConcernsThe component tries to handle too many tasks: fetching data, managing state, rendering UI, and handling user interactions. This lack of separation of concerns can lead to difficulties in debugging and testing the component. Ideally, these concerns should be separated into different components or hooks, following the Single Responsibility Principle.
Problem 5. - Hardcoded Styling and Lack of ThemingThe direct use of Tailwind CSS classes within the component restricts the ability to theme or dynamically style the component. A better approach would involve abstracting the styling, allowing for more flexibility and adaptability to different design systems.
Refined Approach: Enhancing Component Design in ReactAs described earlier, our Order List component needs improvement. Let's start by refining our approach, beginning with the definition of our state.
The main objective is to create a table component that is easily reusable. A great solution is to store the state of this component in a global store. There are several reasons why storing table state in a global store can be advantageous:
- Centralized Management: Storing the state in a global store centralizes the management of the table's state. This is particularly beneficial in larger applications where multiple components might need access to the same state.
- State Consistency: It ensures consistency across the application. When state is managed globally, any changes are immediately reflected across all components that use that state.
- Reusability and Scalability: With state in a global store, creating reusable table components becomes more manageable. You can easily instantiate multiple tables with different states without worrying about state conflicts.
Create Table storeFor creating the store, I install zustand library:
Next, let's define the state for our table.
The key element here is to identify each table with a specific ID, which will allow us to manage its state distinctly within the global state. This approach is crucial for implementing functionalities like pagination, filtering, and other interactions specific to each table. By assigning a unique ID to each table, we can ensure that their data, current page, search filters, and other relevant states are managed independently and efficiently.
Here's a code of our state management setup using the Zustand library:
import { create } from 'zustand';
export type TableState = {
page: number;
limit: number;
search: Record<string, any>;
};
type TableStoreState = {
tables: Record<string, TableState>;
};
type TableStoreActions = {
initTable: (tableId: string, initialState?: TableState) => void;
setPage: (tableId: string, page: number) => void;
setLimit: (tableId: string, limit: number) => void;
setSearch: (tableId: string, search: Record<string, any>) => void;
};
export const useTableStore = create<TableStoreState & TableStoreActions>(
(set) => ({
tables: {},
// Initialize or update a table's state
initTable: (tableId, initialState = { page: 0, limit: 10, search: {} }) =>
set((state) => ({
tables: {
...state?.tables,
[tableId]: state.tables[tableId]
? { ...state.tables[tableId], ...initialState }
: initialState,
},
})),
// Update page for a specific table
setPage: (tableId, page) =>
set((state) => ({
tables: {
...state.tables,
[tableId]: { ...state.tables[tableId], page },
},
})),
// Update limit for a specific table
setLimit: (tableId, limit) =>
set((state) => ({
tables: {
...state.tables,
[tableId]: { ...state.tables[tableId], limit },
},
})),
// Update search for a specific table
setSearch: (tableId, search) =>
set((state) => ({
tables: {
...state.tables,
[tableId]: { ...state.tables[tableId], search },
},
})),
})
);
table-store
In this setup, we're leveraging Zustand to create a global store (useTableStore
) that holds the state for multiple tables. Each table is identified by a unique tableId
, ensuring that operations such as setting the current page (setPage
), updating the limit of rows per page (setLimit
), or changing search filters (setSearch
) affect only the intended table. This method provides a modular and easily maintainable way to handle state in complex applications where multiple instances of the same component type need to operate independently.
Create Table componentNow that we have our state established, we can move on to creating our table component.
type TableProps = React.HTMLAttributes<HTMLTableElement> & {
children: React.ReactNode;
};
export const Table = ({ children, ...props }: TableProps) => {
return <table {...props}>{children}</table>;
};
type THeadProps = React.HTMLAttributes<HTMLTableSectionElement> & {
renderHeader: () => React.ReactNode;
};
export const THead = ({ renderHeader, ...props }: THeadProps) => {
return <thead {...props}>{renderHeader()}</thead>;
};
Table.THead = THead;
type TBody<T> = {
loading?: boolean;
data?: T[];
renderRow: (item: T, index: number) => React.ReactNode;
limit: number;
};
export const TBody = <T extends unknown>({
loading,
data,
renderRow,
limit,
}: TBody<T>) => {
const rows = data?.map((item, index) => renderRow(item, index));
return <tbody>{loading ? <SkeletonRow limit={limit} /> : rows}</tbody>;
};
Table.TBody = TBody;
type SkeletonRowProps = {
limit: number;
};
const SkeletonRow = ({ limit }: SkeletonRowProps) => {
return (
<>
{[...Array(limit)].map((_, rowIndex) => (
<tr
key={rowIndex}
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
>
{[...Array(6)].map((_, columnIndex) => (
<td
key={columnIndex}
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
<div className="h-4 bg-gray-200 rounded-full dark:bg-gray-700 w-auto mb-1"></div>
</td>
))}
</tr>
))}
</>
);
};
common-table-component
The provided code defines a flexible table component in React. The Table
component is a functional component that receives children
and additional HTML attributes, rendering a standard HTML table element. It's designed to be highly reusable and customizable through props.
Table
Component: Acts as a container for the table. It accepts any HTML attributes and children, making it highly versatile.THead
Component: Handles the rendering of the table header. It takes a renderHeader
function as a prop, which returns the header nodes, providing flexibility in header content.TBody
Component: Deals with the table body. It can handle loading states and dynamic data rendering. The renderRow
function allows for custom rendering of each row, based on the data provided.SkeletonRow
: A placeholder component that is displayed during data loading. It enhances user experience by indicating data is being fetched.
This design promotes reusability and separation of concerns, making the table component adaptable for various data types and UI requirements. The use of render props (renderHeader
and renderRow
) adds to its flexibility, allowing custom rendering as per the specific needs of the data being displayed.
Create Pagination componentNow that we have our table component, let's write a pagination component that will enable efficient navigation through the table pages. The Pagination
component uses the useTableStore
for managing the current page state. It provides buttons for navigating to the previous and next pages and displays the current page and total entries. The prevPage
and nextPage
functions adjust the current page within the bounds of available data. This component enhances the user experience by allowing easy navigation and a clear view of the data's scope within the table.
import { useTableStore } from '../../store/table.store';
type PaginationProps = {
page: number;
initId: string;
limit: number;
total: number;
};
export const Pagination = ({ page, initId, limit, total }: PaginationProps) => {
const { setPage } = useTableStore();
const prevPage = () => {
setPage(initId, Math.max(0, page - 1));
};
const nextPage = () => {
setPage(initId, page + 1);
};
return (
<div className="flex flex-col items-center mt-8">
<span className="text-sm text-gray-700 dark:text-gray-400">
Showing{' '}
<span className="font-semibold text-gray-900 dark:text-white">
{page + 1}
</span>{' '}
to{' '}
<span className="font-semibold text-gray-900 dark:text-white">
{limit}
</span>{' '}
of{' '}
<span className="font-semibold text-gray-900 dark:text-white">
{total}
</span>{' '}
Entries
</span>
<div className="inline-flex mt-2 xs:mt-0">
<button
onClick={prevPage}
className="flex items-center justify-center px-3 h-8 text-sm font-medium text-white bg-gray-800 rounded-s hover:bg-gray-900 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
<svg
className="w-3.5 h-3.5 me-2 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 5H1m0 0 4 4M1 5l4-4"
/>
</svg>
Prev
</button>
<button
onClick={nextPage}
className="flex items-center justify-center px-3 h-8 text-sm font-medium text-white bg-gray-800 border-0 border-s border-gray-700 rounded-e hover:bg-gray-900 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
Next
<svg
className="w-3.5 h-3.5 ms-2 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M1 5h12m0 0L9 1m4 4L9 9"
/>
</svg>
</button>
</div>
</div>
);
};
Pagination
Create Filter componentNext, we will aim to replicate the filters but move them into a separate component. This step involves extracting the filtering logic and UI elements from the current setup and encapsulating them into an independent, reusable component.
import { sampleStatuses } from '../../mocks/orders';
type OrderListFiltersPRops = {
handleSearchChange: (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => void;
search: {
item?: string;
status?: string;
};
};
export const OrderListFilters = ({
handleSearchChange,
search,
}: OrderListFiltersPRops) => {
return (
<div className="filters mb-8 flex gap-6">
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Search by items
</label>
<input
type="text"
name="item"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
value={search.item}
onChange={handleSearchChange}
/>
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Select an option
</label>
<select
name="status"
value={search.status}
onChange={handleSearchChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
>
<option value={''}>Choose a status</option>
{sampleStatuses.map((status) => {
return (
<option value={status} key={status}>
{status}
</option>
);
})}
</select>
</div>
</div>
);
};
order-list-filters
The OrderListFilters
component is a well-structured approach to filtering data in a table. Here’s a breakdown of its key elements and why this approach is effective:
- Search Input: Allows users to filter the table by item names. The input field is bound to the
search.item
state, ensuring that the search term is kept in sync with the state. - Status Dropdown: Provides a way to filter the table by order status. The
select
element is populated with options based on sampleStatuses
and is also bound to the search.status
state. - Event Handling (
handleSearchChange
): Both the input and select elements use the same handleSearchChange
function to update the search state. This function is designed to handle changes for different types of inputs, making it flexible and reusable. - Modular and Reusable: This component is an example of a modular and reusable UI element, capable of being used in various parts of the application wherever filtering functionality is needed.
Overall, this component demonstrates clean coding practices, effective state management, and a user-friendly interface, making it a solid choice for handling table filters in a React application.
Create Custom HookCreating the useOrders
hook is an important next step, as it abstracts and centralizes the logic for fetching order data.
We'll use useSWR
to enhance the data fetching process. An important aspect to highlight is that using useSWR
in React helps significantly with caching requests. useSWR
is a powerful hook that automatically handles caching of requests, meaning that it stores the results of API calls and reuses them when the same request is made again. This caching mechanism optimizes performance by reducing the number of API calls, leading to faster load times and a more efficient use of network resources. It's particularly beneficial in scenarios where data changes infrequently or when you want to maintain a seamless user experience by instantly showing previously fetched data.
Let's break down the key elements of this hook:
- useSWR Integration:
useSWR
is a powerful hook for data fetching. It handles caching, revalidation, focus tracking, and error retrying out of the box. - Dynamic Key Generation: The hook generates a unique key based on the
page
, limit
, and search
parameters. This key ensures that the SWR hook fetches and revalidates data only when these parameters change, leading to efficient data fetching and caching. - Fetch Function: The hook calls
fetchOrders
, a function to fetch data from a mock backend. This function simulates the behavior of a real API call, including filtering and pagination based on the provided parameters. - useEffect for Initialization: The
useEffect
hook is used to initialize the table state with default values when the component mounts. It ensures the table starts with the correct initial state (page
, limit
, and search
), enhancing the user experience right from the first render. - Return Value: The hook returns the data, error, and loading states managed by
useSWR
, providing a simple interface for components to render data, show loading indicators, or handle errors.
In essence, this custom hook using useSWR
streamlines the process of fetching, caching, and managing the state of your order data. It abstracts complex functionalities into a more manageable and reusable format, significantly improving the efficiency of data handling in your React application.
import useSWR from 'swr';
import { FetchedData, fetchOrders } from '../lib/fetchOrders';
import { useTableStore } from '../store/table.store';
import { useEffect } from 'react';
type UseOrderProps = {
initId: string;
limit: number;
page: number;
search: {
item?: string;
status?: string;
};
};
export const useOrders = ({ initId, limit, page, search }: UseOrderProps) => {
const { initTable } = useTableStore();
useEffect(() => {
initTable(initId, { page: 0, limit, search: {} });
}, [initId, limit, initTable]);
return useSWR<FetchedData>(
() => (page != null ? { limit, offset: page * limit, search } : null),
fetchOrders
);
};
useOrders
This approach has several benefits:
- Separation of Concerns: By moving data-fetching logic into a custom hook, you keep your component code clean and focused on rendering and user interaction. This makes your components easier to understand and maintain.
- Reusability: The hook can be reused in different components that need to fetch order data, reducing code duplication.
- Enhanced Readability: Centralizing logic in a hook improves readability. Other developers can easily understand what the hook does, making it easier to work with the codebase.
- Simplified State Management
Create the Order List component: let's bring all togetherLet's bring everything together! We will place all the previously defined components into our parent OrderList
component and pass the appropriate props to each. This integration ensures that the components work in unison, enabling a cohesive and functional user interface. By connecting the components such as OrderListFilters
, Table
, and Pagination
with their respective props, we create a comprehensive and interactive table display that is both user-friendly and efficient in handling data.
import { useTableStore } from '../../store/table.store';
import { Table } from '../common/table';
import { OrderItem } from '../../types/orders-type';
import { Pagination } from '../common/pagination';
import { OrderListFilters } from './order-list-filters';
import { useOrders } from '../../hooks/useOrders';
export const OrderList = ({ limit = 10 }) => {
const initId = 'order-list';
const { tables, setSearch } = useTableStore();
const tableState = tables[initId];
const { page, search } = tableState || { page: 0, search: {} };
const { data, error, isLoading } = useOrders({ initId, limit, page, search });
const handleSearchChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
setSearch(initId, { ...search, [e.target.name]: e.target.value });
};
return (
<div className="px-8 m-auto">
<h2 className="py-8 text-3xl text-gray-900 dark:text-white">
Good example
</h2>
{error && (
<div className="error-message">
<p>Failed to load orders. Please try again later.</p>
</div>
)}
{!error && (
<>
<OrderListFilters
handleSearchChange={handleSearchChange}
search={search}
/>
<Table className="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<Table.THead
renderHeader={renderHeader}
className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"
/>
<Table.TBody
loading={isLoading}
data={data?.data || []}
renderRow={renderRow}
limit={limit}
/>
</Table>
<Pagination
page={page}
limit={limit}
initId={initId}
total={data?.total || 0}
/>
</>
)}
</div>
);
};
const renderHeader = () => (
<tr>
<th scope="col" className="px-6 py-3">
ID
</th>
<th scope="col" className="px-6 py-3">
Customer
</th>
<th scope="col" className="px-6 py-3">
Item
</th>
<th scope="col" className="px-6 py-3">
Order Date
</th>
<th scope="col" className="px-6 py-3">
Price
</th>
<th scope="col" className="px-6 py-3">
Status
</th>
</tr>
);
const renderRow = (order: OrderItem, index: number) => {
return (
<tr
key={index}
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600"
>
<th
scope="row"
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
{order.id}
</th>
<td className="px-6 py-4">{order.customer}</td>
<td className="px-6 py-4">{order.item}</td>
<td className="px-6 py-4"> {order.orderDate}</td>
<td className="px-6 py-4">${order.price}</td>
<td className="px-6 py-4">{order.status}</td>
</tr>
);
};
order-list
The OrderList
component we've created is a great example of a comprehensive and well-structured React component. It effectively brings together various elements:
- Integration with
useTableStore
: Manages the state of the table, such as the current page and search parameters. - Error Handling: Displays an error message if the data fetching encounters an issue, enhancing user experience by informing them of any problems.
- Filters with
OrderListFilters
: Allows users to filter the table based on specific criteria like item and status. - Data Fetching with
useOrders
: Handles the retrieval of order data, including pagination and search functionality. - Table Display with
Table
Component: Utilizes a flexible table component for displaying data. It's composed of THead
for headers and TBody
for body content, both of which are customizable. - Pagination Control with
Pagination
Component: Enables users to navigate through different pages of data. - Rendering Functions (
renderHeader
and renderRow
): Define how each row and header in the table is rendered, making the table adaptable to different data structures.
This component showcases a cohesive and modular approach to building complex UIs in React, with clear separation of concerns and reusable components.
StackBlitz link to the projectTo conclude, I'm sharing a link to the example I've talked about. You can view how all the files are organized across folders and see the implemented components in action at this StackBlitz link: View Example on StackBlitz. This will give you a practical insight into the structure and integration of the components in a real-world scenario.
It's been a pleasure assisting you with your React components and sharing insights on effective coding practices. If you have more questions in the future or need further guidance, feel free to reach out. Happy coding, and best of luck with your project! Goodbye for now!