Skip to content
intermediatePhase 37 · Frontend Architecture

Sorting

Build sortable tables and lists with multi-column sort support.

30m
0 problems
Topic Progress0%

Client Sorting

Client Sorting

Sort already-loaded data on the client.

Basic Sort Hook

// hooks/useSorting.js
function useSorting(items, defaultSort = { key: null, direction: 'asc' }) {
  const [sortConfig, setSortConfig] = useState(defaultSort);

  const sortedItems = useMemo(() => {
    if (!sortConfig.key) return items;

    return [...items].sort((a, b) => {
      const aVal = a[sortConfig.key];
      const bVal = b[sortConfig.key];

      if (aVal === bVal) return 0;
      if (aVal === null || aVal === undefined) return 1;
      if (bVal === null || bVal === undefined) return -1;

      let comparison = 0;
      if (typeof aVal === 'string') {
        comparison = aVal.localeCompare(bVal);
      } else if (typeof aVal === 'number') {
        comparison = aVal - bVal;
      } else if (aVal instanceof Date) {
        comparison = aVal.getTime() - bVal.getTime();
      } else if (typeof aVal === 'boolean') {
        comparison = aVal === bVal ? 0 : aVal ? -1 : 1;
      }

      return sortConfig.direction === 'desc' ? -comparison : comparison;
    });
  }, [items, sortConfig]);

  const requestSort = useCallback((key) => {
    setSortConfig(prev => ({
      key,
      direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc',
    }));
  }, []);

  const clearSort = useCallback(() => {
    setSortConfig({ key: null, direction: 'asc' });
  }, []);

  return { sortedItems, sortConfig, requestSort, clearSort };
}

Sort Button Component

// components/SortButton.jsx
function SortButton({ label, sortKey, currentSort, onSort }) {
  const isActive = currentSort.key === sortKey;
  const direction = isActive ? currentSort.direction : null;

  return (
    <button
      className={`sort-button ${isActive ? 'active' : ''}`}
      onClick={() => onSort(sortKey)}
      aria-label={`Sort by ${label} ${direction === 'asc' ? 'ascending' : 'descending'}`}
    >
      {label}
      {isActive && (
        <span className="sort-icon">
          {direction === 'asc' ? '↑' : '↓'}
        </span>
      )}
    </button>
  );
}

// Usage
function ProductTable({ products }) {
  const { sortedItems, sortConfig, requestSort } = useSorting(products);

  return (
    <table>
      <thead>
        <tr>
          <th>
            <SortButton
              label="Name"
              sortKey="name"
              currentSort={sortConfig}
              onSort={requestSort}
            />
          </th>
          <th>
            <SortButton
              label="Price"
              sortKey="price"
              currentSort={sortConfig}
              onSort={requestSort}
            />
          </th>
          <th>
            <SortButton
              label="Rating"
              sortKey="rating"
              currentSort={sortConfig}
              onSort={requestSort}
            />
          </th>
        </tr>
      </thead>
      <tbody>
        {sortedItems.map(product => (
          <tr key={product.id}>
            <td>{product.name}</td>
            <td>${product.price.toFixed(2)}</td>
            <td>{product.rating}★</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Sort State in URL

function useURLSort(defaultSort = 'relevance') {
  const [searchParams, setSearchParams] = useSearchParams();
  const sortBy = searchParams.get('sort') || defaultSort;
  const sortDir = searchParams.get('dir') || 'asc';

  const setSort = useCallback((key, direction = 'asc') => {
    setSearchParams(prev => {
      const params = new URLSearchParams(prev);
      params.set('sort', key);
      params.set('dir', direction);
      return params;
    });
  }, [setSearchParams]);

  return { sortBy, sortDir, setSort };
}

Server Sorting

Server Sorting

Delegate sorting to the server for large datasets.

Server Sort Hook

// hooks/useServerSort.js
function useServerSort({ endpoint, defaultSort = 'createdAt', defaultDir = 'desc' }) {
  const [sortConfig, setSortConfig] = useState({
    key: defaultSort,
    direction: defaultDir,
  });

  const { data, isLoading, error } = useQuery({
    queryKey: ['sorted', endpoint, sortConfig],
    queryFn: async () => {
      const params = new URLSearchParams({
        sortBy: sortConfig.key,
        sortDir: sortConfig.direction,
      });
      const response = await fetch(`${endpoint}?${params}`);
      return response.json();
    },
  });

  const requestSort = useCallback((key) => {
    setSortConfig(prev => ({
      key,
      direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc',
    }));
  }, []);

  return {
    items: data?.items || [],
    total: data?.total || 0,
    sortConfig,
    requestSort,
    isLoading,
    error,
  };
}

Sort Dropdown

function SortDropdown({ value, onChange }) {
  return (
    <select value={value} onChange={(e) => onChange(e.target.value)}>
      <option value="relevance">Relevance</option>
      <option value="name-asc">Name (A-Z)</option>
      <option value="name-desc">Name (Z-A)</option>
      <option value="price-asc">Price (Low to High)</option>
      <option value="price-desc">Price (High to Low)</option>
      <option value="rating-desc">Rating (High to Low)</option>
      <option value="newest">Newest First</option>
    </select>
  );
}

// Usage
function ProductList() {
  const { items, sortConfig, requestSort, isLoading } = useServerSort({
    endpoint: '/api/products',
  });

  return (
    <div>
      <div className="sort-controls">
        <SortDropdown
          value={`${sortConfig.key}-${sortConfig.direction}`}
          onChange={(value) => {
            const [key, dir] = value.split('-');
            requestSort(key);
          }}
        />
      </div>
      {isLoading ? <Spinner /> : <ProductGrid products={items} />}
    </div>
  );
}

API Response Pattern

// Server response format
// GET /api/products?sortBy=price&sortDir=asc&page=1&limit=20

// Response
{
  "items": [...],
  "total": 100,
  "page": 1,
  "limit": 20,
  "sortBy": "price",
  "sortDir": "asc"
}

Multi-Column Sort

Multi-Column Sort

Allow sorting by multiple columns simultaneously.

Multi-Column Sort Hook

// hooks/useMultiSort.js
function useMultiSort(items, defaultSort = []) {
  const [sorts, setSorts] = useState(defaultSort);

  const sortedItems = useMemo(() => {
    if (sorts.length === 0) return items;

    return [...items].sort((a, b) => {
      for (const { key, direction } of sorts) {
        const aVal = a[key];
        const bVal = b[key];

        if (aVal === bVal) continue;
        if (aVal === null || aVal === undefined) return 1;
        if (bVal === null || bVal === undefined) return -1;

        let comparison = 0;
        if (typeof aVal === 'string') {
          comparison = aVal.localeCompare(bVal);
        } else if (typeof aVal === 'number') {
          comparison = aVal - bVal;
        }

        return direction === 'desc' ? -comparison : comparison;
      }
      return 0;
    });
  }, [items, sorts]);

  const addSort = useCallback((key, direction = 'asc') => {
    setSorts(prev => {
      const existing = prev.findIndex(s => s.key === key);
      if (existing >= 0) {
        if (prev[existing].direction === direction) {
          return prev.filter((_, i) => i !== existing);
        }
        return prev.map((s, i) => 
          i === existing ? { ...s, direction } : s
        );
      }
      return [...prev, { key, direction }];
    });
  }, []);

  const clearSort = useCallback(() => {
    setSorts([]);
  }, []);

  const removeSort = useCallback((key) => {
    setSorts(prev => prev.filter(s => s.key !== key));
  }, []);

  return { sortedItems, sorts, addSort, removeSort, clearSort };
}

Sort Priority UI

function SortPriorityList({ sorts, onRemove, onReorder }) {
  if (sorts.length === 0) return null;

  return (
    <div className="sort-priority">
      <h4>Sort Priority</h4>
      <ul>
        {sorts.map((sort, index) => (
          <li key={sort.key} className="sort-item">
            <span className="sort-number">{index + 1}</span>
            <span>{sort.key}</span>
            <span className="sort-direction">
              {sort.direction === 'asc' ? '↑' : '↓'}
            </span>
            <button onClick={() => onRemove(sort.key)}>×</button>
            {index > 0 && (
              <button onClick={() => onReorder(index, index - 1)}>↑</button>
            )}
            {index < sorts.length - 1 && (
              <button onClick={() => onReorder(index, index + 1)}>↓</button>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
}

Advanced Sort Table

function SortableTable({ data, columns }) {
  const { sortedItems, sorts, addSort, removeSort, clearSort } = useMultiSort(data);

  return (
    <div>
      <SortPriorityList sorts={sorts} onRemove={removeSort} />
      <table>
        <thead>
          <tr>
            {columns.map(col => (
              <th key={col.key}>
                <button
                  onClick={() => addSort(col.key)}
                  className="sort-header"
                >
                  {col.label}
                  {sorts.find(s => s.key === col.key) && (
                    <span>
                      {sorts.find(s => s.key === col.key).direction === 'asc' ? ' ↑' : ' ↓'}
                    </span>
                  )}
                </button>
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {sortedItems.map((row, i) => (
            <tr key={row.id || i}>
              {columns.map(col => (
                <td key={col.key}>{row[col.key]}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Practice Problems

0/3solved
Build Sorting Component

Create a reusable React component implementing Sorting. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
Sorting Testing

Write unit and integration tests for Sorting using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
Sorting Performance

Optimize Sorting for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

Quiz

1. How do you toggle sort direction when clicking the same column?

Question 1 options

2. When should you use server-side sorting?

Question 2 options

3. How does multi-column sort determine priority?

Question 3 options

4. Why show sort indicators to users?

Question 4 options

Flashcards

Question

How do you implement ascending/descending toggle?

Answer

Check if clicking the same column and flip direction; otherwise, set ascending for new column.

Question

When should you use server-side sorting?

Answer

For paginated data or datasets too large to load entirely on the client.

Question

What is multi-column sort?

Answer

Sorting by multiple criteria where each subsequent sort only applies to items with equal values in previous sorts.

Question

Why memoize sort calculations?

Answer

To avoid recalculating the entire sort on every render when items or sort config haven't changed.

Question

What is Sorting?

Answer

Sorting is a key concept in frontend development.

Revision Notes

Key Takeaways

  • 1.Client-side sorting works for small, loaded datasets
  • 2.Server-side sorting is needed for paginated data
  • 3.Toggle direction when clicking the same sort column
  • 4.Multi-column sort uses priority order for tiebreaking
  • 5.Always show sort indicators to users

Interview Tips

  • Explain how to implement ascending/descending toggle
  • Discuss when to use client vs server sorting
  • Know how multi-column sort priority works

Cheat Sheet

Sorting Cheat Sheet

Client Sort

const sorted = [...items].sort((a, b) => {
  if (a[key] < b[key]) return direction === 'asc' ? -1 : 1;
  if (a[key] > b[key]) return direction === 'asc' ? 1 : -1;
  return 0;
});

Server Sort

fetch(`/api/items?sortBy=${key}&sortDir=${direction}`);

Toggle Direction

const newDirection = currentKey === key && currentDir === 'asc' ? 'desc' : 'asc';