Skip to main content

sort-handler Utility

sort-handler adalah specialized utility untuk menghandle sorting operations dengan support untuk Mantine DataTable DataTableSortStatus dan custom SortCondition objects. Utility ini menyediakan consistent format untuk sort parameters dengan optional URL encoding.

📋 Overview

🎯 Fungsionalitas Utama

  • Dual Format Support: Support untuk Mantine DataTable dan custom sort conditions
  • Consistent Formatting: Standard format output: "key,direction"
  • URL Encoding: Optional URL encoding untuk API integration
  • Type Safety: Strong TypeScript typing untuk sort conditions
  • Factory Pattern: Configurable handler instances
  • Null Handling: Graceful handling untuk null/undefined sort states

🔄 Supported Sort Formats

🚀 Quick Start

Basic Usage

import { createSortHandler } from '@/utils/sort-handler';
import type { DataTableSortStatus } from 'mantine-datatable';

// Create sort handler instance
const sortHandler = createSortHandler({
encodeSort: true
});

// Process Mantine DataTable sort
const dataTableSort: DataTableSortStatus = {
columnAccessor: 'name',
direction: 'asc'
};

const sortString = sortHandler.processSort(dataTableSort);
console.log(sortString); // "name,asc"

// Process custom sort condition
const customSort = {
key: 'created_at',
direction: 'desc'
};

const sortString2 = sortHandler.processSort(customSort);
console.log(sortString2); // "created_at,desc"

// Handle null/undefined
const nullSort = sortHandler.processSort(null);
console.log(nullSort); // ""

DataTable Integration

import { createSortHandler } from '@/utils/sort-handler';
import { DataTable } from 'mantine-datatable';
import { useState } from 'react';

function SortableTable() {
const [sortStatus, setSortStatus] = useState(null);
const sortHandler = createSortHandler({ encodeSort: true });

// Convert sort status to API parameter
const sortParam = sortHandler.processSort(sortStatus);

// Fetch data with sort
const { data, loading } = useQuery(
['users', sortParam],
() => fetchUsers({ sort: sortParam }),
{ keepPreviousData: true }
);

return (
<DataTable
records={data?.users}
columns={[
{ accessor: 'name', sortable: true },
{ accessor: 'email', sortable: true },
{ accessor: 'created_at', sortable: true }
]}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
loading={loading}
/>
);
}

📝 API Reference

Factory Function

interface SortOptions {
encodeSort?: boolean; // URL encode sort string
}

interface SortCondition {
key: string;
direction: 'asc' | 'desc';
}

function createSortHandler(options: SortOptions = {}): {
processSort: (
sortStatus: DataTableSortStatus | SortCondition | null
) => string;
};

Parameters

ParameterTypeDefaultDescription
optionsSortOptions{}Configuration options

Options

OptionTypeDefaultDescription
encodeSortbooleanfalseURL encode the resulting sort string

Return Value

MethodParametersReturnsDescription
processSortsortStatusstringProcess sort object into formatted string

🎨 Examples

Basic DataTable Usage

import { createSortHandler } from '@/utils/sort-handler';
import { DataTable } from 'mantine-datatable';
import { useState, useEffect } from 'react';

function UserTable() {
const [sortStatus, setSortStatus] = useState(null);
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);

const sortHandler = createSortHandler({ encodeSort: true });

// Fetch users whenever sort changes
useEffect(() => {
const fetchUsers = async () => {
setLoading(true);
try {
const sortParam = sortHandler.processSort(sortStatus);
const response = await fetch(`/api/users?sort=${sortParam}`);
const data = await response.json();
setUsers(data.users);
} catch (error) {
console.error('Failed to fetch users:', error);
} finally {
setLoading(false);
}
};

fetchUsers();
}, [sortStatus]);

const columns = [
{
accessor: 'name',
title: 'Name',
sortable: true
},
{
accessor: 'email',
title: 'Email',
sortable: true
},
{
accessor: 'role',
title: 'Role',
sortable: true
},
{
accessor: 'created_at',
title: 'Created At',
sortable: true,
render: (record) => new Date(record.created_at).toLocaleDateString()
}
];

return (
<div>
<h2>User Management</h2>

<DataTable
records={users}
columns={columns}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
loading={loading}
emptyState="No users found"
highlightOnHover
/>

{/* Debug info */}
<div style={{ marginTop: '20px', padding: '10px', background: '#f5f5f5' }}>
<strong>Current Sort:</strong> {sortHandler.processSort(sortStatus) || 'None'}
</div>
</div>
);
}

Multiple Table Sort Management

import { createSortHandler } from '@/utils/sort-handler';
import { useState } from 'react';

function MultiTableDashboard() {
// Separate sort states for different tables
const [usersSort, setUsersSort] = useState(null);
const [productsSort, setProductsSort] = useState(null);
const [ordersSort, setOrdersSort] = useState(null);

// Create handlers for each table
const usersSortHandler = createSortHandler({ encodeSort: true });
const productsSortHandler = createSortHandler({ encodeSort: true });
const ordersSortHandler = createSortHandler({ encodeSort: true });

// Fetch functions
const fetchUsers = async () => {
const sortParam = usersSortHandler.processSort(usersSort);
const response = await fetch(`/api/users?sort=${sortParam}`);
return response.json();
};

const fetchProducts = async () => {
const sortParam = productsSortHandler.processSort(productsSort);
const response = await fetch(`/api/products?sort=${sortParam}`);
return response.json();
};

const fetchOrders = async () => {
const sortParam = ordersSortHandler.processSort(ordersSort);
const response = await fetch(`/api/orders?sort=${sortParam}`);
return response.json();
};

return (
<div>
<h1>Dashboard</h1>

{/* Users Table */}
<section>
<h2>Recent Users</h2>
<DataTable
records={users}
columns={[
{ accessor: 'name', sortable: true },
{ accessor: 'email', sortable: true },
{ accessor: 'created_at', sortable: true }
]}
sortStatus={usersSort}
onSortStatusChange={setUsersSort}
/>
</section>

{/* Products Table */}
<section>
<h2>Top Products</h2>
<DataTable
records={products}
columns={[
{ accessor: 'name', sortable: true },
{ accessor: 'price', sortable: true },
{ accessor: 'category', sortable: true }
]}
sortStatus={productsSort}
onSortStatusChange={setProductsSort}
/>
</section>

{/* Orders Table */}
<section>
<h2>Recent Orders</h2>
<DataTable
records={orders}
columns={[
{ accessor: 'order_id', sortable: true },
{ accessor: 'customer_name', sortable: true },
{ accessor: 'total', sortable: true },
{ accessor: 'created_at', sortable: true }
]}
sortStatus={ordersSort}
onSortStatusChange={setOrdersSort}
/>
</section>
</div>
);
}

Custom Sort Conditions

import { createSortHandler } from '@/utils/sort-handler';

function CustomSortExample() {
const sortHandler = createSortHandler({ encodeSort: false });

// Custom sort conditions
const sortExamples = [
{ key: 'name', direction: 'asc' },
{ key: 'created_at', direction: 'desc' },
{ key: 'price', direction: 'asc' },
{ key: 'rating', direction: 'desc' }
];

return (
<div>
<h2>Sort Examples</h2>

{sortExamples.map((sort, index) => {
const sortString = sortHandler.processSort(sort);
return (
<div key={index} style={{ padding: '10px', margin: '5px 0', border: '1px solid #ddd' }}>
<strong>Input:</strong> {JSON.stringify(sort)} <br />
<strong>Output:</strong> <code>{sortString}</code>
</div>
);
})}

{/* Null/undefined handling */}
<div style={{ padding: '10px', margin: '5px 0', border: '1px solid #ddd' }}>
<strong>Null Input:</strong> <code>{sortHandler.processSort(null)}</code>
</div>
<div style={{ padding: '10px', margin: '5px 0', border: '1px solid #ddd' }}>
<strong>Undefined Input:</strong> <code>{sortHandler.processSort(undefined)}</code>
</div>
</div>
);
}

API Service Integration

import { createSortHandler } from '@/utils/sort-handler';

// Base API service class
class BaseApiService {
protected sortHandler = createSortHandler({ encodeSort: true });

async request(endpoint: string, options: {
filters?: Record<string, unknown>;
sort?: DataTableSortStatus | SortCondition | null;
pagination?: { page: number; perPage: number };
} = {}) {
const { filters = {}, sort, pagination = { page: 1, perPage: 20 } } = options;

// Build query parameters
const params = new URLSearchParams();

// Add sort parameter
const sortParam = this.sortHandler.processSort(sort);
if (sortParam) {
params.append('sort', sortParam);
}

// Add pagination
params.append('page', pagination.page.toString());
params.append('perPage', pagination.perPage.toString());

// Add filters (assuming filter handler is available)
if (Object.keys(filters).length > 0) {
const filterParam = processFilters(filters); // Assume this exists
if (filterParam) {
params.append('filter', filterParam);
}
}

const response = await fetch(`${endpoint}?${params}`);
return response.json();
}
}

// Specific service implementations
class UserService extends BaseApiService {
async getUsers(options: {
role?: string;
active?: boolean;
sort?: DataTableSortStatus | SortCondition | null;
pagination?: { page: number; perPage: number };
} = {}) {
const filters: Record<string, unknown> = {};

if (options.role) filters.role = options.role;
if (options.active !== undefined) filters.active = options.active;

return this.request('/api/users', {
...options,
filters
});
}

async getActiveUsers(sort?: DataTableSortStatus | SortCondition | null) {
return this.getUsers({
active: true,
sort
});
}

async getUsersByRole(role: string, sort?: DataTableSortStatus | SortCondition | null) {
return this.getUsers({
role,
sort
});
}
}

class ProductService extends BaseApiService {
async getProducts(options: {
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
sort?: DataTableSortStatus | SortCondition | null;
pagination?: { page: number; perPage: number };
} = {}) {
const filters: Record<string, unknown> = {};

if (options.category) filters.category = options.category;
if (options.minPrice !== undefined) filters.min_price = options.minPrice;
if (options.maxPrice !== undefined) filters.max_price = options.maxPrice;
if (options.inStock !== undefined) filters.in_stock = options.inStock;

return this.request('/api/products', {
...options,
filters
});
}

async getProductsByCategory(category: string, sort?: DataTableSortStatus | SortCondition | null) {
return this.getProducts({
category,
sort
});
}

async getInStockProducts(sort?: DataTableSortStatus | SortCondition | null) {
return this.getProducts({
inStock: true,
sort
});
}
}

// Usage in React component
function ServiceIntegratedComponent() {
const userService = new UserService();
const productService = new ProductService();

const [users, setUsers] = useState([]);
const [products, setProducts] = useState([]);
const [usersSort, setUsersSort] = useState(null);
const [productsSort, setProductsSort] = useState(null);

useEffect(() => {
// Load users with sort
userService.getUsers({ sort: usersSort }).then(setUsers);
}, [usersSort]);

useEffect(() => {
// Load products with sort
productService.getProducts({ sort: productsSort }).then(setProducts);
}, [productsSort]);

return (
<div>
<DataTable
records={users}
columns={[
{ accessor: 'name', sortable: true },
{ accessor: 'email', sortable: true }
]}
sortStatus={usersSort}
onSortStatusChange={setUsersSort}
/>

<DataTable
records={products}
columns={[
{ accessor: 'name', sortable: true },
{ accessor: 'price', sortable: true }
]}
sortStatus={productsSort}
onSortStatusChange={setProductsSort}
/>
</div>
);
}

🔧 Advanced Usage

URL Encoding Variants

import { createSortHandler } from '@/utils/sort-handler';

// Different encoding configurations

// No encoding (internal use)
const noEncodingHandler = createSortHandler({ encodeSort: false });

// URL encoding (API use)
const urlEncodingHandler = createSortHandler({ encodeSort: true });

// Custom encoding function
const customEncodingHandler = createSortHandler({ encodeSort: true });

function demonstrateEncoding() {
const sort = { key: 'name with spaces', direction: 'asc' };

// No encoding
const noEncoded = noEncodingHandler.processSort(sort);
console.log('No encoding:', noEncoded); // "name with spaces,asc"

// URL encoding
const urlEncoded = urlEncodingHandler.processSort(sort);
console.log('URL encoded:', urlEncoded); // "name%20with%20spaces%2Casc"

// Complex example with special characters
const complexSort = { key: 'price$amount', direction: 'desc' };
const complexEncoded = urlEncodingHandler.processSort(complexSort);
console.log('Complex encoded:', complexEncoded); // "price%24amount%2Cdesc"
}

Sort State Management

import { createSortHandler } from '@/utils/sort-handler';
import { useCallback, useState } from 'react';

// Custom hook for sort management
function useSortManager(initialSort = null) {
const [sort, setSort] = useState(initialSort);
const sortHandler = createSortHandler({ encodeSort: true });

// Get sort parameter for API
const getSortParam = useCallback(() => {
return sortHandler.processSort(sort);
}, [sort]);

// Reset sort
const resetSort = useCallback(() => {
setSort(null);
}, []);

// Set sort with validation
const setSortSafe = useCallback((newSort) => {
if (newSort && (!newSort.key || !newSort.direction)) {
console.warn('Invalid sort object:', newSort);
return;
}
setSort(newSort);
}, []);

// Toggle sort direction for a key
const toggleSort = useCallback((key) => {
setSort(current => {
if (current?.key === key) {
// Toggle direction
return {
key,
direction: current.direction === 'asc' ? 'desc' : 'asc'
};
} else {
// New sort key
return { key, direction: 'asc' };
}
});
}, []);

return {
sort,
sortParam: getSortParam(),
setSort: setSortSafe,
resetSort,
toggleSort,
sortHandler
};
}

// Usage in component
function AdvancedTable() {
const { sort, sortParam, setSort, resetSort, toggleSort } = useSortManager();

const columns = [
{
accessor: 'name',
sortable: true,
onClick: () => toggleSort('name')
},
{
accessor: 'email',
sortable: true,
onClick: () => toggleSort('email')
},
{
accessor: 'created_at',
sortable: true,
onClick: () => toggleSort('created_at')
}
];

return (
<div>
<div style={{ marginBottom: '10px' }}>
<span>Current sort: {sortParam || 'None'}</span>
<button onClick={resetSort} style={{ marginLeft: '10px' }}>
Reset Sort
</button>
</div>

<DataTable
records={data}
columns={columns}
sortStatus={sort}
onSortStatusChange={setSort}
/>
</div>
);
}

Sort Persistence

import { createSortHandler } from '@/utils/sort-handler';

// Sort persistence utility
class SortPersistence {
private sortHandler = createSortHandler({ encodeSort: false });
private storageKey = 'table-sort-state';

// Save sort to localStorage
saveSort(sort: DataTableSortStatus | SortCondition | null, tableId?: string) {
const key = tableId ? `${this.storageKey}-${tableId}` : this.storageKey;
localStorage.setItem(key, JSON.stringify(sort));
}

// Load sort from localStorage
loadSort(tableId?: string): DataTableSortStatus | SortCondition | null {
const key = tableId ? `${this.storageKey}-${tableId}` : this.storageKey;
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : null;
}

// Clear saved sort
clearSort(tableId?: string) {
const key = tableId ? `${this.storageKey}-${tableId}` : this.storageKey;
localStorage.removeItem(key);
}

// Get sort parameter for API
getSortParam(sort: DataTableSortStatus | SortCondition | null): string {
return this.sortHandler.processSort(sort);
}
}

// Usage in component
function PersistentTable() {
const sortPersistence = new SortPersistence();
const tableId = 'users-table';

// Load saved sort on mount
const [sort, setSort] = useState(() => {
return sortPersistence.loadSort(tableId);
});

// Save sort when it changes
const handleSortChange = (newSort) => {
setSort(newSort);
sortPersistence.saveSort(newSort, tableId);
};

// Get sort parameter for API
const sortParam = sortPersistence.getSortParam(sort);

// Clear sort
const clearSort = () => {
setSort(null);
sortPersistence.clearSort(tableId);
};

return (
<div>
<div style={{ marginBottom: '10px' }}>
<span>Persistent sort: {sortParam || 'None'}</span>
<button onClick={clearSort} style={{ marginLeft: '10px' }}>
Clear Saved Sort
</button>
</div>

<DataTable
records={data}
columns={[
{ accessor: 'name', sortable: true },
{ accessor: 'email', sortable: true }
]}
sortStatus={sort}
onSortStatusChange={handleSortChange}
/>
</div>
);
}

🔧 Dependencies

Required Dependencies

npm install mantine-datatable

Type Dependencies

import type { DataTableSortStatus } from 'mantine-datatable';

interface SortCondition {
key: string;
direction: 'asc' | 'desc';
}

🚨 Error Handling

Validation and Error Recovery

import { createSortHandler } from '@/utils/sort-handler';

// Safe sort handler with validation
const safeSortHandler = createSortHandler({ encodeSort: true });

function validateSortObject(sort: any): DataTableSortStatus | SortCondition | null {
if (!sort || typeof sort !== 'object') {
return null;
}

// Check required properties
if (!sort.key || typeof sort.key !== 'string') {
console.warn('Invalid sort object: missing or invalid key', sort);
return null;
}

if (!sort.direction || !['asc', 'desc'].includes(sort.direction)) {
console.warn('Invalid sort object: missing or invalid direction', sort);
return null;
}

return {
key: sort.key,
direction: sort.direction
};
}

// Safe processing function
function safeProcessSort(sort: any): string {
try {
const validatedSort = validateSortObject(sort);
return safeSortHandler.processSort(validatedSort);
} catch (error) {
console.error('Error processing sort:', error);
return '';
}
}

// Usage
function SafeSortComponent() {
const [sort, setSort] = useState(null);

const handleSortChange = (newSort) => {
const validatedSort = validateSortObject(newSort);
setSort(validatedSort);
};

const sortParam = safeProcessSort(sort);

return (
<div>
<DataTable
records={data}
columns={[
{ accessor: 'name', sortable: true },
{ accessor: 'email', sortable: true }
]}
sortStatus={sort}
onSortStatusChange={handleSortChange}
/>

<div>Safe sort param: {sortParam}</div>
</div>
);
}

📚 Best Practices

1. Consistent Sort Key Naming

// Use consistent naming conventions
const SORT_KEYS = {
NAME: 'name',
CREATED_AT: 'created_at',
UPDATED_AT: 'updated_at',
PRICE: 'price',
RATING: 'rating'
} as const;

// Usage with type safety
function getSortByKey(key: keyof typeof SORT_KEYS) {
return { key: SORT_KEYS[key], direction: 'asc' };
}

2. Default Sort Management

// Default sort configurations
const DEFAULT_SORTS = {
users: { key: 'created_at', direction: 'desc' },
products: { key: 'name', direction: 'asc' },
orders: { key: 'created_at', direction: 'desc' }
} as const;

function getDefaultSort(type: keyof typeof DEFAULT_SORTS) {
return DEFAULT_SORTS[type];
}

3. Performance Optimization

// Memoized sort handler
const memoizedSortHandler = useMemo(() =>
createSortHandler({ encodeSort: true }),
[]
);

// Memoized sort processing
const sortParam = useMemo(() =>
memoizedSortHandler.processSort(sort),
[sort, memoizedSortHandler]
);

🔍 Troubleshooting

Common Issues

Problem: Sort not working with DataTable Solution: Ensure sort object has correct columnAccessor and direction properties

Problem: API receiving malformed sort parameters Solution: Check if encodeSort is properly configured

Problem: Sort state not persisting Solution: Implement proper state management or localStorage persistence

Problem: Sort direction not toggling correctly Solution: Implement proper toggle logic in sort change handler

Debug Mode

import { createSortHandler } from '@/utils/sort-handler';

// Debug handler with logging
const debugSortHandler = createSortHandler({ encodeSort: true });

function debugProcessSort(sort) {
console.log('Input sort:', sort);
console.log('Sort type:', sort?.constructor.name);

if (sort) {
console.log('Key:', sort.key || sort.columnAccessor);
console.log('Direction:', sort.direction);
}

const result = debugSortHandler.processSort(sort);
console.log('Output:', result);

return result;
}

// Usage in development
if (process.env.NODE_ENV === 'development') {
const debugResult = debugProcessSort({ key: 'name', direction: 'asc' });
}