← Назад к вопросам

Зачем нужны стабильные ссылки на функции?

2.0 Middle🔥 191 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Стабильные ссылки на функции в React

Стабильная ссылка на функцию — это функция, которая не меняется между ре-рендерингами компонента. Это важный момент в React для оптимизации производительности и правильного поведения эффектов.

Проблема: нестабильные функции

По умолчанию в React функция создаётся заново при каждом ре-рендеринге:

function Parent() {
  // Эта функция создаётся заново каждый ре-рендеринг!
  const handleClick = () => {
    console.log('clicked');
  };

  return <Child onClick={handleClick} />;
}

function Child({ onClick }: { onClick: () => void }) {
  useEffect(() => {
    // ПРОБЛЕМА: эффект запускается каждый ре-рендеринг родителя
    // потому что onClick функция новая!
    console.log('Effect runs');
  }, [onClick]); // onClick меняется -> эффект запускается

  return <button onClick={onClick}>Click me</button>;
}

Что происходит:

  1. Parent рендерится -> создаёт новую handleClick
  2. Передаёт её в Child
  3. Child видит новую функцию -> запускает useEffect
  4. useEffect может быть дорогой операцией (запрос на сервер, подписка на события)

В результате — бесконечный цикл обновлений.

Решение 1: useCallback

useCallback мемоизирует функцию, возвращая ту же ссылку, пока зависимости не изменились:

function Parent() {
  const [count, setCount] = useState(0);

  // Функция стабильна — один раз создана, переиспользуется
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []); // Зависимостей нет — функция создаётся один раз

  return (
    <>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </>
  );
}

function Child({ onClick }: { onClick: () => void }) {
  useEffect(() => {
    console.log('Effect runs only once'); // Запускается один раз!
  }, [onClick]); // onClick стабильна

  return <button onClick={onClick}>Click me</button>;
}

С зависимостями:

function Parent() {
  const [userId, setUserId] = useState(1);

  // Функция зависит от userId
  // Создаётся заново, когда userId меняется
  const handleLoadUser = useCallback(() => {
    fetch(`/api/users/${userId}`); // Использует текущий userId
  }, [userId]); // userId в зависимостях

  return (
    <>
      <Child onLoadUser={handleLoadUser} />
      <button onClick={() => setUserId(userId + 1)}>Next user</button>
    </>
  );
}

Решение 2: Встроенные функции (event handlers)

Для обработчиков событий в JSX можно использовать встроенные функции — React оптимизирует их:

function MyComponent() {
  const [count, setCount] = useState(0);

  // React оптимизирует это — функция может пересоздаваться,
  // но обработчик остаётся стабильным для Child
  return (
    <Child onClick={() => setCount(count + 1)} />
  );
}

Решение 3: Классовые компоненты и методы

В классах методы по умолчанию стабильны:

class MyComponent extends React.Component<any> {
  handleClick = () => { // Arrow function property — стабильна
    console.log('clicked');
  };

  render() {
    return <Child onClick={this.handleClick} />; // Всегда одна и та же ссылка
  }
}

Без стрелочной функции пришлось бы вручную привязывать:

class MyComponent extends React.Component<any> {
  constructor(props: any) {
    super(props);
    this.handleClick = this.handleClick.bind(this); // Привязка в конструкторе
  }

  handleClick() {
    console.log('clicked');
  }

  render() {
    return <Child onClick={this.handleClick} />;
  }
}

Когда стабильность ссылок критична

1. Оптимизация перерисовок с React.memo

const Button = React.memo(function Button({
  onClick,
  label,
}: {
  onClick: () => void;
  label: string;
}) {
  console.log('Button rendered'); // Сколько раз запускается?
  return <button onClick={onClick}>{label}</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // БЕЗ useCallback
  const handleOld = () => console.log('old');

  // С useCallback
  const handleNew = useCallback(() => {
    console.log('new');
  }, []);

  return (
    <>
      <Button onClick={handleOld} label="Old" />
      {/* "Button rendered" выведется каждый раз при ре-рендере Parent */}
      
      <Button onClick={handleNew} label="New" />
      {/* "Button rendered" выведется только один раз! */}
      
      <button onClick={() => setCount(count + 1)}>Trigger re-render</button>
    </>
  );
}

2. Зависимости в useEffect и других хуках

function DataFetcher({ userId }: { userId: number }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  // Этот fetchData функция нужна для useEffect
  const fetchData = useCallback(async () => {
    try {
      const response = await fetch(`/api/users/${userId}`);
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err);
    }
  }, [userId]); // Зависит от userId

  useEffect(() => {
    fetchData(); // Запускается, когда fetchData меняется (когда меняется userId)
  }, [fetchData]); // fetchData в зависимостях

  return <>...</>;
}

3. Подписки на события

function DataFetcher() {
  const [search, setSearch] = useState('');

  // БЕЗ useCallback — функция пересоздаётся каждый ре-рендеринг
  const handleSearch = (query: string) => {
    fetch(`/api/search?q=${query}`);
  };

  useEffect(() => {
    const timeout = setTimeout(() => handleSearch(search), 500);
    return () => clearTimeout(timeout);
  }, [search, handleSearch]); // handleSearch в зависимостях!
  // -> При каждом ре-рендере timeout переустанавливается -> плохо

  return <input onChange={(e) => setSearch(e.target.value)} />;
}

// Лучше:
function DataFetcher() {
  const [search, setSearch] = useState('');

  const handleSearch = useCallback((query: string) => {
    fetch(`/api/search?q=${query}`);
  }, []); // Зависит только от пустого массива

  useEffect(() => {
    const timeout = setTimeout(() => handleSearch(search), 500);
    return () => clearTimeout(timeout);
  }, [search, handleSearch]); // handleSearch стабильна -> useEffect запускается только при изменении search

  return <input onChange={(e) => setSearch(e.target.value)} />;
}

Производительность: когда useCallback помогает, а когда нет

Помогает useCallback

// Передача мемоизированного компонента
const ExpensiveChild = React.memo(({ onClick }) => {
  // Дорогой рендеринг — расчёты, большой список
  const items = Array.from({ length: 1000 }, (_, i) => i);
  return (
    <div>
      {items.map(item => <div key={item}>{item}</div>)}
      <button onClick={onClick}>Click</button>
    </div>
  );
});

function Parent() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []); // useCallback предотвращает ненужный рендеринг ExpensiveChild

  return <ExpensiveChild onClick={handleClick} />;
}

Может быть не нужен useCallback

// Простой компонент
function SimpleChild({ onClick }: { onClick: () => void }) {
  return <button onClick={onClick}>Click</button>; // Дешёвый рендеринг
}

function Parent() {
  // useCallback тут можно опустить — рендеринг SimpleChild дешёвый
  const handleClick = () => {
    console.log('clicked');
  };

  return <SimpleChild onClick={handleClick} />;
}

Best Practices

1. Используй useCallback для дорогих компонентов

const ExpensiveChart = React.memo(({ onDataUpdate }: any) => {
  // Дорогой рендеринг
  return <div>Chart</div>;
});

function Dashboard() {
  const handleDataUpdate = useCallback(() => {
    // Запрос на сервер
  }, []);

  return <ExpensiveChart onDataUpdate={handleDataUpdate} />;
}

2. Не переусложняй

function SimpleForm() {
  // useCallback не нужен — простой компонент
  return (
    <input
      onChange={(e) => {
        console.log(e.target.value);
      }}
    />
  );
}

3. Следи за зависимостями

const handler = useCallback(() => {
  console.log(userId); // userId должна быть в зависимостях!
}, [userId]); // ПРАВИЛЬНО

// const handler = useCallback(() => {
//   console.log(userId);
// }, []); // НЕПРАВИЛЬНО — будет старое значение

Альтернатива: useMemo для объектов

Стабильность нужна не только функциям, но и объектам:

function Parent() {
  // Объект пересоздаётся каждый ре-рендеринг
  const config = { fontSize: 14, color: 'red' };

  return <Child config={config} />; // Child перерендерится
}

// Лучше:
function Parent() {
  const config = useMemo(() => ({
    fontSize: 14,
    color: 'red',
  }), []); // Стабильный объект

  return <Child config={config} />; // Child не перерендерится
}

Заключение

Стабильные ссылки нужны для:

  1. Оптимизации React.memo — предотвратить лишние ре-рендеры
  2. Зависимостей хуков — useEffect, useMemo, useCallback сами
  3. Предсказуемого поведения — избежать бесконечных циклов
  4. Правильной работы с подписками — не переустанавливать слушатели

Как обеспечить стабильность:

  • useCallback для функций
  • useMemo для объектов и массивов
  • Классовые компоненты (методы стабильны по умолчанию)
  • Event handlers в JSX (React оптимизирует автоматически)