React and JSX
These guidelines cover React and JSX patterns used in SlideSpeak. They focus on avoiding common pitfalls, using React effectively, and following patterns that make code easier to maintain and debug.
Avoiding useEffect When Possible
Section titled “Avoiding useEffect When Possible”useEffect is an escape hatch from React’s paradigm. Most code doesn’t need Effects. State updates should occur event-based, not through Effects.
When you use useEffect unnecessarily, you create extra render cycles, make code harder to understand because it no longer reads from top to bottom, and introduce potential bugs like race conditions or stale closures.
When NOT to Use useEffect
Section titled “When NOT to Use useEffect”Don’t Transform Data for Rendering
Section titled “Don’t Transform Data for Rendering”React already re-renders when props or state change. Calculating derived values during render is more efficient than using Effects because it avoids unnecessary render cycles.
❌ Bad: Using Effect to calculate derived state
const [isSelected, setIsSelected] = useState(false);
useEffect(() => { setIsSelected(selectedSlideId === id);}, [selectedSlideId, id]);This causes two renders: first with stale isSelected, then immediately again with the updated value.
✅ Good: Calculate during render
const isSelected = selectedSlideId === id;const isFocused = isSelected && slideTrayIsFocused;Don’t Sync Props to Local State
Section titled “Don’t Sync Props to Local State”If you need to reset local state when props change, use the key prop to reset the entire component instead of syncing in an Effect.
❌ Bad: Syncing props to state with Effect
const [localTitle, setLocalTitle] = useState(title);
useEffect(() => { setLocalTitle(title);}, [title]);✅ Good: Use props directly, or use key to reset component
const EditableSlide = ({ title }: { title: string }) => { const [localTitle, setLocalTitle] = useState(title);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { setLocalTitle(e.target.value); };
return <input value={localTitle} onChange={handleChange} />;};
<EditableSlide title={title} key={slideId} />When slideId changes, React will remount the component with fresh state.
Don’t Handle User Events in Effects
Section titled “Don’t Handle User Events in Effects”Event handlers already know exactly what action the user took. Effects run after render and don’t have direct access to the user’s intent.
❌ Bad: Event logic in Effect
useEffect(() => { if (product.isInCart) { showNotification(`Added ${product.name} to cart!`); }}, [product]);✅ Good: Event logic in handler
const handleClick = () => { EditorActions.updateSelectedSlideId(id); EditorActions.updateSlideTrayIsFocused(true);};
<button onClick={handleClick}>Select</button>Don’t Fetch Data in Effects
Section titled “Don’t Fetch Data in Effects”Use React Query instead of Effects for data fetching. React Query handles caching, race conditions, loading states, error handling, and refetching automatically.
❌ Bad: Fetching data with Effect
const [data, setData] = useState(null);const [isLoading, setIsLoading] = useState(true);
useEffect(() => { let ignore = false;
fetch(`/api/user/${userId}`) .then(res => res.json()) .then(json => { if (!ignore) { setData(json); setIsLoading(false); } });
return () => { ignore = true; };}, [userId]);You have to manually handle cleanup, loading states, errors, and caching.
✅ Good: Use React Query
const { data: user, refetch } = useQuery({ queryKey: [USER_INFO_QUERY_KEY], queryFn: () => getUserInfo(), staleTime: 1000 * 60,});React Query automatically handles:
- Caching
- Loading and error states
- Race condition prevention
For conditional fetching:
const query = useQuery({ queryKey: ['document', 'selected', resolvedId], queryFn: async () => { if (!resolvedId) return null; return getDocumentForUser(resolvedId); }, enabled: Boolean(resolvedId), staleTime: Infinity, gcTime: Infinity,});When to Use useEffect
Section titled “When to Use useEffect”Only use useEffect for:
- Component mount logic (no dependency array) - Initialize something once when component mounts
- Syncing with external systems - Connect to timers, subscriptions, browser APIs, or non-React widgets
✅ Good: Syncing with external system (timer)
useEffect(() => { const interval = setInterval(() => { updateTime(); }, 1000);
return () => clearInterval(interval);}, []);✅ Good: Component mount initialization
useEffect(() => { initializeAnalytics();}, []);Always clean up subscriptions, timers, or event listeners in the cleanup function to prevent memory leaks.
Defining Components
Section titled “Defining Components”Always use const syntax with arrow functions for component definitions. Use FC<> (Function Component) type from React to type your components.
Import FC
Section titled “Import FC”import { FC } from 'react';Component with Props
Section titled “Component with Props”Define props as a type and use FC<PropsType>:
import { FC } from 'react';
type ActionBannerProps = { title: string; description: string; actionText: string; onAction: () => void; icon?: React.ReactNode;};
export const ActionBanner: FC<ActionBannerProps> = ({ title, description, actionText, onAction, icon,}) => { return ( <div className="flex items-center gap-3"> <div>{title}</div> <Button onClick={onAction}>{actionText}</Button> </div> );};Component without Props
Section titled “Component without Props”For components without props, use FC without type parameters:
import { FC } from 'react';
export const LanguageSelector: FC = () => { const { language } = useOutlinePresentationSettings();
return ( <div> <SearchLanguageSelect value={language} /> </div> );};Extending HTML Attributes for Reusable Components
Section titled “Extending HTML Attributes for Reusable Components”When creating reusable UI components that will be used in multiple places, extend HTMLAttributes to allow consumers to pass through standard HTML props like onClick, data-* attributes, aria-* attributes, and custom className.
Use intersection types with HTMLAttributes:
import { FC, HTMLAttributes } from 'react';import { cn } from '@/utils/cn';
type OnboardingMessageProps = { message: string; description?: string; buttonText: string; buttonAction: () => void;} & HTMLAttributes<HTMLDivElement>;
export const OnboardingMessage: FC<OnboardingMessageProps> = ({ message, description, buttonText, buttonAction, className, ...props}) => ( <div className={cn( 'flex w-[360px] max-w-[90vw] flex-col items-center gap-6 rounded-2xl bg-white p-6', className, )} {...props} > <div>{message}</div> <Button onClick={buttonAction}>{buttonText}</Button> </div>);Key points:
- Extract
classNameseparately to merge withcn() - Spread
...propsonto the rootdivto pass through all HTML attributes - This allows consumers to add
onClick,data-*attributes,aria-*attributes, etc.
Using cn() for Conditional Classes
Section titled “Using cn() for Conditional Classes”Always use cn() (from @/utils/cn) instead of string concatenation for className. It combines clsx for conditional classes and tailwind-merge to intelligently resolve Tailwind class conflicts.
Benefits:
- Conditional classes - Easily add classes based on conditions
- Merge props - Automatically merges
classNameprop with base classes - Conflict resolution - Resolves Tailwind conflicts (e.g.,
p-4andp-2→ keepsp-2)
❌ Bad: String concatenation
<div className={`flex w-full gap-3 ${isUser ? 'flex-row-reverse' : 'flex-row'} ${className || ''}`}>✅ Good: Using cn()
import { cn } from '@/utils/cn';
<div className={cn( 'flex w-full gap-3 py-2', isUser ? 'flex-row-reverse justify-start' : 'flex-row justify-start', className, )}>Example: Conditional classes
<div className={cn( 'relative border rounded-lg overflow-hidden border-gray-200', isSelected && 'outline outline-[3px] outline-blue-600', )}>Example: Conditional values
<div className={cn( 'rounded-xl p-4', isUser ? 'bg-blue-100 outline-blue-200' : 'bg-white outline-blue-100', bubbleClassName, )}>When merging className prop, always put it last so consumers can override base styles:
className={cn( 'base-classes-here', conditionalClasses, className, // Always last to allow overrides)}