TypeScript עם React

React ו-TypeScript הם הצמד הנפוץ ביותר בפיתוח Frontend היום. השיעור מראה את הדפוסים שתכתבו כל יום: props, state, אירועים, refs ו-Context. הוא מניח שעברתם את קורס React.

npm create vite@latest my-app -- --template react-ts

Props

type ButtonProps = {
    label: string;
    variant?: 'primary' | 'secondary';
    disabled?: boolean;
    onClick: () => void;
};

function Button({ label, variant = 'primary', disabled = false, onClick }: ButtonProps) {
    return (
        <button className={`btn btn-${variant}`} disabled={disabled} onClick={onClick}>
            {label}
        </button>
    );
}

<Button label="שמירה" onClick={save} />
<Button label="שמירה" variant="danger" onClick={save} />
//                    ~~~~~~~ Type '"danger"' is not assignable to type '"primary" | "secondary"'

לא צריך React.FC: פונקציה רגילה עם props מוקלדים היא הסגנון המומלץ.

children

import type { ReactNode } from 'react';

type CardProps = {
    title: string;
    children: ReactNode;   // כל מה ש-React יודעת לרנדר
};

function Card({ title, children }: CardProps) {
    return (
        <section className="card">
            <h2>{title}</h2>
            {children}
        </section>
    );
}

להרחיב props של אלמנט HTML

import type { ComponentProps } from 'react';

// כל מה ש-<input> מקבל, ועוד label
type TextFieldProps = ComponentProps<'input'> & { label: string };

function TextField({ label, ...inputProps }: TextFieldProps) {
    return (
        <label>
            {label}
            <input {...inputProps} />
        </label>
    );
}

<TextField label="אימייל" type="email" required placeholder="[email protected]" />

useState

const [count, setCount] = useState(0);              // מוסק: number
const [user, setUser] = useState<User | null>(null); // צריך להגיד: עכשיו null, אחר כך User
const [tags, setTags] = useState<string[]>([]);      // מערך ריק: TypeScript לא יודעת ממה

if (user) {
    console.log(user.name);   // Narrowing עובד גם כאן
}

אירועים

import type { ChangeEvent, FormEvent } from 'react';

function SearchBox() {
    const [query, setQuery] = useState('');

    function handleChange(event: ChangeEvent<HTMLInputElement>) {
        setQuery(event.target.value);
    }

    function handleSubmit(event: FormEvent<HTMLFormElement>) {
        event.preventDefault();
        search(query);
    }

    return (
        <form onSubmit={handleSubmit}>
            <input value={query} onChange={handleChange} />
        </form>
    );
}

טריק: כשה-handler כתוב ישירות ב-JSX (onChange={(event) => ...}), הטיפוס של event מוסק לבד. לא צריך לזכור את השמות.

useRef

const inputRef = useRef<HTMLInputElement>(null);

function focusInput() {
    inputRef.current?.focus();   // current יכול להיות null לפני הרינדור
}

return <input ref={inputRef} />;

Context עם טיפוס

type Theme = 'light' | 'dark';
type ThemeContextValue = { theme: Theme; toggle: () => void };

const ThemeContext = createContext<ThemeContextValue | null>(null);

// הוק שמגן מפני שימוש מחוץ ל-Provider
function useTheme(): ThemeContextValue {
    const value = useContext(ThemeContext);
    if (!value) throw new Error('useTheme חייב להיות בתוך ThemeProvider');
    return value;
}

function ThemeProvider({ children }: { children: ReactNode }) {
    const [theme, setTheme] = useState<Theme>('light');
    const toggle = () => setTheme((current) => (current === 'light' ? 'dark' : 'light'));
    return <ThemeContext value={{ theme, toggle }}>{children}</ThemeContext>;
}

קומפוננטה גנרית

type ListProps<T> = {
    items: T[];
    renderItem: (item: T) => ReactNode;
    getKey: (item: T) => string | number;
};

function List<T>({ items, renderItem, getKey }: ListProps<T>) {
    return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}

<List items={products} getKey={(p) => p.id} renderItem={(p) => p.name} />
// p מוסק כ-Product, בלי לכתוב כלום

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. איך נותנים טיפוס ל-state שמתחיל ב-null ויהיה User?

    1. useState(null)
    2. useState<User>()
    3. useState<User | null>(null)
    4. useState(User)
    הצגת התשובה

    תשובה ג. מ-null לבד TypeScript לא יכולה לדעת מה יבוא אחר כך.

  2. איזה טיפוס מתאים ל-children?

    1. any
    2. JSX
    3. ReactNode
    4. string
    הצגת התשובה

    תשובה ג. ReactNode כולל כל מה ש-React יודעת לרנדר.