Как подключить Yandex JS API 3 к Next.js?

Рейтинг: 0Ответов: 1Опубликовано: 27.07.2023

Видел, как подключать через CDN, но в документации не понятно как правильно подключать JS API 3 как библиотека?

Нужно подключить карту и на карте добавлять маркеры, получаемые из API и сделать функционал показа данных при нажатии на эти маркеры.

Ответы

▲ 1

Кратко, Яндекс-карты не совместимы с серверным режимом в Next. Поэтому их нужно вставлять через dynamic import с параметром ssr: false. Вот так:

page.tsx

// инициализация и использование карты внутри, включая Script, загружающий код карт
const DynamicPageContent = dynamic(async () => import('./DynamicPageContent'), {
  ssr: false,
});

export default function Page() {
  return <DynamicPageContent />;
}

Сам скрипт вставляется через компонент Script со стратегией strategy="beforeInteractive", что гарантирует то, что ваш код будет выполнен после загрузки скрипта.

DynamicPageContent.tsx

'use client';

import Script from 'next/script';

export function DynamicPageContent() {
  const apiKey = process.env.NEXT_PUBLIC_YMAPS_API_KEY;
  return (
    <>
      <Script src={`https://api-maps.yandex.ru/v3/?apikey=${apiKey}&lang=ru_RU`} strategy="beforeInteractive" />
      <Компонент_вставляющий_саму_карту />
    </>
  );  
}

Это приемлимый вариант, но если вы хотите сохранить отзывчивость next-а и не заставлять пользователя ждать (даже если это просто секунда), можно применить чуть более бессмысленно сложный вариант. Примерно такой:

(данный пример не использует react-обёртку, поскольку смысл её существования в текущей реализации автором не осилен; впрочем, пример для этого легко модифицируется)

types.ts

import * as ClustererModule from '@yandex/ymaps3-types/packages/clusterer';

export type YMaps = typeof ymaps3 & typeof ClustererModule;

context.ts

import React from 'react';

import type { DeferredResult } from '@/features/core/hooks/useDeferred';

import type { YMaps } from './types';

export const YMapsContext = React.createContext<DeferredResult<YMaps> | null>(
  null,
);

YMapsProvider.tsx

'use client';

import Script from 'next/script';
import React, { useEffect } from 'react';

import { useDeferred } from '@/features/core/hooks/useDeferred';
import { YMapsContext } from '@/features/map/cogs/YMapsProvider/context';

import { YMaps } from './types';

type Props = React.PropsWithChildren<{ apiKey?: string }>;

export function YMapsProvider({ children, apiKey }: Props) {
  const scriptDeferred = useDeferred<true>();
  const mapsDeferred = useDeferred<YMaps>();

  useEffect(() => {
    if (
      scriptDeferred.state === 'resolved' &&
      mapsDeferred.state === 'pending'
    ) {
      Promise.all([
        ymaps3.import('@yandex/ymaps3-clusterer@0.0.1'),
        ymaps3.ready,
      ]).then(
        ([clustererModule]) =>
          mapsDeferred.resolve({ ...ymaps3, ...clustererModule }),
        (error) => mapsDeferred.reject(error),
      );
    } else if (
      scriptDeferred.state === 'rejected' &&
      mapsDeferred.state === 'pending'
    ) {
      mapsDeferred.reject(scriptDeferred.error);
    }
  }, [scriptDeferred, mapsDeferred]);

  const handleScriptLoaded = () => {
    if (scriptDeferred.state === 'pending') {
      scriptDeferred.resolve(true);
    }
  };

  const handleScriptLoadingFailed = (error: Error) => {
    if (scriptDeferred.state === 'pending') {
      scriptDeferred.reject(error);
    }
  };

  return (
    <>
      <Script
        src={`https://api-maps.yandex.ru/v3/?apikey=${apiKey}&lang=en_RU`}
        onReady={handleScriptLoaded}
        onError={handleScriptLoadingFailed}
      />
      <YMapsContext.Provider value={mapsDeferred}>
        {children}
      </YMapsContext.Provider>
    </>
  );
}

useYMaps.ts

import { useContext } from 'react';

import { YMapsContext } from './context';

import type { YMaps } from './types';

export function useYMaps(): YMaps {
  const deferred = useContext(YMapsContext);
  if (!deferred) {
    throw new Error('useYMaps requires YMapsProvider installed');
  }
  if (deferred.state === 'resolved') {
    return deferred.result;
  }
  if (deferred.state === 'rejected') {
    throw deferred.error;
  }
  throw deferred.promise;
}

хелпер useDeferred.ts

import 'client-only';
import { useMemo, useRef, useState } from 'react';

type Deferred<T> = {
  promise: Promise<T>;
  resolve: (value: T) => void;
  reject: (error: Error) => void;
};

export type DeferredResult<T> =
  | { state: 'resolved'; result: T }
  | { state: 'rejected'; error: Error }
  | {
      state: 'pending';
      promise: Promise<T>;
      resolve: (value: T) => void;
      reject: (error: Error) => void;
    };

const Empty = Symbol('empty');

export function useDeferred<T>(): DeferredResult<T> {
  const deferred = useRef<Deferred<T> | null>(null);
  const [result, setResult] = useState<T | Symbol>(Empty);
  const [error, setError] = useState<Error | Symbol>(Empty);

  if (!deferred.current) {
    let deferredValue: Deferred<T> = {
      promise: Promise.resolve('temp' as unknown as T),
      resolve: () => {},
      reject: () => {},
    };

    deferredValue.promise = new Promise<T>((resolve, reject) => {
      deferredValue.resolve = resolve;
      deferredValue.reject = reject;
    });

    deferred.current = deferredValue;
  }

  const resolvedState = useMemo(() => {
    if (result === Empty) {
      return null;
    }
    return { state: 'resolved' as const, result: result as T };
  }, [result]);

  const rejectedState = useMemo(() => {
    if (error === Empty) {
      return null;
    }
    return { state: 'rejected' as const, error: error as Error };
  }, [error]);

  const pendingState = useMemo(
    () => ({
      state: 'pending' as const,
      promise: deferred.current!.promise,
      resolve: (value: T) => {
        setResult(value);
        deferred.current!.resolve(value);
      },
      reject: (error: Error) => {
        setError(error);
        deferred.current!.reject(error);
      },
    }),
    [],
  );

  return resolvedState || rejectedState || pendingState;
}

Далее в своих компонентах используете хук useYMaps, не забыв предварительно установить Suspense между провайдером и компонентом-пользователем карт.