한ęµě–´ 문서(Korean Documentation)
A lightweight, efficient state management library for React applications that provides component tree-scoped state with optimized rendering.
React offers several ways to manage state, but each has limitations in specific scenarios:
- 
Global State (Redux, Zustand) is designed for app-wide data sharing, not for specific component trees. It's also challenging to handle state based on component lifecycle.
 - 
React Context API creates scoped state within component trees, but causes unnecessary re-renders across all child components when any part of the context changes.
 - 
React Query excels at server state management but uses a global key-based approach, not ideal for component-scoped client state.
 
Context Query combines the best aspects of these approaches:
- Component Tree Scoping: Like Context API, state is tied to component lifecycle
 - Subscription Model: Like React Query, only components that subscribe to specific state keys re-render
 - Simple API: Familiar hook-based pattern similar to React's 
useState 
Context Query is ideal for:
- Component Groups: When you need to share state among a group of components without prop drilling
 - Component-Scoped State: When state should be tied to a specific component tree's lifecycle
 - Performance Critical UIs: When you need to minimize re-renders in complex component hierarchies
 
Context Query is not a one-size-fits-all solution. For optimal performance and architecture, choose state management tools based on their intended purpose:
- Global State (Redux, Zustand): Use for true application-wide state that needs to persist across the entire app
 - React Query: Use for server state management and data fetching, which is its primary purpose
 - Context API: Use for theme changes, locale settings, or other cases where you intentionally want all child components to re-render
 - Context Query: Use when you need component tree-scoped state sharing without prop drilling, while preventing unnecessary sibling re-renders
 
- 🚀 Granular Re-rendering: Components only re-render when their specific subscribed state changes
 - 🔄 Component Lifecycle Integration: State is automatically cleaned up when provider components unmount
 - 🔌 Simple API: Familiar hook-based API similar to React's 
useState - đź§© TypeScript Support: Full type safety with TypeScript
 - 📦 Lightweight: Minimal bundle size with zero dependencies
 - đź”§ Compatible: Works alongside existing state management solutions
 
# Using npm
npm install @context-query/react
# Using yarn
yarn add @context-query/react
# Using pnpm
pnpm add @context-query/react// CounterContextQueryProvider.tsx
import { createContextQuery } from "@context-query/react";
type CounterAtoms = {
  primaryCounter: {
    name: string;
    value: number;
    description: string;
  };
  secondaryCounter: {
    name: string;
    value: number;
    description: string;
  };
};
export const {
  ContextQueryProvider: CounterQueryProvider,
  useContextAtom: useCounterAtom,
  useContextAtomValue: useCounterAtomValue,
  useContextSetAtom: useCounterSetAtom,
} = createContextQuery<CounterAtoms>();// CounterApp.tsx
import { CounterQueryProvider } from "./CounterContextQueryProvider";
function CounterApp() {
  return (
    <CounterQueryProvider
      atoms={{
        primaryCounter: {
          name: "Primary Counter",
          value: 0,
          description: "Main counter that controls other counters",
        },
        secondaryCounter: {
          name: "Secondary Counter",
          value: 0,
          description: "Secondary counter linked to primary",
        },
      }}
    >
      <CounterContent />
    </CounterQueryProvider>
  );
}
function CounterContent() {
  return (
    <div className="counter-app">
      <PrimaryCounterComponent />
      <SecondaryCounterComponent />
    </div>
  );
}// PrimaryCounterComponent.tsx
import { useCounterAtom, useCounterSetAtom } from "./CounterContextQueryProvider";
function PrimaryCounterComponent() {
  // Subscribe to primary counter atom only
  const [primaryCounter, setPrimaryCounter] = useCounterAtom("primaryCounter");
  const setSecondaryCounter = useCounterSetAtom("secondaryCounter");
  const increment = () => {
    setPrimaryCounter((prev) => ({ ...prev, value: prev.value + 1 }));
    // Also update secondary counter
    setSecondaryCounter((prev) => ({ ...prev, value: prev.value + 1 }));
  };
  const decrement = () => {
    setPrimaryCounter((prev) => ({ ...prev, value: prev.value - 1 }));
  };
  const reset = () => {
    setPrimaryCounter((prev) => ({ ...prev, value: 0 }));
  };
  return (
    <div className="counter">
      <h2>{primaryCounter.name}</h2>
      <p>{primaryCounter.description}</p>
      <div className="counter-controls">
        <span>{primaryCounter.value}</span>
        <button onClick={decrement}>-</button>
        <button onClick={increment}>+</button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
}
// SecondaryCounterComponent.tsx
import { useCounterAtomValue } from "./CounterContextQueryProvider";
function SecondaryCounterComponent() {
  // Read-only access to secondary counter atom
  const secondaryCounter = useCounterAtomValue("secondaryCounter");
  return (
    <div className="counter secondary">
      <h3>{secondaryCounter.name}</h3>
      <p>{secondaryCounter.description}</p>
      <div className="counter-display">
        <span>{secondaryCounter.value}</span>
      </div>
    </div>
  );
}
// BatchUpdateComponent.tsx
import { useCounterSetAtom } from "./CounterContextQueryProvider";
function BatchUpdateComponent() {
  const setPrimaryCounter = useCounterSetAtom("primaryCounter");
  const setSecondaryCounter = useCounterSetAtom("secondaryCounter");
  const resetAll = () => {
    setPrimaryCounter((prev) => ({ ...prev, value: 0 }));
    setSecondaryCounter((prev) => ({ ...prev, value: 0 }));
  };
  const incrementAll = () => {
    setPrimaryCounter((prev) => ({ ...prev, value: prev.value + 1 }));
    setSecondaryCounter((prev) => ({ ...prev, value: prev.value + 1 }));
  };
  return (
    <div className="batch-controls">
      <button onClick={resetAll}>Reset All Counters</button>
      <button onClick={incrementAll}>Increment All Counters</button>
    </div>
  );
}This example demonstrates:
- Atom-based Architecture: Each piece of state is managed as a separate atom
 - Granular Subscriptions: Components subscribe only to the atoms they need, optimizing re-renders
 - Read-Write Separation: Use 
useContextAtomfor read-write access,useContextAtomValuefor read-only access, anduseContextSetAtomfor write-only access - Cross-Atom Updates: Components can update multiple atoms independently
 
The createContextQuery function returns three hooks for different use cases:
const {
  ContextQueryProvider,
  useContextAtom,        // Read-write access to an atom
  useContextAtomValue,   // Read-only access to an atom
  useContextSetAtom,     // Write-only access to an atom
} = createContextQuery<YourAtomTypes>();function CounterComponent() {
  const [counter, setCounter] = useContextAtom("counter");
  
  const increment = () => {
    setCounter((prev) => ({ ...prev, value: prev.value + 1 }));
  };
  
  return (
    <div>
      <span>{counter.value}</span>
      <button onClick={increment}>+</button>
    </div>
  );
}function DisplayComponent() {
  const counter = useContextAtomValue("counter");
  
  return <div>Current value: {counter.value}</div>;
}function ControlComponent() {
  const setCounter = useContextSetAtom("counter");
  
  const reset = () => {
    setCounter((prev) => ({ ...prev, value: 0 }));
  };
  
  return <button onClick={reset}>Reset</button>;
}Similar to React's useState, you can pass a function to atom setters:
const [counter, setCounter] = useContextAtom("counter");
// Update based on previous state
const increment = () => {
  setCounter((prev) => ({ ...prev, value: prev.value + 1 }));
};Using the same provider multiple times creates independent state instances:
function App() {
  return (
    <div>
      {/* First counter instance */}
      <CounterQueryProvider atoms={{ counter: { value: 0, name: "First Counter" } }}>
        <CounterSection title="First Section" />
      </CounterQueryProvider>
      {/* Second counter instance (completely independent) */}
      <CounterQueryProvider atoms={{ counter: { value: 10, name: "Second Counter" } }}>
        <CounterSection title="Second Section" />
      </CounterQueryProvider>
    </div>
  );
}
function CounterSection({ title }) {
  const [counter, setCounter] = useCounterAtom("counter");
  
  return (
    <div>
      <h2>{title}</h2>
      <p>{counter.name}: {counter.value}</p>
      <button onClick={() => setCounter(prev => ({ ...prev, value: prev.value + 1 }))}>
        Increment
      </button>
    </div>
  );
}Each provider maintains its own state, so changing one counter won't affect the other.
The project consists of multiple packages:
@context-query/core: Core functionality and state management@context-query/react: React bindings and hooksplayground: Demo application showcasing the library
- Node.js >= 18
 - pnpm >= 9.0.0
 
# Clone the repository
git clone https://github.com/load28/context-query.git
cd context-query
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run the playground demo
pnpm playgroundsequenceDiagram
    participant M as Main Branch
    participant R as Release Branch
    participant W as Work Branch
    M->>R: Create Release Branch (0.3.0)
    R->>W: Create Work Branch (WIP/0.3.0/feat/update)
    Note over W: Feature Development and Bug Fixes
    W->>R: Rebase onto Release Branch
    Note over R: Change Package Version (0.3.0-dev.1)
    Note over R: Test and Fix
    Note over R: Change Package Version (0.3.0-dev.2)
    Note over R: Test and Fix
    Note over R: Finalize Package Version (0.3.0)
    R->>M: Rebase onto Main Branch
    M->>M: Add Version Tag (v0.3.0)
    MIT