npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@proscom/prostore-react-router

v0.2.13

Published

Adapter for react-router and prostore. Allows subscription to query parameters and more

Downloads

23

Readme

prostore-react-router

Адаптер для react-router, связывающий его с prostore.

Основное назначение этого адаптера - упростить подписку компонентов на изменение квери-параметров.

Хранение фильтров и других параметров отображения страницы в квери-параметрах - частый кейс в наших проектах. Это позволяет пользователю например отправить ссылку другу или открыть ссылку в новой вкладке с полным сохранением контекста.

При реализации работы с квери-параметрами напрямую через react-router возникают две проблемы:

  1. Необходимость парсить квери-строку в каждом компоненте и сериализовать значения при записи в url

  2. При изменении одного из квери-параметров будут обновлены все компоненты, подписанные на роутер, даже если они не используют квери-параметр.

Для решения этих проблем и создан этот адаптер. Он предоставляет класс LocationStore, обеспечивающий хранение данных, их сериализацию и десериализацию; React-компонент LocationProvider, отвечающий за синхронизацию react-router и LocationStore; а также хук useLocationQuery, позволяющий подписать компонент на квери-парамтеры.

Использование

Сначала необходимо создать глобальный стор LocationStore и заинжектить его через ProstoreContext:

import { ProstoreContext } from '@proscom/prostore-react';
import { LocationStore, LocationProvider } from '@proscom/prostore-react-router';
import { createBrowserHistory } from 'history';
import { Router } from 'react-router';

// Создаем browserHistory для взаимодействия с браузерной навигацией
const appHistory = createBrowserHistory();

// Ключ по которому мы будем идентифицировать LocationStore в контексте
const STORE_LOCATION = 'location';

// Сериализаторы квери-параметров. Про это см. ниже
const queryTransformers = {};

// Создаем сам стор, передаем ему history и сериализаторы
const locationStore = new LocationStore({ history: appHistory, transformers: queryTransformers });

// Сохраняем все сторы для инжектирования
const stores = {
  [STORE_LOCATION]: locationStore
};

ReactDOM.render(
  // Рендерим контекст react-router с той же самой history, что и LocationStore
  <Router history={appHistory}>
    {/* Инжектим сторы в контекст */}
    <ProstoreContext.Provider value={stores}>
      {/* Рендерим LocationProvider для синхронизации react-router и STORE_LOCATION */}
      <LocationProvider storeOrName={STORE_LOCATION}>
        {/* Наконец, рендерим наше приложение */}
        <App />
      </LocationProvider>
    </ProstoreContext>
  </Router>
);

Теперь можно использовать стор через контекст через useStore. Тогда мы подписываемся на все изменения Location:

import { useStore } from '@proscom/prostore-react';

function MyComponent() {
  const [locationState, locationStore] = useStore(STORE_LOCATION);

  // locationState.location - актуальный Location (как если бы мы вызвали useLocation у react-router)

  // locationState.query - объект вида { [query]: value, ... }, содержащий значения query-параметров

  // locationStore.changeQuery({ key: value, ... }) - позволяет удобно поменять значения одного или нескольких параметров,
  //    в итоге делает history.push с тем же pathname, но другими квери-параметрами
  // Если вызвать locationStore.changeQuery({}, true), то сделает history.replace

  // locationStore.history - appHistory (аналогично вызову useHistory)
}

Можно подписаться только на изменения конкретных квери-параметров через хук useLocationQuery:

import { useLocationQuery } from '@proscom/prostore-react-router';

const QUERY_KEY_PAGE = 'page';
const QUERY_KEY_SEARCH = 'search';

function MyComponent() {
  const [query, locationStore] = useLocationQuery(STORE_LOCATION, [
    // Массив квери-параметров, на которые надо подписаться
    // (если он изменится, то компонент переподпишется на новые параметры)
    QUERY_KEY_PAGE,
    QUERY_KEY_SEARCH
  ]);

  // query.page - значение query-параметра page
  // query.search - значение query-параметра search

  // locationStore - аналогично примеру выше
}

Такой способ подписки позволяет при изменении одного квери-параметра не перерендеривать всю страницу. Обратите внимание, что компонент страницы автоматически перерендеривается при смене location из-за react-router. Поэтому, чтобы оптимизировать этот перерендер, оберните компонент страницы в React.memo:

const IndexPageComponent = React.memo(function IndexPageComponent() {
  // ...

  return (
    <div>
      {/* ... */}
      <MyComponent />
    </div>
  );
});

export function IndexPage() {
  // Не прокидывайте пропы в IndexPageComponent, так как они включают location, и приводят к перерендеру
  return <IndexPageComponent />;
}

За пределами реакта можно подписаться на квери-параметры с помощью метода LocationStore.get$:

// Создает обзервабл на часть квери
const queryPart$ = locationStore.get$(QUERY_KEY_PAGE, QUERY_KEY_SEARCH);
queryPart$.subscribe((queryPart) => {
  /* ... */
});

Текущие значения квери-параметров можно получить через locationStore.state.query, например:

console.log(locationStore.state.query[QUERY_KEY_PAGE]);

Если подписка на квери-парамтеры не требуется, а требуется только получить доступ к функциям стора, то можно использовать хук useContextStore из prostore:

function MyComponent() {
  const locationStore = useContextStore<LocationStore>(STORE_LOCATION);

  return (
    <button onClick={() => locationStore.changeQuery({ enabled: true })}>
      Enable
    </button>
  );
}

Трансформеры

Квери-параметры могут хранить только строки. LocationStore позволяет использовать отдельные трансформеры (сериализаторы и десериализаторы) для различных квери-параметров, чтобы преобразовывать произвольные данные в строки и обратно.

Десериализаторы вызываются только один раз при обновлении url, а сериализаторы - только при вызове LocationStore.changeQuery. Десериализатор также вызывается, если какой-то код подписан на квери-параметр, но он не присутствует в адресной строке. В таком случае queryValue = undefined, и вы можете сами вернуть необходимое значение по-умолчанию. Из сериализатора можно вернуть undefined, чтобы исключить квери-параметр из адресной строки.

Чтобы определить трансформеры, создайте объект интерфейса IQueryTransformers, и передайте его в конструктор LocationStore. В этом объекте ключами являются квери-параметры, а значениями - объекты вида IQueryTransformer.

import {
  IQueryTransformers,
  IQueryTransformer,
  defineQueryTransformers,
  ExtractTransformedQueryParams
} from '@proscom/prostore-react-router';

const QUERY_KEY_PAGE = 'page';

// Создаем трансформер для нужного типа
const numberTransformer: IQueryTransformer<number | undefined> = {
  parse: (queryValue) => {
    const n = +queryValue;
    return isNaN(n) ? undefined : n;
  },
  stringify: (value) =>
    value !== undefined && !isNaN(n) ? String(value) : undefined
};

// Определяем трансформеры через функцию-хелпер, которая проверяет типы
const queryTransformers = defineQueryTransformers({
  [QUERY_KEY_PAGE]: numberTransformer
});

// Тип параметров для использования с useLocationQuery
type TransformedQueryParams = ExtractTransformedQueryParams<
  typeof queryTransformers
>;

const locationStore = new LocationStore({
  transformers: queryTransformers /* ... */
});

// Теперь значение в стейте будет уже десериализованное
// locationStore.state.query[QUERY_KEY_PAGE] = 5

// Значения, отправленные в changeQuery, проходят через сериализацию перед попаданием в url
locationStore.changeQuery({ [QUERY_KEY_PAGE]: 5 });
// Пример использования с useLocationQuery

const queryKeys = [QUERY_KEY_PAGE, QUERY_KEY_SEARCH] as const;

function MyComponent() {
  const [query, locationStore] = useLocationQuery<
    TransformedQueryParams,
    typeof queryKeys
  >(STORE_LOCATION, queryKeys);

  // query.page - значение query-параметра page
  // query.search - значение query-параметра search

  // locationStore - аналогично примеру выше
}

Сохранение квери-параметров

У LocationStore есть метод createUrlPreserver, который позволяет автоматически сохранять квери-параметры в ссылках при переходах между страницами.

function IndexPage() {
  // Для сохранения квери-параметров необходимо подписаться на LocationStore для их получения в десериализованном виде
  const [query, locationStore] = useLocationQuery<LocationStore>(
    STORE_LOCATION,
    [QUERY_KEY_SEARCH]
  );

  // Передайте в функцию те десериализованные квери-параметры, которые необходимо сохранить
  const url = useMemo(() => locationStore.createUrlPreserver(query), [query]);

  // Используйте функцию url для создания ссылки
  return <a href={url('/filters')}>Перейти к фильтрам</a>;
}

// В результате на странице /?search=Test
// компонент отрендерит <a href="/filters?search=Test">Перейти к фильтрам</a>

API

LocationStore

Стор, обеспечивающий синхронизацию react-router и prostore.

history: History

History из пакета history, переданная в конструкторе стора.

state: ILocationStoreState

Состояние стора

state.location: Location

Текущий Location, как в react-router

state.query: ITransformedQuery

Десериализованные значения квери-параметров

get$<Params, Keys extends string[]>(...items: Keys): Observable<QueryPart<Params, Keys>>

Возвращает обзвервабл, содержащий трансформированные значения части квери-параметров

  • items - набор квери параметров, на которые надо подписаться

createUrlPreserver(query: ITransformedQuery|null|undefined)

Создает конструктор ссылок, сохраняющих набор параметров адресной строки.

  • query - набор десериализованных квери-параметров для сохранения. По-умолчанию сохраняет все текущие параметры

Возвращает функцию типа (pathname: string, newQuery?: ITransformedQuery) => string. Она создает ссылку с сохранением query-параметров. При этом прогоняет query-параметры через сериализаторы из набора трансформеров

Параметры функции:

  • pathname - адрес для перехода
  • newQuery - новые квери-параметры

setTransformer(key: string, transformer: IQueryTransformer | undefined)

Позволяет добавить трансформер квери-параметра после создания стора

  • key - квери-параметр
  • transformer - трнасформер

changeQuery(changes: Partial<ITransformedQuery>, replace = false)

Позволяет изменить квери-параметры в URL. Принимает квери-параметры для изменения. Все остальные квери-параметры будут сохранены. Если необходимо убрать какой-то параметр, то в качестве значения следует передать undefined.

По-умолчанию делает history.push (т.е. сохраняет навигацию в истории браузера для кнопки назад).

  • changes - изменения квери-параметров
  • replace - если true, то вместо history.push происходит history.replace

LocationProvider

Компонент, который синхронизирует LocationStore и контекст react-router

useLocationQuery<Params, Keys>(storeOrName: StoreOrName<LocationStore>, keys: Keys)

Хук, позволяющий подписаться на часть квери-параметров

  • storeOrName - LocationStore или его имя в ProstoreContext
  • keys - массив имен квери-параметров, на которые надо подписаться

Возвращает [state, store], где

  • state - трансформированные значения квери-параметров,
  • store - LocationStore

useReactRouter

Инжектирует контекст react-router в компонент

createUrlPreserver(preservedQuery: ITransformedQuery, transformers?: IQueryTransformers)

Создает конструктор ссылок, сохраняющих набор параметров адресной строки.

  • preservedQuery - набор десериализованных квери-параметров для сохранения. Они будут обработаны сериализатором из переданного набора трансформеров
  • transformers - набор трансформеров для сериализации квери-параметров

Возвращает функцию типа (pathname: string, newQuery?: ITransformedQuery) => string. Она создает ссылку с сохранением query-параметров. При этом прогоняет query-параметры через сериализаторы из набора трансформеров

Параметры функции:

  • pathname - адрес для перехода
  • newQuery - новые квери-параметры

IQueryTransformer<T>

Тип для трансформера квери-параметра

defineQueryTransformers<T>(x)

Возвращает переданное значение, но шаманит с типами, чтобы тип результата был наиболее точен, но TS проверял, что он расширяет IQueryTransformers. Используйте эту функцию при создании трансформеров.

ExtractTransformedQueryParams<T>

Тип, достающий из типа трансформеров типы десериализованных значений