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

v0.2.12

Published

Prostore hooks and utils for React

Downloads

79

Readme

prostore-react

Данный адаптер предоставляет набор полезных инструментов для интеграции prostore в React.

Можно прокидывать сторы через контекст и подписываться на сторы с помощью хуков, а также автоматически выполнять запросы в RequestStore.

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

Для примеров ниже будет использован простой стор, считающий количество вызовов функции increment:

// IncStore.ts
import { BehaviorStore } from '@proscom/prostore';

export interface IncStoreState {
  number: number;
}

const initialState: IncStoreState = {
  number: 0
};

export class IncStore extends BehaviorStore<IncStoreState> {
  constructor() {
    super(initialState);
  }

  increment() {
    this.setState({
      number: this.state.number + 1
    });
  }
}
// incStore.ts
import { IncStore } from './IncStore';
export const incStore = new IncStore();

См. пример в CodeSandbox

useStore

Основной хук, который позволяет подписать компонент на все изменения стора

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

// Импортируем любой store, расширяющий IStore
import { incStore } from './incStore';

function MyComponent() {
  const [state, store] = useStore(incStore);

  // state - состояние стора
  // store - инстанс стора, по которому можно вызывать методы

  // в этом примере store === incStore, но ниже есть пример, когда
  // useStore используется в связке с контекстом, где это не так

  const onClick = () => {
    store.increment();
  };

  return <button onClick={onClick}>{state.number}</button>;
}

См. пример в CodeSandbox

useStoreState

Иногда сам стор не нужен, а нужно только его состояние. Тогда можно использовать этот хук, чтобы не делать лишнюю деструктуризацию:

function MyComponent() {
  const state = useStoreState(incStore);
  return <div>{state.number}</div>;
}

См. пример в CodeSandbox

ProstoreContext

Контекст позволяет прокидывать инстансы сторов в компоненты по ключам. Таким образом реализуется принцип Dependency Injection и снижается связанность компонентов и сторов.

// stores.ts
export const STORE_TEST = 'STORE_TEST';
// index.ts
import ReactDOM from 'react-dom';
import { STORE_TEST } from './stores';
import { IncStore } from './IncStore';
import { App } from './App';
import { ProstoreContext } from '@proscom/prostore-react';

const testStore = new IncStore();

// В коде компонентов можно будет подключить
// инстанс testStore по ключу STORE_TEST
const stores = {
  [STORE_TEST]: testStore
};

ReactDOM.render(
  <ProstoreContext.Provider value={stores}>
    <App />
  </ProstoreContext.Provider>,
  document.getElementById('root')
);
// App.ts
import { useStore } from '@proscom/prostore-react';
import { IncStore } from './IncStore';
import { STORE_TEST } from './stores';

function App() {
  // Так как в контексте сохранено, что STORE_TEST это incStore,
  // то этот вызов полностью аналогичен useStore(incStore)

  // При использовании с TypeScript необходимо указать тип стора
  // в качестве аргумента дженерика. Проще всего указать имя класс стора (IncStore).
  // При необходимости можно уменьшить связанность кода, реализовав
  // отдельный интерфейс, который реализуется классом стора, и указать
  // его здесь в качестве аргумента дженерика.
  const [state, store] = useStore<IncStore>(STORE_TEST);

  const onClick = () => {
    store.increment();
  };

  return <button onClick={onClick}>{state.number}</button>;
}

См. пример в CodeSandbox

useContextStore

Иногда состояние стора в компоненте вообще не нужно, а нужен только сам стор для вызова его методов. При использовании сторов как глобальных переменных можно просто вызывать их методы:

import { incStore } from './incStore';

function IncButton() {
  return <button onClick={() => incStore.increment()}>Increment</button>;
}

См. пример в CodeSandbox

При использовании контекста можно получить доступ к стору с помощью хука useContextStore. По сравнению с использованием useStore это избавит компонент от лишних перерендеров при изменении состояния стора.

import { useContextStore } from '@proscom/prostore-react';
import { IncStore } from './IncStore';
import { STORE_TEST } from './stores';
function IncButton() {
  // При использовании с TypeScript необходимо указать тип стора
  // в качестве аргумента дженерика. Проще всего указать имя класс стора (IncStore).
  // При необходимости можно уменьшить связанность кода, реализовав
  // отдельный интерфейс, который реализуется классом стора, и указать
  // его здесь в качестве аргумента дженерика.
  const incStore = useContextStore<IncStore>(STORE_TEST);
  return <button onClick={() => incStore.increment()}>Increment</button>;
}

См. пример в CodeSandbox

useRequestStore

При использовании RequestStore можно воспользоваться специальным хуком useRequestStore, чтобы автоматически выполнять запрос при изменении его переменных, а также отслеживать состояние запроса (идет загрузка, ошибка, готов результат).

import { AxiosQueryStore } from '@proscom/prostore-axios';
import { useRequestStore } from '@proscom/prostore-react';

// Для примера воспользуемся AxiosQueryStore из @proscom/prostore-axios
// Этот класс расширяет RequestStore реализуя выполнение
// http-запросов с помощью axios

// Определяем динамические параметры запроса
export interface DataQueryStoreVariables {
  params?: {
    page?: number;
  };
}

// Определяем тип результата запроса
export interface DataQueryStoreData {
  message: string;
}

// Создаём стор, который будет хранить результат запроса.
// Сам запрос вызовется, только при подключении стора в компонент
const store = new AxiosQueryStore<DataQueryStoreVariables, DataQueryStoreData>({
  client: axios.create(),
  query: {
    url: '/data.json',
    method: 'get'
  }
});

function MyComponent() {
  // Параметры запроса
  const variables = {
    params: {
      page: 1
    }
  };

  // Дополнительные опции
  const options = undefined;

  // Подключаем стор в компонент и выполняем запрос при необходимости
  // По факту выполнения запроса компонент ререндерится
  const query = useRequestStore(store, variables, options);

  const { check, state, load } = query;

  // check - утилитарный объект, который позволяет
  //  быстро определить, что надо рендерить

  // state - состояние RequestStore

  // load - функция, которая позволяет повторить запрос
  //  с теми же переменными

  if (check.spinner) {
    return <Spinner />;
  } else if (check.error) {
    return <ErrorMessage error={state.error} />;
  }

  return <Info data={state.data} refresh={load} />;
}

См. пример в CodeSandbox

При каждом рендере useRequestStore глубоко сравнивает предыдущие переменные с новыми, и если есть отличия, то вызывает выполнение запроса с новыми переменными. Под глубоким сравнением понимается lodash.isEqual.

options передаются вторым аргументом в store.loadData в неизменном виде. При изменении options запрос не будет повторен.

useRequestStore можно также использовать с контекстом. В таком случае необходимо указать тип переменных и тип результата аргументами дженерика:

import { useRequestStore } from '@proscom/prostore-react';
import { STORE_TEST } from './stores';
import { StoreTestVariables, StoreTestData } from './stores/StoreTest';

function MyComponent() {
  const variables = {};
  const query = useRequestStore<StoreTestVariables, StoreTestData>(
    STORE_TEST,
    variables
  );
  // ...
}

См. пример в CodeSandbox

useAsyncOperation

RequestStore представляет собой реактивную зависимость результата запроса от переменных (параметров запроса). Это значит, что момент выполнения запроса определяется автоматически. Такая семантика подходит только запросов, удовлетворяющим свойствам чистой функции (возвращающих одни и те же значения для одних и тех же переменных и не выполняющим побочных эффектов), например для GET HTTP запросов на получение данных и запросов GraphQL Query.

Мутирующие запросы (POST HTTP или GraphQL Mutation) следует вызывать в коде императивно в нужный момент (например, в ответ на клик пользователя по кнопке). Мутирующий запрос может не оказывать никакого влияния на состояние компонентов, тогда его можно вызывать просто напрямую без использования хуков из этой библиотеки.

Если же возникает потребность отслеживать статус выполнения мутирующего запроса, то для этого можно использовать хук useAsyncOperation:

import axios from 'axios';
import { useAsyncOperation, AsyncSingletonError } from '@proscom/prostore-react';

function MyComponent() {
  const saveOp = useAsyncOperation(
    () => {
      // Чтобы корректно отследить статус завершения операции,
      // колбек должен возвращать промис
      return axios.post('/save', data).catch((e) => console.error(e));

      // То, как обрабатывать ошибку следует решить на уровне конкретного проекта.
      // Рекомендуется отобразить пользователю toast или текст ошибки рядом с кнопкой действия.
    },
    {
      // После завершения операции ставит finished=true на 5 секунд
      // Передайте Infinity, чтобы поставить finished=true навсегда после первого выполнения операции
      finishedTimeout: 5000,
      // Предотвращает повторный вызов операции до завершения предыдущей.
      // При использовании этого параметра в случае повторного вызова будет выброшена ошибка AsyncSingletonError.
      // Её надо обработать
      singleton: true
    }
  );

  const {
    // Функция для вызова операции
    run,
    // true, если операция выполняется
    loading,
    // true, если операция недавно завершена
    finished,
    // позволяет поменять значение finished в сложных случаях
    setFinished
  } = saveOp;

  const handleClick = () => {
    // Для обработки ошибки AsyncSingletonError достаточно добавить catch при вызове `run`, 
    // который и так должен быть, чтобы избежать более серьезной проблемы с `Unhandled Rejection`
    run().catch((err) => {
      // Ошибку AsyncSingletonError достаточно просто поглотить, никак не обрабатывая.
      // Эта ошибка выбрасывается, если передан параметр `singleton: true`,
      // и функция `run` вызвана второй раз до завершения предыдущего вызова.
      if (err instanceof AsyncSingletonError) return;
      console.error(err);
    });
  };

  return (
    <button type="button" onClick={handleClick} disabled={loading}>
      {finished ? 'Saved' : loading ? 'Saving...' : 'Save'}
    </button>
  );
}

См. пример в CodeSandbox

usePropsObservable

Преобразовывает пропы компонента или любые другие данные, связанные с циклом рендера реакта, в Observable из rxjs. Этот Observable затем можно использовать для построения реактивных пайплайнов, и дальнейшей подписки этого или другого компонента на результат.

Обратите внимание, что если подписать тот же самый компонент, на обзервабл, который возвращается из этого хука, то это может привести к бесконечному циклу перерендеров, если передаваемые пропы не стабилизируются. При передаче объект с пропами поверхностно проверяется (см. shallowequal). Если ни один ключ не изменился, то обзервабл не будет эмитить новое значение.

Так как этот хук достаточно сложен для понимания и несет за собой накладные расходы при выполнении, то использовать его следует только в том случае, если нужно связать компонент с каким-то существующим обзерваблом, либо использовать специфичный функционал rxjs.

import {
  useObservableState,
  usePropsObservable
} from '@proscom/prostore-react';
import React, { useState } from 'react';
import { debounceTime, map, tap } from 'rxjs/operators';

export default function App() {
  const [search, setSearch] = useState('');
  console.log('render', search);

  const debounced$ = usePropsObservable(
    // Значения, которые передаются в props$
    { search },
    // Конструктор обзервабла, возвращает debounced$
    (props$) => {
      return props$.pipe(
        debounceTime(500),
        map((p) => p.search),
        tap((v) => console.log('debounced', v))
      );
    },
    // Зависимости, от которых зависит вызов функции-конструктора.
    // Проверяются аналогично useMemo
    []
  );

  // Подписываемся на debounced$
  const debouncedSearch = useObservableState(debounced$);

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

См. пример в CodeSandbox

useObservable

Этот хук позволяет подписать колбек на произвольный обзервабл.

import { useObservable } from '@proscom/prostore-react';
import { fromEvent } from 'rxjs';

const resize$ = fromEvent(window, 'resize');

function MyComponent() {
  const handleChange = useCallback((value) => {
    console.log('changed', value);
  });
  useObservable(resize$, handleChange);

  // ...
}

useObservableCallback

Этот хук позволяет подписаться на произвольный обзервабл, но не выполняет переподписку при изменении колбека. Поэтому пример выше может быть переписан следующим образом:

import { useObservableCallback } from '@proscom/prostore-react';
import { fromEvent } from 'rxjs';

const resize$ = fromEvent(window, 'resize');

function MyComponent() {
  useObservableCallback(resize$, (value) => {
    console.log('changed', value);
  });

  // ...
}

Этот хук полезен при создании подписки на события сторов, а не на их состояния. См. пример в документации prostore.

useObservableState

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

Убедитесь, что используемый обзервабл повторяет своё последнее значение при подписке, в противном случае в редких ситуациях часть значений может быть потеряна. Чтобы создать обзервабл, повторяющий свои значения, воспользуйтесь оператором shareReplay. BehaviorSubject повторяет свои значения при подписке.

import { useObservableState } from '@proscom/prostore-react';
import { fromEvent } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';

const windowSize$ = fromEvent(window, 'resize').pipe(
  map(() => window.innerWidth),
  shareReplay()
);

function MyComponent() {
  const windowWidth = useObservableState(windowSize$, window.innerWidth);
  // ...
}

useSubject

Этот хук аналогичен useObservableState, только принимает не просто Observable, а кастомный тип ObservableWithValue, и автоматически задает первоначальное значение стейта компонента.

Убедитесь, что используемый обзервабл повторяет своё последнее значение при подписке, в противном случае в редких ситуациях часть значений может быть потеряна. Чтобы создать обзервабл, повторяющий свои значения, воспользуйтесь оператором shareReplay. BehaviorSubject повторяет свои значения при подписке.

import { useSubject } from '@proscom/prostore-react';
import { BehaviorSubject } from 'rxjs';

const data$ = new BehaviorSubject(5);

function MyComponent() {
  const data = useSubject(data$);
  // ...
}