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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@makstashkevich/zustand-model

v1.0.5

Published

A lightweight and powerful Zustand plugin for simplified asynchronous API state management in React. Eliminates boilerplate for loading, error, and data handling, offering a clean, provider-less solution with built-in caching.

Downloads

31

Readme

zustand-model

Жанглируй данными из API-запросов (и не только) как Бог!

Привет друг!

Хочу рассказать тебе про свою новую библиотеку @makstashkevich/zustand-model.

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

Зачем я это вообще сделал?

Знаешь, как бывает: пишешь кучу API-запросов, и каждый раз приходится дублировать код для отслеживания состояния загрузки, ошибок, успешного ответа...

А еще эти бесконечные провайдеры, которые засоряют дерево компонентов!

Меня это жутко достало.

Хотелось чего-то простого, элегантного, что позволило бы один раз описать логику работы с API, а потом просто использовать ее в любом месте приложения, не заморачиваясь с лишним кодом. Вот так и родился этот плагин!

Он дает тебе несколько классных преимуществ:

  • Меньше кода: Забудь про ручное отслеживание loading и error для каждого запроса. Плагин делает это за тебя.
  • Чистота: Никаких лишних провайдеров! Все работает на базе Zustand, а значит, состояние доступно глобально, но при этом структурировано.
  • Удобство: Легко получать данные, состояния загрузки и ошибки прямо в компонентах.
  • Гибкость: Поддерживает как асинхронные, так и синхронные действия, что делает его универсальным решением для любого типа состояния.
  • Кэширование: Есть встроенный механизм для отслеживания "свежести" данных, что очень удобно для кэширования.

Как это установить?

Все просто, как дважды два:

npm install @makstashkevich/zustand-model zustand
# или, если ты любишь yarn
yarn add @makstashkevich/zustand-model zustand

Давай посмотрим, как это работает на примерах!

Представь, что у нас есть модель для работы со страницами сайта.

1. Создаем модель и действия

Сначала мы определяем, какие данные у нас будут храниться (IPageModelDataState) и какие асинхронные действия мы можем выполнять (IPageModelActions).

import { IBaseModelState, IBaseModelActions, createModel, createFreshnessHook } from '@makstashkevich/zustand-model';

// Предположим, у нас есть такие типы и сервисы API
interface PageSchema {
  pathname: string;
  title: string;
  content: string;
  views: number;
}

interface IPageModelDataState {
  page: PageSchema | null;
  pages: PageSchema[] | null;
  popularPages: PageSchema[] | null;
}

interface IPageModelActions extends IBaseModelActions {
  getPage: (pathname: string) => Promise<Partial<IPageModelDataState>>;
  // ... другие действия
}

export type IPageModelState = IPageModelDataState & IPageModelActions & IBaseModelState<IPageModelActions>;

const initialDataState: IPageModelDataState = {
  page: null,
  pages: null,
  popularPages: null,
};

const asyncActions: IPageModelActions = {
  getPage: async (pathname) => {
    // Здесь вызываем наш API-сервис
    const fetchedPage = await fetch(`/api/pages/${pathname}`).then(res => res.json());
    return { page: fetchedPage }; // Возвращаем только то, что хотим обновить в состоянии
  },
  // ... другие действия
};

export const usePageModel = createModel<IPageModelDataState, IPageModelActions>(
  initialDataState,
  asyncActions,
);

// Для удобства можно экспортировать действия напрямую
export const getPage = (pathname: string) => usePageModel.getState().getPage(pathname);

2. Используем в компоненте — это же магия!

Теперь самое интересное. Как получить данные, узнать, идет ли загрузка, или была ли ошибка? Элементарно!

import React, { useEffect } from 'react';
import { usePageModel, getPage } from './pageModel'; // Импортируем нашу модель и действие

function PageDisplay({ pathname }) {
  // Получаем саму страницу
  const page = usePageModel.use.page();
  // Узнаем, идет ли загрузка для действия 'getPage'
  const isLoading = usePageModel.use.loadingStates().getPage;
  // Была ли ошибка при выполнении 'getPage'
  const error = usePageModel.use.errorsState().getPage;

  useEffect(() => {
    // Загружаем страницу, когда компонент монтируется или меняется pathname
    getPage(pathname);
  }, [pathname]);

  if (isLoading) {
    return <p>Загружаю страницу...</p>;
  }

  if (error) {
    return <p>Ой, что-то пошло не так: {error.message}</p>;
  }

  if (!page) {
    return <p>Страница не найдена.</p>;
  }

  return (
    <div>
      <h1>{page.title}</h1>
      <p>{page.content}</p>
      <p>Просмотров: {page.views}</p>
    </div>
  );
}

export default PageDisplay;

Видишь? Никаких useState для loading и error, никаких try/catch в компоненте! Все это уже внутри модели. Просто вызываешь действие, а потом читаешь состояние. Красота!

3. А что насчет кэширования?

Для этого есть специальный хук createFreshnessHook. Он позволяет пометить данные как "свежие" или "устаревшие".

import React, { useEffect } from 'react';
import { usePageModel, usePageDataFreshness, getPage } from './pageModel';

function CachedPageInfo({ pathname }) {
  const page = usePageModel.use.page();
  // Отслеживаем свежесть для 'page' с ключом 'pathname'
  const { isFresh, markStale } = usePageDataFreshness('page', pathname);

  useEffect(() => {
    // Если данные не свежие, загружаем их
    if (!isFresh) {
      console.log('Данные устарели, загружаю заново...');
      getPage(pathname);
    } else {
      console.log('Данные свежие, использую кэш.');
    }
  }, [pathname, isFresh]);

  const handleRefresh = () => {
    // Принудительно помечаем данные как устаревшие, чтобы они обновились
    markStale();
  };

  return (
    <div>
      <h2>Информация о странице: {page?.title || 'Загрузка...'}</h2>
      <p>Статус данных: {isFresh ? 'Свежие' : 'Устаревшие'}</p>
      <button onClick={handleRefresh}>Обновить данные</button>
    </div>
  );
}

export default CachedPageInfo;

Это очень удобно, когда нужно контролировать, когда данные должны быть обновлены, а когда можно использовать кэшированную версию.

4. Обновление и удаление данных

Все действия работают по тому же принципу: вызываешь функцию, а модель сама обновляет состояние.

import React, { useState } from 'react';
import { usePageModel, updatePage, deletePage } from './pageModel';

function PageActions({ pathname }) {
  const page = usePageModel.use.page();
  const [newTitle, setNewTitle] = useState(page?.title || '');

  useEffect(() => {
    if (page) setNewTitle(page.title);
  }, [page]);

  const handleUpdate = async () => {
    try {
      await updatePage(pathname, { title: newTitle });
      alert('Страница обновлена!');
    } catch (e) {
      alert('Ошибка при обновлении!');
    }
  };

  const handleDelete = async () => {
    if (window.confirm('Точно удалить?')) {
      try {
        await deletePage(pathname);
        alert('Страница удалена!');
        // Возможно, перенаправить пользователя или обновить список страниц
      } catch (e) {
        alert('Ошибка при удалении!');
      }
    }
  };

  return (
    <div>
      <input value={newTitle} onChange={(e) => setNewTitle(e.target.value)} />
      <button onClick={handleUpdate}>Обновить заголовок</button>
      <button onClick={handleDelete}>Удалить страницу</button>
    </div>
  );
}

export default PageActions;

Как видишь, все очень интуитивно и требует минимум кода. Надеюсь, тебе понравится!

5. Работа с синхронным состоянием: Модальные окна

Плагин отлично подходит не только для асинхронных операций, но и для управления обычным синхронным состоянием. Давай посмотрим, как легко можно управлять модальными окнами.

import { IBaseModelState, IBaseModelActions, createModel } from '@makstashkevich/zustand-model';

// Определяем типы модальных окон
enum ModalType {
  LOGIN = 'LOGIN',
  REGISTER = 'REGISTER',
  SETTINGS = 'SETTINGS',
}

// Состояние для модальных окон
interface IModalModelDataState {
  currentModal: ModalType | null;
}

// Действия для модальных окон (синхронные!)
interface IModalModelActions extends IBaseModelActions {
  openModal: (modalType: ModalType) => Partial<IModalModelDataState>;
  closeModal: () => Partial<IModalModelDataState>;
}

export type IModalModelState = IModalModelDataState & IModalModelActions & IBaseModelState<IModalModelActions>;

const initialDataState: IModalModelDataState = {
  currentModal: null,
};

// Синхронные действия
const actions: IModalModelActions = {
  openModal: (modalType) => {
    return { currentModal: modalType };
  },
  closeModal: () => {
    return { currentModal: null };
  },
};

export const useModalModel = createModel<IModalModelDataState, IModalModelActions>(
  initialDataState,
  actions,
);

// Для удобства экспортируем действия
export const openModal = (modalType: ModalType) => useModalModel.getState().openModal(modalType);
export const closeModal = () => useModalModel.getState().closeModal();

А вот как это использовать в компоненте:

import React from 'react';
import { useModalModel, openModal, closeModal } from './modalModel'; // Импортируем модель и действия

function App() {
  const currentModal = useModalModel.use.currentModal(); // Получаем текущую открытую модалку

  return (
    <div>
      <h1>Мое приложение</h1>
      <button onClick={() => openModal(ModalType.LOGIN)}>Открыть логин</button>
      <button onClick={() => openModal(ModalType.SETTINGS)}>Открыть настройки</button>

      {currentModal === ModalType.LOGIN && (
        <div className="modal">
          <h2>Вход</h2>
          <p>Форма входа...</p>
          <button onClick={closeModal}>Закрыть</button>
        </div>
      )}

      {currentModal === ModalType.SETTINGS && (
        <div className="modal">
          <h2>Настройки</h2>
          <p>Настройки приложения...</p>
          <button onClick={closeModal}>Закрыть</button>
        </div>
      )}
    </div>
  );
}

export default App;

Как видишь, управление модальными окнами становится очень простым и централизованным!

Ссылки