Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Реализация собственного useState
Реализация useState — один из лучших способов понять, как работают React hooks изнутри. Хотя полная реализация очень сложная, мы можем создать упрощённую версию, которая демонстрирует основной принцип.
Основная идея
React использует замыкания и массив для хранения состояния каждого компонента. Каждый вызов setState вызывает ре-рендер компонента, при котором индекс хука позволяет получить правильное значение из массива состояния.
Простая реализация
let componentHooks = [];
let hookIndex = 0;
function useState(initialValue) {
const index = hookIndex;
hookIndex++;
// Инициализируем значение, если это первый рендер
if (componentHooks[index] === undefined) {
const initial = typeof initialValue === "function" ? initialValue() : initialValue;
componentHooks[index] = initial;
}
// Функция для обновления состояния
const setState = (newValue) => {
const value = typeof newValue === "function"
? newValue(componentHooks[index])
: newValue;
if (value !== componentHooks[index]) {
componentHooks[index] = value;
render(); // ре-рендер всего приложения
}
};
return [componentHooks[index], setState];
}
// Пример использования
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState("Мир");
return {
render: () => `Привет, ${name}! Счётчик: ${count}`,
increment: () => setCount(count + 1),
setName: (newName) => setName(newName),
};
}
Проблема: Эта реализация имеет критический баг — индекс хука должен быть сброшен перед каждым ре-рендером. Вот исправленная версия:
Улучшенная реализация
let components = new WeakMap(); // Сохраняем состояние для каждого компонента
function createComponent(componentFn) {
let hooks = [];
let hookIndex = 0;
let isMounted = false;
function useState(initialValue) {
const index = hookIndex;
hookIndex++;
// Инициализация при первом монтировании
if (!isMounted) {
const initial = typeof initialValue === "function" ? initialValue() : initialValue;
hooks[index] = initial;
}
const setState = (newValue) => {
const value = typeof newValue === "function"
? newValue(hooks[index])
: newValue;
// Не обновляем если значение не изменилось (оптимизация)
if (value !== hooks[index]) {
hooks[index] = value;
render(); // ре-рендер
}
};
return [hooks[index], setState];
}
function render() {
hookIndex = 0; // Сбрасываем индекс для следующего рендера!
isMounted = true;
return componentFn(useState);
}
return { render, hooks };
}
// Использование
const counter = createComponent((useState) => {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
return {
display: `Count: ${count}, Text: ${text}`,
increment: () => setCount(count + 1),
decrement: () => setCount(count - 1),
setText: (newText) => setText(newText),
};
});
console.log(counter.render().display); // Count: 0, Text:
counter.render().increment();
console.log(counter.render().display); // Count: 1, Text:
Более реалистичная реализация (с модулем)
class HookSystem {
constructor() {
this.currentComponent = null;
this.hookIndex = 0;
}
useState(initialValue) {
const component = this.currentComponent;
const index = this.hookIndex++;
// Инициализируем состояние компонента
if (!component.hooks) {
component.hooks = [];
}
if (component.hooks[index] === undefined) {
const value = typeof initialValue === "function" ? initialValue() : initialValue;
component.hooks[index] = { state: value };
}
const setState = (action) => {
const hook = component.hooks[index];
const newState = typeof action === "function" ? action(hook.state) : action;
// Сравниваем по значению (поверхностное сравнение)
if (!Object.is(newState, hook.state)) {
hook.state = newState;
this.scheduleRender(component);
}
};
return [component.hooks[index].state, setState];
}
scheduleRender(component) {
// В реальном React это более сложно — используется приоритет, батчинг и т.д.
this.hookIndex = 0;
component.render();
}
runComponent(component) {
this.currentComponent = component;
this.hookIndex = 0;
return component.componentFn();
}
}
// Использование
const hookSystem = new HookSystem();
const MyComponent = {
componentFn: function() {
const [count, setCount] = hookSystem.useState(0);
const [name, setName] = hookSystem.useState("Alice");
return {
state: { count, name },
handlers: {
increment: () => setCount(c => c + 1),
changeName: (n) => setName(n),
}
};
},
render() {
const result = hookSystem.runComponent(this);
console.log(`${result.state.name}: ${result.state.count}`);
}
};
MyComponent.render(); // Alice: 0
Ключевые принципы, которые мы реализовали
- Замыкания — функция
setStateзакрывает переменнуюindexиhooks - Сброс индекса — перед каждым ре-рендером индекс обнуляется
- Ленивая инициализация — значение инициализируется только при первом рендере
- Функциональное обновление —
setStateпринимает функцию для вычисления нового значения - Батчинг обновлений — всё перерисовывается при изменении
Почему реальный React сложнее
- Fiber архитектура — React использует linked list для управления обновлениями
- Приоритеты — одни обновления важнее других
- Concurrent mode — рендер можно прерывать и возобновлять
- Dependency tracking —
useEffectотслеживает зависимости - Error boundaries — обработка ошибок при рендере
- Strict mode — двойной вызов эффектов для отладки
Правило вызова useState
Хотя мы это не реализовали явно, важно помнить:
// ❌ НЕПРАВИЛЬНО — условный вызов useState
function BadComponent() {
if (someCondition) {
const [state, setState] = useState(0); // может меняться порядок!
}
return null;
}
// ✅ ПРАВИЛЬНО — всегда вызывай hooks на верхнем уровне
function GoodComponent() {
const [state, setState] = useState(0);
if (someCondition) {
// используй состояние здесь
}
return null;
}
Резюме
Реализация собственного useState показывает, как React использует:
- Замыкания для инкапсуляции состояния
- Индекс хука для определения какое состояние получить
- Сброс индекса перед каждым ре-рендером
- Функции обновления для контролируемого изменения состояния
Это упрощённая версия, но она демонстрирует основные механизмы, которые стоят за магией React hooks.