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

gw-store

v0.2.0

Published

A small immutable React store with selectors and typed sync or async actions

Readme

GW Store


React 컴포넌트 수명에 맞춰 생성되는 작은 불변 외부 스토어입니다. 자유롭게 상태를 변경하는 useStore와, 동기·비동기 함수를 미리 등록하는 useActionStore를 함께 제공합니다. 별도의 middleware 없이 일반 async 함수와 await를 사용할 수 있습니다.

설치

npm install gw-store

React 18 이상이 필요합니다. react-dom에는 의존하지 않으므로 React Native에서도 사용할 수 있습니다.

기본 사용법

import { shallowEqual, useSelector, useStore, type Store } from "gw-store";

type CounterState = {
  count: number;
  profile: {
    name: string;
  };
};

function Count({ store }: { store: Store<CounterState> }) {
  // 인라인 selector를 사용해도 안전합니다.
  const count = useSelector(store, (state) => state.count);

  return <strong>{count}</strong>;
}

export function Counter() {
  const store = useStore<CounterState>({
    count: 0,
    profile: { name: "GW" },
  });

  return (
    <div>
      <Count store={store} />
      <button onClick={() => store.dispatch({ count: store.state.count + 1 })}>
        Increase
      </button>
      <button
        onClick={() =>
          store.dispatch((draft) => {
            draft.profile.name = "Store";
          })
        }
      >
        Rename
      </button>
    </div>
  );
}

useStore(initialState)는 최초 렌더에서 스토어를 한 번만 만들며 이후 같은 객체를 반환합니다. initialState가 바뀌어도 스토어를 재생성하거나 초기화하지 않습니다.

Selector와 동등성 비교

useSelectoruseSyncExternalStore와 선택 결과 캐시를 함께 사용합니다. selector가 렌더마다 새 함수로 만들어지더라도, 동일한 스토어 상태를 읽는 동안에는 캐시된 결과를 반환하므로 getSnapshot의 정체성이 흔들려 발생하는 무한 렌더를 피합니다.

문자열이나 숫자처럼 안정적인 값을 선택할 때는 기본 Object.is 비교면 충분합니다.

const name = useSelector(store, (state) => state.profile.name);

객체나 배열을 새로 만들어 반환한다면 비교 함수를 전달해야 관련 없는 상태 변경에서 렌더를 건너뛸 수 있습니다.

const profile = useSelector(
  store,
  (state) => ({ name: state.profile.name }),
  shallowEqual,
);

selector가 props를 캡처하거나 렌더 사이에 바뀌는 경우도 지원합니다.

Dispatch 규칙

얕은 patch는 patch 자체가 가진 enumerable property만 복사합니다.

store.dispatch({ count: 2 });

중첩 값을 변경할 때는 Immer recipe를 사용합니다. draft 타입은 Draft<TState>로 추론되므로 readonly 상태 타입도 recipe 안에서는 안전하게 변경할 수 있습니다.

store.dispatch((draft) => {
  draft.profile.name = "New name";
});

초기 상태와 이후 스냅샷은 깊게 동결됩니다. recipe가 상태를 바꾸지 않으면 subscriber에게 알리지 않으며 store.state는 읽기 전용입니다.

선택적으로 알림 key를 전달할 수 있으며 직접 등록한 subscriber에서 받을 수 있습니다.

const unsubscribe = store.subscribe((state, key) => {
  console.log(key, state);
});

store.dispatch({ count: 3 }, { key: "counter" });
unsubscribe();

여러 동기 업데이트를 한 번만 알리려면 batch를 사용합니다.

store.batch(
  () => {
    store.dispatch({ count: 1 });
    store.dispatch((draft) => {
      draft.profile.name = "Batched";
    });
  },
  { key: "counter:update" },
);

subscriber가 알림 중 다시 dispatch해도 새 알림은 현재 알림이 끝난 뒤 순서대로 처리됩니다.

등록형 Action Store

업데이트 함수를 한곳에 모으고 싶다면 useActionStore를 사용합니다. 호출부에서는 Redux action object나 middleware 없이 타입이 보존된 함수를 직접 호출합니다.

import { useActionStore, useSelector } from "gw-store";

type UserState = {
  count: number;
  status: "idle" | "loading" | "success" | "error";
  user: { id: string; name: string } | null;
  error: string | null;
};

function UserPanel() {
  const initialState: UserState = {
    count: 0,
    status: "idle",
    user: null,
    error: null,
  };

  const store = useActionStore(
    initialState,
    {
      increment({ set }, amount: number = 1) {
        set((draft) => {
          draft.count += amount;
        });
      },

      async loadUser({ set, getState }, id: string, signal?: AbortSignal) {
        set({ status: "loading", error: null });

        try {
          const response = await fetch(`/api/users/${id}`, { signal });
          if (!response.ok) throw new Error(`HTTP ${response.status}`);

          const user = await response.json();
          set({ user, status: "success" });
          return getState().user;
        } catch (error) {
          set({
            status: "error",
            error: error instanceof Error ? error.message : "Unknown error",
          });
          throw error;
        }
      },
    },
  );

  const status = useSelector(store, (state) => state.status);

  return (
    <>
      <button onClick={() => store.actions.increment(2)}>+2</button>
      <button onClick={() => void store.actions.loadUser("42")}>Load</button>
      <span>{status}</span>
    </>
  );
}

각 action은 { set, getState, batch }를 받습니다.

  • set: 얕은 patch 또는 Immer recipe를 적용합니다.
  • getState(): await 이후에도 항상 최신 스냅샷을 반환합니다.
  • batch: 여러 동기 업데이트를 한 번만 알립니다.
  • action의 인자와 반환값은 store.actions에 그대로 추론됩니다.
  • 직접 등록한 subscriber의 알림 key에는 기본적으로 action 이름이 전달됩니다.
  • 오류는 일반 함수처럼 throw/reject되므로 호출자가 try/catch할 수 있습니다.
  • 취소가 필요하면 별도 라이브러리 대신 표준 AbortSignal을 action 인자로 전달합니다.

batch는 동기 구간만 묶습니다. await 이후 여러 업데이트를 하나로 알리고 싶다면 그 업데이트들을 별도의 batch(() => { ... }) 호출 안에 넣습니다.

action 정의는 첫 렌더에서 한 번 등록됩니다. 렌더마다 변하는 props나 값은 action 클로저에 캡처하지 말고 action 인자로 전달해야 합니다.

React Server Components

패키지는 React Hook만 제공하므로 배포 파일에 "use client" 경계를 포함합니다. Next.js App Router에서는 이 패키지를 사용하는 컴포넌트가 Client Component로 처리됩니다.

개발

npm run check
npm run test:package
npm run build
npm pack --dry-run

패키지는 CommonJS와 ESM을 모두 제공하며 공개 진입점은 gw-store 하나입니다.

릴리스

릴리스 버전의 변경 사항을 CHANGELOG.md에 기록하고 다음 검증을 통과해야 합니다.

npm run release:check
npm publish --dry-run

배포는 새 버전을 커밋한 뒤 main 브랜치에 push하면 자동으로 시작됩니다. 이미 npm에 게시된 버전이면 배포 단계는 안전하게 건너뜁니다.

git add .
git commit -m "Release v0.2.0"
git push origin main

GitHub Actions는 lockfile로 의존성을 설치하고 prepublishOnly 검증과 prepack 빌드를 거쳐 public 패키지와 provenance를 npm에 게시합니다. npm 패키지 설정에서 GitHub 저장소 oh-jinsu/gw-store와 workflow publish.yaml을 Trusted Publisher로 등록한 뒤에는 장기 NPM_PUBLISH_TOKEN secret을 제거할 수 있습니다.