Предпочтительный способ — функция с явным типом props (без React.FC):
// Рекомендуемый способ — явная типизация props:
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
disabled?: boolean
children: React.ReactNode
}
function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
)
}
// React.FC<Props> — устаревший стиль, имеет нюансы:
// - неявно добавлял children (исправлено в React 18)
// - несколько хуже работает с generics
const Button2: React.FC<ButtonProps> = ({ label, onClick }) => { ... }interface CardProps {
title: string
children: React.ReactNode // строка, JSX, массив, null — всё подходит
}
// Альтернативы:
// React.ReactElement — только JSX элементы
// JSX.Element — синоним ReactElement
// string — только строкиfunction Form() {
// Кнопка
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
console.log('clicked')
}
// Инпут
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
}
// Форма
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>Submit</button>
</form>
)
}// useState — тип выводится из начального значения
const [name, setName] = useState('') // string
const [count, setCount] = useState(0) // number
const [user, setUser] = useState<User | null>(null) // явный тип нужен
// useRef — DOM элемент
const inputRef = useRef<HTMLInputElement>(null)
// inputRef.current может быть null до монтирования
// useRef — изменяемое значение (не DOM)
const timerRef = useRef<number | null>(null)
timerRef.current = setTimeout(() => {}, 1000)
// useReducer с типами
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number }
interface State { count: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { count: state.count + 1 }
case 'decrement': return { count: state.count - 1 }
case 'reset': return { count: action.payload }
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 })
dispatch({ type: 'reset', payload: 10 }) // payload обязателен!function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [stored, setStored] = useState<T>(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = (value: T) => {
setStored(value)
localStorage.setItem(key, JSON.stringify(value))
}
return [stored, setValue]
}
// Использование:
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')interface ThemeContextType {
theme: 'light' | 'dark'
toggle: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
function useTheme(): ThemeContextType {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
return ctx
}interface InputProps {
placeholder?: string
value: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ placeholder, value, onChange }, ref) => (
<input ref={ref} placeholder={placeholder} value={value} onChange={onChange} />
)
)React-like система в чистом JS: createElement, useState хук и минимальный рендер — демонстрация как хуки работают внутри
// Реализуем упрощённый React в чистом JS — чтобы понять
// как работают хуки и почему TypeScript так важен в React.
// --- Минимальный React-like движок ---
let currentComponent = null
let hookIndex = 0
const componentStates = new Map()
function useState(initialValue) {
const component = currentComponent
const index = hookIndex++
if (!componentStates.has(component)) {
componentStates.set(component, [])
}
const states = componentStates.get(component)
if (states[index] === undefined) {
states[index] = initialValue
}
const setState = (newValue) => {
states[index] = typeof newValue === 'function'
? newValue(states[index])
: newValue
// В реальном React здесь был бы re-render
console.log(` [setState] ${component}.hook[${index}] = ${JSON.stringify(states[index])}`)
}
return [states[index], setState]
}
function runComponent(name, componentFn, props = {}) {
currentComponent = name
hookIndex = 0
console.log(`\n--- Рендер компонента: ${name} ---`)
const result = componentFn(props)
currentComponent = null
return result
}
// --- Компоненты ---
function Counter({ initialCount = 0 }) {
// TypeScript: useState<number>(initialCount)
const [count, setCount] = useState(initialCount)
// TypeScript: onClick: React.MouseEventHandler
const increment = () => setCount(c => c + 1)
const decrement = () => setCount(c => c - 1)
const reset = () => setCount(initialCount)
console.log(` count = ${count}`)
return { count, increment, decrement, reset }
}
function Form() {
// TypeScript: useState<string>('')
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [errors, setErrors] = useState({})
// TypeScript: (e: React.ChangeEvent<HTMLInputElement>) => void
const handleNameChange = (value) => setName(value)
const handleEmailChange = (value) => setEmail(value)
const validate = () => {
const newErrors = {}
if (!name) newErrors.name = 'Имя обязательно'
if (!email.includes('@')) newErrors.email = 'Некорректный email'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
console.log(` name="${name}", email="${email}"`)
return { name, email, errors, handleNameChange, handleEmailChange, validate }
}
// --- Демонстрация ---
console.log('=== Counter ===')
const counter = runComponent('Counter', Counter, { initialCount: 5 })
counter.increment()
counter.increment()
counter.decrement()
// Второй рендер — состояние сохранено
const counter2 = runComponent('Counter', Counter, { initialCount: 5 })
console.log('После двух increment и одного decrement, count =', componentStates.get('Counter')[0])
console.log('\n=== Form ===')
const form = runComponent('Form', Form)
form.handleNameChange('Алексей')
form.handleEmailChange('alex')
const form2 = runComponent('Form', Form)
console.log('Валидация (email без @):', form2.validate())
form.handleEmailChange('alex@example.com')
const form3 = runComponent('Form', Form)
console.log('Валидация (корректный email):', form3.validate())Предпочтительный способ — функция с явным типом props (без React.FC):
// Рекомендуемый способ — явная типизация props:
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
disabled?: boolean
children: React.ReactNode
}
function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
)
}
// React.FC<Props> — устаревший стиль, имеет нюансы:
// - неявно добавлял children (исправлено в React 18)
// - несколько хуже работает с generics
const Button2: React.FC<ButtonProps> = ({ label, onClick }) => { ... }interface CardProps {
title: string
children: React.ReactNode // строка, JSX, массив, null — всё подходит
}
// Альтернативы:
// React.ReactElement — только JSX элементы
// JSX.Element — синоним ReactElement
// string — только строкиfunction Form() {
// Кнопка
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
console.log('clicked')
}
// Инпут
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
}
// Форма
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>Submit</button>
</form>
)
}// useState — тип выводится из начального значения
const [name, setName] = useState('') // string
const [count, setCount] = useState(0) // number
const [user, setUser] = useState<User | null>(null) // явный тип нужен
// useRef — DOM элемент
const inputRef = useRef<HTMLInputElement>(null)
// inputRef.current может быть null до монтирования
// useRef — изменяемое значение (не DOM)
const timerRef = useRef<number | null>(null)
timerRef.current = setTimeout(() => {}, 1000)
// useReducer с типами
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number }
interface State { count: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { count: state.count + 1 }
case 'decrement': return { count: state.count - 1 }
case 'reset': return { count: action.payload }
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 })
dispatch({ type: 'reset', payload: 10 }) // payload обязателен!function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [stored, setStored] = useState<T>(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = (value: T) => {
setStored(value)
localStorage.setItem(key, JSON.stringify(value))
}
return [stored, setValue]
}
// Использование:
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')interface ThemeContextType {
theme: 'light' | 'dark'
toggle: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
function useTheme(): ThemeContextType {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
return ctx
}interface InputProps {
placeholder?: string
value: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ placeholder, value, onChange }, ref) => (
<input ref={ref} placeholder={placeholder} value={value} onChange={onChange} />
)
)React-like система в чистом JS: createElement, useState хук и минимальный рендер — демонстрация как хуки работают внутри
// Реализуем упрощённый React в чистом JS — чтобы понять
// как работают хуки и почему TypeScript так важен в React.
// --- Минимальный React-like движок ---
let currentComponent = null
let hookIndex = 0
const componentStates = new Map()
function useState(initialValue) {
const component = currentComponent
const index = hookIndex++
if (!componentStates.has(component)) {
componentStates.set(component, [])
}
const states = componentStates.get(component)
if (states[index] === undefined) {
states[index] = initialValue
}
const setState = (newValue) => {
states[index] = typeof newValue === 'function'
? newValue(states[index])
: newValue
// В реальном React здесь был бы re-render
console.log(` [setState] ${component}.hook[${index}] = ${JSON.stringify(states[index])}`)
}
return [states[index], setState]
}
function runComponent(name, componentFn, props = {}) {
currentComponent = name
hookIndex = 0
console.log(`\n--- Рендер компонента: ${name} ---`)
const result = componentFn(props)
currentComponent = null
return result
}
// --- Компоненты ---
function Counter({ initialCount = 0 }) {
// TypeScript: useState<number>(initialCount)
const [count, setCount] = useState(initialCount)
// TypeScript: onClick: React.MouseEventHandler
const increment = () => setCount(c => c + 1)
const decrement = () => setCount(c => c - 1)
const reset = () => setCount(initialCount)
console.log(` count = ${count}`)
return { count, increment, decrement, reset }
}
function Form() {
// TypeScript: useState<string>('')
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [errors, setErrors] = useState({})
// TypeScript: (e: React.ChangeEvent<HTMLInputElement>) => void
const handleNameChange = (value) => setName(value)
const handleEmailChange = (value) => setEmail(value)
const validate = () => {
const newErrors = {}
if (!name) newErrors.name = 'Имя обязательно'
if (!email.includes('@')) newErrors.email = 'Некорректный email'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
console.log(` name="${name}", email="${email}"`)
return { name, email, errors, handleNameChange, handleEmailChange, validate }
}
// --- Демонстрация ---
console.log('=== Counter ===')
const counter = runComponent('Counter', Counter, { initialCount: 5 })
counter.increment()
counter.increment()
counter.decrement()
// Второй рендер — состояние сохранено
const counter2 = runComponent('Counter', Counter, { initialCount: 5 })
console.log('После двух increment и одного decrement, count =', componentStates.get('Counter')[0])
console.log('\n=== Form ===')
const form = runComponent('Form', Form)
form.handleNameChange('Алексей')
form.handleEmailChange('alex')
const form2 = runComponent('Form', Form)
console.log('Валидация (email без @):', form2.validate())
form.handleEmailChange('alex@example.com')
const form3 = runComponent('Form', Form)
console.log('Валидация (корректный email):', form3.validate())Реализуй `createEmitter()` — типизированный EventEmitter. Методы: `on(event, handler)` — подписка на событие, `emit(event, data)` — вызов всех подписчиков события с данными, `off(event, handler)` — отписка конкретного обработчика. Тесты проверяют подписку, вызов, отписку.
Используй new Map() для хранения. В on(): listeners.get(event) возвращает Set, добавляй через .add(handler). В off(): listeners.get(event)?.delete(handler). В emit(): listeners.get(event)?.forEach(handler => handler(data)).
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке