All skills
davila7 avatar

/frontend-dev-guidelines

@1deb97c

Frontend development guidelines for React/TypeScript applications. Modern patterns including Suspense, lazy loading, useSuspenseQuery, file organization with features directory, MUI v7 styling, TanStack Router, performance optimization, and TypeScript best practices. Use when creating components, pages, features, fetching data, styling, routing, or working with frontend code.

Use this Skill: https://skilld.dev/gh/davila7/claude-code-templates/frontend-dev-guidelines

This session only. Nothing lands on disk.

resourcesperformance.md

≈2.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Performance Optimization

Patterns for optimizing React component performance, preventing unnecessary re-renders, and avoiding memory leaks.


Memoization Patterns

useMemo for Expensive Computations

import { useMemo } from 'react';

export const DataDisplay: React.FC<{ items: Item[], searchTerm: string }> = ({
    items,
    searchTerm,
}) => {
    // ❌ AVOID - Runs on every render
    const filteredItems = items
        .filter(item => item.name.includes(searchTerm))
        .sort((a, b) => a.name.localeCompare(b.name));

    // ✅ CORRECT - Memoized, only recalculates when dependencies change
    const filteredItems = useMemo(() => {
        return items
            .filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase()))
            .sort((a, b) => a.name.localeCompare(b.name));
    }, [items, searchTerm]);

    return <List items={filteredItems} />;
};

When to use useMemo:

  • Filtering/sorting large arrays
  • Complex calculations
  • Transforming data structures
  • Expensive computations (loops, recursion)

When NOT to use useMemo:

  • Simple string concatenation
  • Basic arithmetic
  • Premature optimization (profile first!)

useCallback for Event Handlers

The Problem

// ❌ AVOID - Creates new function on every render
export const Parent: React.FC = () => {
    const handleClick = (id: string) => {
        console.log('Clicked:', id);
    };

    // Child re-renders every time Parent renders
    // because handleClick is a new function reference each time
    return <Child onClick={handleClick} />;
};

The Solution

import { useCallback } from 'react';

export const Parent: React.FC = () => {
    // ✅ CORRECT - Stable function reference
    const handleClick = useCallback((id: string) => {
        console.log('Clicked:', id);
    }, []); // Empty deps = function never changes

    // Child only re-renders when props actually change
    return <Child onClick={handleClick} />;
};

When to use useCallback:

  • Functions passed as props to children
  • Functions used as dependencies in useEffect
  • Functions passed to memoized components
  • Event handlers in lists

When NOT to use useCallback:

  • Event handlers not passed to children
  • Simple inline handlers: onClick={() => doSomething()}

React.memo for Component Memoization

Basic Usage

import React from 'react';

interface ExpensiveComponentProps {
    data: ComplexData;
    onAction: () => void;
}

// ✅ Wrap expensive components in React.memo
export const ExpensiveComponent = React.memo<ExpensiveComponentProps>(
    function ExpensiveComponent({ data, onAction }) {
        // Complex rendering logic
        return <ComplexVisualization data={data} />;
    }
);

When to use React.memo:

  • Component renders frequently
  • Component has expensive rendering
  • Props don't change often
  • Component is a list item
  • DataGrid cells/renderers

When NOT to use React.memo:

  • Props change frequently anyway
  • Rendering is already fast
  • Premature optimization

Debounced Search

Using use-debounce Hook

import { useState } from 'react';
import { useDebounce } from 'use-debounce';
import { useSuspenseQuery } from '@tanstack/react-query';

export const SearchComponent: React.FC = () => {
    const [searchTerm, setSearchTerm] = useState('');

    // Debounce for 300ms
    const [debouncedSearchTerm] = useDebounce(searchTerm, 300);

    // Query uses debounced value
    const { data } = useSuspenseQuery({
        queryKey: ['search', debouncedSearchTerm],
        queryFn: () => api.search(debouncedSearchTerm),
        enabled: debouncedSearchTerm.length > 0,
    });

    return (
        <input
            value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)}
            placeholder='Search...'
        />
    );
};

Optimal Debounce Timing:

  • 300-500ms: Search/filtering
  • 1000ms: Auto-save
  • 100-200ms: Real-time validation

Memory Leak Prevention

Cleanup Timeouts/Intervals

import { useEffect, useState } from 'react';

export const MyComponent: React.FC = () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
        // ✅ CORRECT - Cleanup interval
        const intervalId = setInterval(() => {
            setCount(c => c + 1);
        }, 1000);

        return () => {
            clearInterval(intervalId);  // Cleanup!
        };
    }, []);

    useEffect(() => {
        // ✅ CORRECT - Cleanup timeout
        const timeoutId = setTimeout(() => {
            console.log('Delayed action');
        }, 5000);

        return () => {
            clearTimeout(timeoutId);  // Cleanup!
        };
    }, []);

    return <div>{count}</div>;
};

Cleanup Event Listeners

useEffect(() => {
    const handleResize = () => {
        console.log('Resized');
    };

    window.addEventListener('resize', handleResize);

    return () => {
        window.removeEventListener('resize', handleResize);  // Cleanup!
    };
}, []);

Abort Controllers for Fetch

useEffect(() => {
    const abortController = new AbortController();

    fetch('/api/data', { signal: abortController.signal })
        .then(response => response.json())
        .then(data => setState(data))
        .catch(error => {
            if (error.name === 'AbortError') {
                console.log('Fetch aborted');
            }
        });

    return () => {
        abortController.abort();  // Cleanup!
    };
}, []);

Note: With TanStack Query, this is handled automatically.


Form Performance

Watch Specific Fields (Not All)

import { useForm } from 'react-hook-form';

export const MyForm: React.FC = () => {
    const { register, watch, handleSubmit } = useForm();

    // ❌ AVOID - Watches all fields, re-renders on any change
    const formValues = watch();

    // ✅ CORRECT - Watch only what you need
    const username = watch('username');
    const email = watch('email');

    // Or multiple specific fields
    const [username, email] = watch(['username', 'email']);

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <input {...register('username')} />
            <input {...register('email')} />
            <input {...register('password')} />

            {/* Only re-renders when username/email change */}
            <p>Username: {username}, Email: {email}</p>
        </form>
    );
};

List Rendering Optimization

Key Prop Usage

// ✅ CORRECT - Stable unique keys
{items.map(item => (
    <ListItem key={item.id}>
        {item.name}
    </ListItem>
))}

// ❌ AVOID - Index as key (unstable if list changes)
{items.map((item, index) => (
    <ListItem key={index}>  // WRONG if list reorders
        {item.name}
    </ListItem>
))}

Memoized List Items

const ListItem = React.memo<ListItemProps>(({ item, onAction }) => {
    return (
        <Box onClick={() => onAction(item.id)}>
            {item.name}
        </Box>
    );
});

export const List: React.FC<{ items: Item[] }> = ({ items }) => {
    const handleAction = useCallback((id: string) => {
        console.log('Action:', id);
    }, []);

    return (
        <Box>
            {items.map(item => (
                <ListItem
                    key={item.id}
                    item={item}
                    onAction={handleAction}
                />
            ))}
        </Box>
    );
};

Preventing Component Re-initialization

The Problem

// ❌ AVOID - Component recreated on every render
export const Parent: React.FC = () => {
    // New component definition each render!
    const ChildComponent = () => <div>Child</div>;

    return <ChildComponent />;  // Unmounts and remounts every render
};

The Solution

// ✅ CORRECT - Define outside or use useMemo
const ChildComponent: React.FC = () => <div>Child</div>;

export const Parent: React.FC = () => {
    return <ChildComponent />;  // Stable component
};

// ✅ OR if dynamic, use useMemo
export const Parent: React.FC<{ config: Config }> = ({ config }) => {
    const DynamicComponent = useMemo(() => {
        return () => <div>{config.title}</div>;
    }, [config.title]);

    return <DynamicComponent />;
};

Lazy Loading Heavy Dependencies

Code Splitting

// ❌ AVOID - Import heavy libraries at top level
import jsPDF from 'jspdf';  // Large library loaded immediately
import * as XLSX from 'xlsx';  // Large library loaded immediately

// ✅ CORRECT - Dynamic import when needed
const handleExportPDF = async () => {
    const { jsPDF } = await import('jspdf');
    const doc = new jsPDF();
    // Use it
};

const handleExportExcel = async () => {
    const XLSX = await import('xlsx');
    // Use it
};

Summary

Performance Checklist:

  • ✅ useMemo for expensive computations (filter, sort, map)
  • ✅ useCallback for functions passed to children
  • ✅ React.memo for expensive components
  • ✅ Debounce search/filter (300-500ms)
  • ✅ Cleanup timeouts/intervals in useEffect
  • ✅ Watch specific form fields (not all)
  • ✅ Stable keys in lists
  • ✅ Lazy load heavy libraries
  • ✅ Code splitting with React.lazy

See Also:

Source: SKILL.md on GitHub

1 alert16d4 checks · Risk HIGH
  • Gen Agent Trust Hub16d

    The skill recommends the use of non-standard and potentially malicious Node.js packages that appear to be typosquats of the popular react-hook-form library. Furthermore, the documentation contains a systematic and deceptive string replacement pattern where 'form' is replaced with 'blog' (e.g., 'transblogers', 'Perblogance'), which could be used to evade security filters or cause the generation of broken code.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    11/11 files flagged

Signed by skilld at 1deb97c. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 14 hours ago.

Activeupdated 8 months ago

README badge

README badge for davila7/claude-code-templates/frontend-dev-guidelines