This document outlines best practices for maintaining and extending AlgoSketch's visualizers.
- Keep algorithm components in separate directories
- Each algorithm should have its own directory under
src/components/ - Shared components should be placed in
src/components/shared/
- Each algorithm should have its own directory under
-
Keep components focused on a single responsibility
- Bar component should only handle rendering a bar
- Control component should only handle user interaction
- Visualizer component should coordinate, not implement details
-
Consistent component naming
- Name components according to their function:
Bar,Control,InfoBox, etc. - Use consistent naming across different algorithm implementations
- Name components according to their function:
Always create new copies of objects when modifying state. This ensures React's reactivity works correctly and prevents hard-to-debug issues.
// ✅ Good: Creating a new copy
setArray(array.map((item) => ({ ...item, state: "default" })));
// ❌ Bad: Mutating state directly
array[0].state = "comparing";
setArray(array);Keep related state together and isolate it from unrelated state:
// ✅ Good: Grouping related state
const [sortStats, setSortStats] = useState({
comparisons: 0,
swaps: 0,
});
// Update together
setSortStats((prev) => ({
...prev,
comparisons: prev.comparisons + 1,
}));
// ❌ Bad: Scattered related state
const [comparisons, setComparisons] = useState(0);
const [swaps, setSwaps] = useState(0);- Store complete state for each step, not just changes
- This makes it easier to navigate backward and forward in the visualization
Always clean up intervals to prevent memory leaks:
useEffect(() => {
const interval = setInterval(() => {
// Animation logic
}, 1000);
// ✅ Good: Clean up on unmount or dependency change
return () => {
clearInterval(interval);
};
}, [dependencies]);- Use CSS transitions for smooth animations
- Keep track of animation state to prevent interrupted animations
// Adding transitions to elements
<div className="transition-all duration-300 ease-in-out" style={{ height: `${heightPercentage}%` }} />Use React.memo for components that render frequently but don't change often:
const Bar = React.memo(function Bar({ item, maxValue, index }: BarProps) {
// Implementation
});Use useCallback for functions passed as props to memoized components:
const handleNextStep = useCallback(() => {
// Implementation
}, [dependencies]);- Use
React.useMemofor expensive calculations - Use primitive values for props when possible
- Avoid creating new objects or arrays in render functions
// ✅ Good: Memoize expensive calculations
const sortedIndices = useMemo(() => {
return calculateSortedIndices(array);
}, [array]);
// ❌ Bad: Creating new arrays on every render
<Component items={[1, 2, 3]} />Define clear interfaces for all props and state:
interface BarProps {
item: BarItem;
maxValue: number;
index: number;
}
interface BarItem {
value: number;
state: SortingState;
id: string;
}
type SortingState = "default" | "comparing" | "swapping" | "sorted";Use TypeScript generics for shared components that need to work with different data structures:
function useAlgorithmVisualizer<T, S>({
generateRandomArray,
generateSteps,
}: {
generateRandomArray: (size: number) => T[];
generateSteps: (array: T[]) => S[];
}) {
// Implementation
}- Keep the core algorithm logic separate from visualization details
- Algorithm functions should return step data, not handle rendering
// ✅ Good: Separation of concerns
function bubbleSortSteps(array) {
// Returns step data for visualization
return steps;
}
// In component:
const BubbleSortVisualizer = () => {
// Rendering logic using step data
};Use consistent data structures across different algorithm implementations to make creating shared components easier:
// Bubble Sort and Insertion Sort using compatible step structures
interface BaseSortingStep {
array: BarItem[];
sortedIndices: number[];
}
interface BubbleSortStep extends BaseSortingStep {
comparing: number[];
swapped: boolean;
}
interface InsertionSortStep extends BaseSortingStep {
currentIndex: number;
comparingIndex: number;
insertPosition: number;
}Test your visualizers with various input types:
- Empty arrays
- Single-element arrays
- Already sorted arrays
- Reverse-sorted arrays
- Arrays with duplicate values
Add debugging aids during development:
// During development
console.log("Current step:", currentStep);
// Or use React DevTools
useDebugValue(currentStep);Use semantic HTML elements for better accessibility:
<button onClick={handleNext} aria-label="Next step">
Next
</button>
<div aria-label={`Value: ${item.value}`} role="img">
{/* Bar content */}
</div>Ensure all interactive elements are keyboard accessible:
<button
onClick={handleNext}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleNext();
}
}}
tabIndex={0}
>
Next
</button>Ensure sufficient color contrast for text and UI elements:
- Use the Tailwind color palette, which is designed with accessibility in mind
- Test with a color contrast checker for custom colors
Move common functionality to utility functions:
// In utils.ts
export function generateRandomArray(length, maxValue = 100) {
// Implementation
}
// In component
import { generateRandomArray } from "@/utils";Use component composition to build complex UIs from simpler components:
// ✅ Good: Composition
<Visualizer>
<VisualizerDisplay>
<BarsContainer>
{bars.map((bar) => (
<Bar {...bar} />
))}
</BarsContainer>
</VisualizerDisplay>
<ControlPanel>
<PlayControls />
<SpeedControl />
</ControlPanel>
</Visualizer>Add meaningful comments for complex logic:
// Calculate the insertion position for the current element
// by comparing it with each element in the sorted portion
// and finding the first element that is greater
let insertPos = j;
while (insertPos > 0 && array[insertPos - 1].value > currentValue) {
// Shift elements to the right
array[insertPos] = array[insertPos - 1];
insertPos--;
}Use JSDoc comments for functions and components:
/**
* Generates the steps for bubble sort visualization.
*
* @param inputArray - The array to sort
* @returns An array of steps, each representing a state in the sorting process
*/
export function bubbleSortSteps(inputArray: BarItem[]): SortingStep[] {
// Implementation
}Document complex types with JSDoc:
/**
* Represents a single step in the sorting algorithm.
* @property array - The current state of the array
* @property comparing - Indices of elements being compared
* @property swapped - Whether elements were swapped in this step
* @property sortedIndices - Indices of elements that are in their final sorted position
*/
export interface SortingStep {
array: BarItem[];
comparing: number[];
swapped: boolean;
sortedIndices: number[];
}Handle errors gracefully to prevent the UI from breaking:
try {
// Code that might fail
const steps = generateSteps(array);
setSteps(steps);
} catch (error) {
console.error("Failed to generate steps:", error);
// Set fallback or display error message
setSteps([{ array, sortedIndices: [] }]);
setError("Failed to generate visualization steps");
}Validate inputs to prevent errors:
function resetArray(size: number) {
// Validate input
if (size <= 0 || size > MAX_ARRAY_SIZE) {
console.warn(`Invalid array size: ${size}. Using default size of 10.`);
size = 10;
}
// Continue with valid size
const newArray = generateRandomArray(size);
// ...
}Design components to work on different screen sizes:
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="md:col-span-2">{/* Main visualization */}</div>
<div>{/* Info panel */}</div>
</div>- Use appropriate touch targets for mobile devices
- Consider touch interactions vs. mouse interactions
- Test on various screen sizes
Consider a plugin architecture for adding new algorithms:
// Register a new algorithm
registerAlgorithm({
name: "quicksort",
title: "Quick Sort",
description: "A divide-and-conquer algorithm...",
generateSteps: quickSortSteps,
components: {
StepDescription: QuickSortStepDescription,
CodeSnippet: QuickSortCodeSnippet,
},
});Make components configurable through props:
<Bar
item={item}
maxValue={maxValue}
index={index}
showValue={true}
showIndex={true}
stateStyles={{
default: "bg-blue-500",
comparing: "bg-yellow-500",
// Custom colors
}}
/>Use descriptive commit messages:
feat: Add merge sort visualization
fix: Fix animation timing issue in insertion sort
refactor: Extract shared bar component
docs: Update algorithm documentation
Use feature branches for new algorithms or major changes:
feature/merge-sort
feature/shared-components
fix/animation-timing
Conduct thorough code reviews:
- Check for consistent styling
- Ensure proper error handling
- Verify performance considerations
- Test on different browsers and devices