한ęµě–´ 문서(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
useContextAtom
for read-write access,useContextAtomValue
for read-only access, anduseContextSetAtom
for 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 playground
sequenceDiagram
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