í•śęµě–´ 문서(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
// UserContextQueryProvider.tsx
import { createContextQuery } from "@context-query/react";
interface UserData {
name: string;
email: string;
preferences: {
theme: "light" | "dark";
notifications: boolean;
};
}
export const {
Provider: UserQueryProvider,
useContextQuery: useUserQuery,
updateState: updateUserState,
setState: setUserState,
} = createContextQuery<UserData>({
name: "",
email: "",
preferences: {
theme: "light",
notifications: true,
},
});
// UserProfilePage.tsx
import { UserQueryProvider, updateUserState } from "./UserContextQueryProvider";
async function fetchUserData(userId: string) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
function UserProfilePage({ userId }: { userId: string }) {
useEffect(() => {
// Initialize state with external data
const loadUserData = async () => {
const userData = await fetchUserData(userId);
updateUserState(userData); // Update entire state with fetched data
};
loadUserData();
}, [userId]);
return (
<UserQueryProvider>
<div className="user-profile">
<UserInfoForm />
<UserPreferencesForm />
<SaveButton />
</div>
</UserQueryProvider>
);
}
// UserInfoForm.tsx
import { useUserQuery } from "./UserContextQueryProvider";
function UserInfoForm() {
// Subscribe to user info fields only
const [state, setState] = useUserQuery(["name", "email"]);
return (
<div className="user-info">
<h3>Basic Information</h3>
<div>
<label>Name:</label>
<input
value={state.name}
onChange={(e) =>
setState((prev) => ({ ...prev, name: e.target.value }))
}
/>
</div>
<div>
<label>Email:</label>
<input
value={state.email}
onChange={(e) =>
setState((prev) => ({ ...prev, email: e.target.value }))
}
/>
</div>
</div>
);
}
// UserPreferencesForm.tsx
import { useUserQuery } from "./UserContextQueryProvider";
function UserPreferencesForm() {
// Subscribe to preferences only
const [state, setState] = useUserQuery(["preferences"]);
const toggleTheme = () => {
setState((prev) => ({
...prev,
preferences: {
...prev.preferences,
theme: prev.preferences.theme === "light" ? "dark" : "light",
},
}));
};
const toggleNotifications = () => {
setState((prev) => ({
...prev,
preferences: {
...prev.preferences,
notifications: !prev.preferences.notifications,
},
}));
};
return (
<div className="user-preferences">
<h3>User Preferences</h3>
<div>
<label>Theme: {state.preferences.theme}</label>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
<div>
<label>
<input
type="checkbox"
checked={state.preferences.notifications}
onChange={toggleNotifications}
/>
Enable Notifications
</label>
</div>
</div>
);
}
// SaveButton.tsx
import { useUserQuery, updateUserState } from "./UserContextQueryProvider";
function SaveButton() {
// Get all user data for saving
const [userData] = useUserQuery(["name", "email", "preferences"]);
const handleSave = async () => {
try {
const response = await fetch("/api/users/update", {
method: "POST",
body: JSON.stringify(userData),
});
const updatedUser = await response.json();
// Update entire state with server response
updateUserState(updatedUser);
} catch (error) {
console.error("Failed to save user data:", error);
}
};
return <button onClick={handleSave}>Save Changes</button>;
}
This example demonstrates:
- Separation of concerns by splitting user information and preferences into separate components
- Each component subscribes only to the state it needs, optimizing re-renders
- Components can independently update their relevant portions of the state
Similar to React's useState
, you can pass a function to the state setter:
const [count, setCount] = useCounterQuery(["count1"]);
// Update based on previous state
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
// Or use setState directly
const incrementOutside = () => {
setCounterState("count1", (prev) => prev + 1);
};
You can update multiple states at once using the updateState function:
import { updateCounterState } from "./CounterContextQueryProvider";
// Update multiple states at once
const resetCounters = () => {
updateCounterState({
count1: 0,
count2: 0,
});
};
You can use multiple providers for different component subtrees:
function App() {
return (
<div>
<FeatureAProvider>
<FeatureAComponents />
</FeatureAProvider>
<FeatureBProvider>
<FeatureBComponents />
</FeatureBProvider>
</div>
);
}
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