createContext принимает дженерик-параметр для типа значения:
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
// Вариант 1: с начальным значением (undefined потребует проверки)
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
// Вариант 2: с fake начальным значением (небезопасно, но удобно)
const ThemeContext = createContext<ThemeContextType>({} as ThemeContextType)Лучший подход — передавать undefined как начальное значение и бросать ошибку при использовании вне провайдера:
interface AuthContextType {
user: User | null
login: (credentials: Credentials) => Promise<void>
logout: () => void
isLoading: boolean
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
// Кастомный хук с проверкой:
function useAuth(): AuthContextType {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}interface AuthProviderProps {
children: React.ReactNode
initialUser?: User | null
}
function AuthProvider({ children, initialUser = null }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(initialUser)
const [isLoading, setIsLoading] = useState(false)
const login = async (credentials: Credentials) => {
setIsLoading(true)
try {
const user = await authApi.login(credentials)
setUser(user)
} finally {
setIsLoading(false)
}
}
const logout = () => {
setUser(null)
authApi.logout()
}
const value: AuthContextType = { user, login, logout, isLoading }
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}// Переиспользуемый паттерн для любого контекста
function createTypedContext<T>(name: string) {
const Context = createContext<T | undefined>(undefined)
function useTypedContext(): T {
const ctx = useContext(Context)
if (ctx === undefined) {
throw new Error(`use${name} must be used within ${name}Provider`)
}
return ctx
}
return [Context.Provider, useTypedContext] as const
}
// Использование:
interface CartContextType {
items: CartItem[]
addItem: (item: CartItem) => void
total: number
}
const [CartProvider, useCart] = createTypedContext<CartContextType>('Cart')function AppProviders({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
{children}
</CartProvider>
</ThemeProvider>
</AuthProvider>
)
}type AppAction =
| { type: 'SET_USER'; payload: User }
| { type: 'SET_THEME'; payload: 'light' | 'dark' }
| { type: 'RESET' }
interface AppState {
user: User | null
theme: 'light' | 'dark'
}
interface AppContextType {
state: AppState
dispatch: React.Dispatch<AppAction>
}
const AppContext = createContext<AppContextType | undefined>(undefined)// Разделяем на два контекста чтобы избежать лишних ре-рендеров
const ThemeStateContext = createContext<'light' | 'dark'>('light')
const ThemeDispatchContext = createContext<() => void>(() => {})
function useThemeState() { return useContext(ThemeStateContext) }
function useThemeDispatch() { return useContext(ThemeDispatchContext) }Context API симуляция в чистом JS: createContext, Provider, useContext — полная реализация паттернов из React TypeScript
// Реализуем Context API с нуля — как работает React Context внутри.
// В TypeScript каждый контекст строго типизирован.
// --- Минимальный Context API ---
let contextRegistry = new Map()
let currentConsumer = null
function createContext(defaultValue) {
const contextId = Symbol('context')
const Context = {
_id: contextId,
_currentValue: defaultValue,
Provider: null, // будет заполнено ниже
}
Context.Provider = function(value, children) {
const prev = Context._currentValue
Context._currentValue = value
const result = children()
Context._currentValue = prev // восстанавливаем (вложенные провайдеры)
return result
}
return Context
}
function useContext(Context) {
return Context._currentValue
}
// --- Типизированный паттерн createTypedContext ---
// В TS: function createTypedContext<T>(name: string): [Provider, () => T]
function createTypedContext(name, defaultValue = undefined) {
const Context = createContext(defaultValue)
function useTypedContext() {
const value = useContext(Context)
if (value === undefined) {
throw new Error(`use${name} must be used within ${name}Provider`)
}
return value
}
return { Context, useTypedContext }
}
// --- Auth Context ---
// TS: interface AuthContextType { user: User | null; login: ...; logout: ... }
const { Context: AuthContext, useTypedContext: useAuth } = createTypedContext('Auth')
function AuthProvider(initialUser, children) {
let user = initialUser || null
let isLoading = false
const listeners = new Set()
const notify = () => listeners.forEach(fn => fn({ user, isLoading }))
const value = {
get user() { return user },
get isLoading() { return isLoading },
login(credentials) {
isLoading = true
notify()
// Симуляция async
setTimeout(() => {
user = { id: 1, name: credentials.username, role: 'user' }
isLoading = false
notify()
}, 50)
},
logout() {
user = null
notify()
},
subscribe(fn) {
listeners.add(fn)
return () => listeners.delete(fn)
}
}
return AuthContext.Provider(value, children)
}
// --- Theme Context ---
const { Context: ThemeContext, useTypedContext: useTheme } = createTypedContext('Theme')
function ThemeProvider(initialTheme, children) {
let theme = initialTheme || 'light'
const value = {
get theme() { return theme },
toggleTheme() {
theme = theme === 'light' ? 'dark' : 'light'
console.log(' [ThemeContext] theme changed to:', theme)
},
setTheme(t) { theme = t }
}
return ThemeContext.Provider(value, children)
}
// --- Cart Context ---
const { Context: CartContext, useTypedContext: useCart } = createTypedContext('Cart')
function CartProvider(children) {
const items = []
const value = {
get items() { return [...items] },
get total() { return items.reduce((sum, item) => sum + item.price * item.quantity, 0) },
addItem(item) {
const existing = items.find(i => i.id === item.id)
if (existing) {
existing.quantity += item.quantity
} else {
items.push({ ...item })
}
},
removeItem(id) {
const idx = items.findIndex(i => i.id === id)
if (idx >= 0) items.splice(idx, 1)
},
clear() { items.length = 0 }
}
return CartContext.Provider(value, children)
}
// --- Composition провайдеров (как AppProviders в React) ---
function AppProviders(children) {
return AuthProvider(null,
() => ThemeProvider('light',
() => CartProvider(children)
)
)
}
// --- Демонстрация ---
console.log('=== Auth Context ===')
AppProviders(() => {
const auth = useAuth()
const theme = useTheme()
const cart = useCart()
console.log('initial user:', auth.user)
console.log('initial theme:', theme.theme)
auth.login({ username: 'Алексей', password: 'secret' })
// user устанавливается через setTimeout, поэтому проверим позже
console.log('\n=== Theme Context ===')
console.log('current theme:', theme.theme)
theme.toggleTheme()
console.log('after toggle:', theme.theme)
theme.toggleTheme()
console.log('after second toggle:', theme.theme)
console.log('\n=== Cart Context ===')
cart.addItem({ id: 1, name: 'Товар 1', price: 100, quantity: 2 })
cart.addItem({ id: 2, name: 'Товар 2', price: 250, quantity: 1 })
cart.addItem({ id: 1, name: 'Товар 1', price: 100, quantity: 1 }) // добавляется к существующему
console.log('items:', cart.items.map(i => `${i.name} x${i.quantity}`))
console.log('total:', cart.total) // 100*3 + 250*1 = 550
cart.removeItem(2)
console.log('after removeItem(2), items:', cart.items.length)
console.log('after removeItem(2), total:', cart.total) // 300
})
// --- Ошибка вне провайдера ---
console.log('\n=== Ошибка без провайдера ===')
try {
useAuth() // выбросит ошибку
} catch (e) {
console.log('Ожидаемая ошибка:', e.message)
}createContext принимает дженерик-параметр для типа значения:
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
// Вариант 1: с начальным значением (undefined потребует проверки)
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
// Вариант 2: с fake начальным значением (небезопасно, но удобно)
const ThemeContext = createContext<ThemeContextType>({} as ThemeContextType)Лучший подход — передавать undefined как начальное значение и бросать ошибку при использовании вне провайдера:
interface AuthContextType {
user: User | null
login: (credentials: Credentials) => Promise<void>
logout: () => void
isLoading: boolean
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
// Кастомный хук с проверкой:
function useAuth(): AuthContextType {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}interface AuthProviderProps {
children: React.ReactNode
initialUser?: User | null
}
function AuthProvider({ children, initialUser = null }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(initialUser)
const [isLoading, setIsLoading] = useState(false)
const login = async (credentials: Credentials) => {
setIsLoading(true)
try {
const user = await authApi.login(credentials)
setUser(user)
} finally {
setIsLoading(false)
}
}
const logout = () => {
setUser(null)
authApi.logout()
}
const value: AuthContextType = { user, login, logout, isLoading }
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}// Переиспользуемый паттерн для любого контекста
function createTypedContext<T>(name: string) {
const Context = createContext<T | undefined>(undefined)
function useTypedContext(): T {
const ctx = useContext(Context)
if (ctx === undefined) {
throw new Error(`use${name} must be used within ${name}Provider`)
}
return ctx
}
return [Context.Provider, useTypedContext] as const
}
// Использование:
interface CartContextType {
items: CartItem[]
addItem: (item: CartItem) => void
total: number
}
const [CartProvider, useCart] = createTypedContext<CartContextType>('Cart')function AppProviders({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
{children}
</CartProvider>
</ThemeProvider>
</AuthProvider>
)
}type AppAction =
| { type: 'SET_USER'; payload: User }
| { type: 'SET_THEME'; payload: 'light' | 'dark' }
| { type: 'RESET' }
interface AppState {
user: User | null
theme: 'light' | 'dark'
}
interface AppContextType {
state: AppState
dispatch: React.Dispatch<AppAction>
}
const AppContext = createContext<AppContextType | undefined>(undefined)// Разделяем на два контекста чтобы избежать лишних ре-рендеров
const ThemeStateContext = createContext<'light' | 'dark'>('light')
const ThemeDispatchContext = createContext<() => void>(() => {})
function useThemeState() { return useContext(ThemeStateContext) }
function useThemeDispatch() { return useContext(ThemeDispatchContext) }Context API симуляция в чистом JS: createContext, Provider, useContext — полная реализация паттернов из React TypeScript
// Реализуем Context API с нуля — как работает React Context внутри.
// В TypeScript каждый контекст строго типизирован.
// --- Минимальный Context API ---
let contextRegistry = new Map()
let currentConsumer = null
function createContext(defaultValue) {
const contextId = Symbol('context')
const Context = {
_id: contextId,
_currentValue: defaultValue,
Provider: null, // будет заполнено ниже
}
Context.Provider = function(value, children) {
const prev = Context._currentValue
Context._currentValue = value
const result = children()
Context._currentValue = prev // восстанавливаем (вложенные провайдеры)
return result
}
return Context
}
function useContext(Context) {
return Context._currentValue
}
// --- Типизированный паттерн createTypedContext ---
// В TS: function createTypedContext<T>(name: string): [Provider, () => T]
function createTypedContext(name, defaultValue = undefined) {
const Context = createContext(defaultValue)
function useTypedContext() {
const value = useContext(Context)
if (value === undefined) {
throw new Error(`use${name} must be used within ${name}Provider`)
}
return value
}
return { Context, useTypedContext }
}
// --- Auth Context ---
// TS: interface AuthContextType { user: User | null; login: ...; logout: ... }
const { Context: AuthContext, useTypedContext: useAuth } = createTypedContext('Auth')
function AuthProvider(initialUser, children) {
let user = initialUser || null
let isLoading = false
const listeners = new Set()
const notify = () => listeners.forEach(fn => fn({ user, isLoading }))
const value = {
get user() { return user },
get isLoading() { return isLoading },
login(credentials) {
isLoading = true
notify()
// Симуляция async
setTimeout(() => {
user = { id: 1, name: credentials.username, role: 'user' }
isLoading = false
notify()
}, 50)
},
logout() {
user = null
notify()
},
subscribe(fn) {
listeners.add(fn)
return () => listeners.delete(fn)
}
}
return AuthContext.Provider(value, children)
}
// --- Theme Context ---
const { Context: ThemeContext, useTypedContext: useTheme } = createTypedContext('Theme')
function ThemeProvider(initialTheme, children) {
let theme = initialTheme || 'light'
const value = {
get theme() { return theme },
toggleTheme() {
theme = theme === 'light' ? 'dark' : 'light'
console.log(' [ThemeContext] theme changed to:', theme)
},
setTheme(t) { theme = t }
}
return ThemeContext.Provider(value, children)
}
// --- Cart Context ---
const { Context: CartContext, useTypedContext: useCart } = createTypedContext('Cart')
function CartProvider(children) {
const items = []
const value = {
get items() { return [...items] },
get total() { return items.reduce((sum, item) => sum + item.price * item.quantity, 0) },
addItem(item) {
const existing = items.find(i => i.id === item.id)
if (existing) {
existing.quantity += item.quantity
} else {
items.push({ ...item })
}
},
removeItem(id) {
const idx = items.findIndex(i => i.id === id)
if (idx >= 0) items.splice(idx, 1)
},
clear() { items.length = 0 }
}
return CartContext.Provider(value, children)
}
// --- Composition провайдеров (как AppProviders в React) ---
function AppProviders(children) {
return AuthProvider(null,
() => ThemeProvider('light',
() => CartProvider(children)
)
)
}
// --- Демонстрация ---
console.log('=== Auth Context ===')
AppProviders(() => {
const auth = useAuth()
const theme = useTheme()
const cart = useCart()
console.log('initial user:', auth.user)
console.log('initial theme:', theme.theme)
auth.login({ username: 'Алексей', password: 'secret' })
// user устанавливается через setTimeout, поэтому проверим позже
console.log('\n=== Theme Context ===')
console.log('current theme:', theme.theme)
theme.toggleTheme()
console.log('after toggle:', theme.theme)
theme.toggleTheme()
console.log('after second toggle:', theme.theme)
console.log('\n=== Cart Context ===')
cart.addItem({ id: 1, name: 'Товар 1', price: 100, quantity: 2 })
cart.addItem({ id: 2, name: 'Товар 2', price: 250, quantity: 1 })
cart.addItem({ id: 1, name: 'Товар 1', price: 100, quantity: 1 }) // добавляется к существующему
console.log('items:', cart.items.map(i => `${i.name} x${i.quantity}`))
console.log('total:', cart.total) // 100*3 + 250*1 = 550
cart.removeItem(2)
console.log('after removeItem(2), items:', cart.items.length)
console.log('after removeItem(2), total:', cart.total) // 300
})
// --- Ошибка вне провайдера ---
console.log('\n=== Ошибка без провайдера ===')
try {
useAuth() // выбросит ошибку
} catch (e) {
console.log('Ожидаемая ошибка:', e.message)
}Реализуй `createStore(initialState, reducers)` — упрощённый Redux-подобный store. `reducers` — объект функций `{ [actionType]: (state, payload) => newState }`. Store возвращает: `getState()` — текущее состояние, `dispatch(type, payload)` — вызывает нужный reducer, `subscribe(listener)` — подписка на изменения (возвращает функцию отписки). При каждом dispatch все слушатели уведомляются.
Используй Set() или массив для listeners. В dispatch: state = reducer(state, payload); listeners.forEach(l => l(state)). subscribe возвращает () => listeners.delete(listener) (для Set) или filter (для массива). getState должен возвращать { ...state } чтобы нельзя было изменить напрямую.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке