Добавляем Callsite Revalidation Opt-out в React Router

#react#react-router#open-source#performance

TL;DR

Разбираем реальный кейс контрибьюта в React Router — добавляем опцию отключения revalidation при навигации. Показываем процесс от issue до PR, разбираем архитектурные решения и их влияние на performance.

Введение: зачем это нужно

В React Router 6.4+ появилась концепция revalidation — при навигации все loader’ы текущей страницы перезапускаются для валидации данных. Это полезно для data consistency, но иногда избыточно. Разбираем, как добавить opt-out механизм без breaking changes.

Проблема и анализ кода

Исходная проблема: при переходе между nested routes все parent loader’ы выполняются повторно:

// До изменений
<Route path="/" loader={rootLoader}>
  <Route path="projects" loader={projectsLoader} />
</Route>

При переходе /projects/1/projects/2 выполняются оба loader’а, хотя часто достаточно только projectsLoader.

Где живет логика revalidation

Ключевые файлы в кодовой базе:

Логика revalidation находится в router.ts, метод validateNavigation:

function validateNavigation(
  nextMatches: AgnosticDataRouteMatch[],
  currentMatches: AgnosticDataRouteMatch[]
): boolean {
  // По умолчанию revalidate при любом изменении URL
  return nextMatches.some(
    (nextMatch, index) =>
      !currentMatches[index] ||
      nextMatch.pathname !== currentMatches[index].pathname
  );
}

Реализация opt-out

Добавляем новый параметр revalidateOnNavigation в RouteObject:

  1. Расширяем типы:
interface RouteObject {
  // ...
  revalidateOnNavigation?: boolean | ((nextUrl: URL, currentUrl: URL) => boolean);
}
  1. Модифицируем validateNavigation:
function validateNavigation(
  nextMatches: AgnosticDataRouteMatch[],
  currentMatches: AgnosticDataRouteMatch[],
  routes: AgnosticDataRouteObject[]
): boolean {
  return nextMatches.some((nextMatch, index) => {
    const route = routes[index];
    const shouldRevalidate = route.revalidateOnNavigation ?? true;
    
    if (typeof shouldRevalidate === 'function') {
      return shouldRevalidate(
        new URL(nextMatch.pathname, window.location.origin),
        new URL(currentMatches[index]?.pathname || '/', window.location.origin)
      );
    }
    
    return shouldRevalidate && (
      !currentMatches[index] ||
      nextMatch.pathname !== currentMatches[index].pathname
    );
  });
}

Пример использования

Опция может быть трех видов:

  1. Полное отключение:
<Route 
  path="/"
  loader={heavyLoader}
  revalidateOnNavigation={false}
/>
  1. Условное отключение:
<Route
  path="/dashboard"
  loader={dashboardLoader}
  revalidateOnNavigation={(nextUrl, currentUrl) => 
    nextUrl.pathname !== currentUrl.pathname
  }
/>

Тестирование изменений

Добавляем unit-тесты для новой функциональности:

describe('revalidateOnNavigation', () => {
  it('should skip revalidation when false', () => {
    const loader = jest.fn();
    const router = createTestRouter(
      createRoutesFromElements(
        <Route path="/" loader={loader} revalidateOnNavigation={false}>
          <Route path="child" />
        </Route>
      )
    );
    
    router.navigate('/child');
    expect(loader).toHaveBeenCalledTimes(1); // Только initial load
  });
});

Перформанс-импликации

Нагрузочное тестирование показало:

Выводы

  1. API Design Matters: Добавление опций должно быть backward compatible
  2. Performance Wins: Даже небольшие оптимизации дают заметный эффект
  3. Контрибьютить реально: React Router — хорошо структурированный код с понятными contribution guidelines

Для senior разработчиков такой контрибьют — хороший способ глубже понять routing в React и внести вклад в экосистему. Главное — начинать с small scope changes и активно коммуницировать с мейнтейнерами.


Источник: https://programmingarehard.com/2026/04/11/contributing-to-react-router.html/