@metorial/util-react-fetch
v2.0.0
Published
React fetch utilities for Metorial. Provides React-specific hooks and utilities for data fetching, state management, and mutation handling with automatic caching, polling, and error management.
Readme
@metorial/util-react-fetch
React fetch utilities for Metorial. Provides React-specific hooks and utilities for data fetching, state management, and mutation handling with automatic caching, polling, and error management.
Installation
npm install @metorial/util-react-fetch
# or
yarn add @metorial/util-react-fetch
# or
pnpm add @metorial/util-react-fetch
# or
bun add @metorial/util-react-fetchUsage
Basic Data Loading
import { createLoader } from '@metorial/util-react-fetch';
// Create a loader for user data
let userLoader = createLoader({
name: 'user',
fetch: async (userId: string) => {
let response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
},
hash: (userId: string) => `user-${userId}`,
mutators: {
updateUser: async (data: { name: string }, ctx) => {
let response = await fetch(`/api/users/${ctx.input}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('Failed to update user');
let updatedUser = await response.json();
// Optimistically update the cache
ctx.setOutput(updatedUser);
return updatedUser;
}
}
});
// Use in a React component
function UserProfile({ userId }: { userId: string }) {
let { data: user, error, isLoading, mutators } = userLoader.use(userId);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={() => mutators.updateUser({ name: 'New Name' })}>
Update Name
</button>
</div>
);
}Advanced Loading with Polling
import { createLoader } from '@metorial/util-react-fetch';
// Create a loader with polling for real-time data
let stockPriceLoader = createLoader({
name: 'stock-price',
fetch: async (symbol: string) => {
let response = await fetch(`/api/stocks/${symbol}/price`);
if (!response.ok) throw new Error('Failed to fetch stock price');
return response.json();
},
hash: (symbol: string) => `stock-${symbol}`,
polling: {
interval: 30000 // Refresh every 30 seconds
},
onSuccess: (data) => {
console.log(`Stock price updated: ${data.price}`);
},
onError: (error) => {
console.error('Failed to fetch stock price:', error);
}
});
function StockPrice({ symbol }: { symbol: string }) {
let { data, error, isLoading } = stockPriceLoader.use(symbol);
if (isLoading) return <div>Loading price...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>{symbol}</h2>
<p>Price: ${data?.price}</p>
<p>Last updated: {data?.timestamp}</p>
</div>
);
}Mutation Hooks
import { useMutation } from '@metorial/util-react-fetch';
function CreateUserForm() {
let createUserMutation = useMutation(
async (userData: { name: string; email: string }) => {
let response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) throw new Error('Failed to create user');
return response.json();
}
);
let handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
let formData = new FormData(event.target as HTMLFormElement);
let [user, error] = await createUserMutation.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string
});
if (error) {
console.error('Failed to create user:', error);
} else {
console.log('User created:', user);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={createUserMutation.isLoading}>
{createUserMutation.isLoading ? 'Creating...' : 'Create User'}
</button>
{createUserMutation.isSuccess && (
<div className="success">User created successfully!</div>
)}
<createUserMutation.RenderError component={ErrorDisplay} />
</form>
);
}
function ErrorDisplay({ children }: { children: string }) {
return <div className="error">{children}</div>;
}Complex Data Relationships
import { createLoader } from '@metorial/util-react-fetch';
// Create loaders with parent-child relationships
let postsLoader = createLoader({
name: 'posts',
fetch: async (userId: string) => {
let response = await fetch(`/api/users/${userId}/posts`);
if (!response.ok) throw new Error('Failed to fetch posts');
return response.json();
},
hash: (userId: string) => `posts-${userId}`,
mutators: {
createPost: async (data: { title: string; content: string }, ctx) => {
let response = await fetch(`/api/users/${ctx.input}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('Failed to create post');
return response.json();
}
}
});
let userLoader = createLoader({
name: 'user',
fetch: async (userId: string) => {
let response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
},
hash: (userId: string) => `user-${userId}`,
parents: [postsLoader] // When user data changes, refetch posts
});
function UserDashboard({ userId }: { userId: string }) {
let { data: user, error: userError } = userLoader.use(userId);
let { data: posts, error: postsError, mutators } = postsLoader.use(userId);
if (userError) return <div>Error loading user: {userError.message}</div>;
if (postsError) return <div>Error loading posts: {postsError.message}</div>;
if (!user || !posts) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}'s Dashboard</h1>
<h2>Posts ({posts.length})</h2>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
<button onClick={() => mutators.createPost({
title: 'New Post',
content: 'Post content'
})}>
Create Post
</button>
</div>
);
}Global Error and Toast Handlers
import { setMetorialReactHandlersGlobal } from '@metorial/util-react-fetch';
// Set up global error and toast handlers
setMetorialReactHandlersGlobal({
onError: (error) => {
console.error('Global error:', error);
// Send to error tracking service
analytics.track('error', { message: error.message });
},
onToast: (level, message) => {
// Integrate with your toast library
if (level === 'error') {
toast.error(message);
} else if (level === 'success') {
toast.success(message);
} else {
toast.info(message);
}
}
});
// Use in your app
function App() {
return (
<div>
<UserProfile userId="123" />
<StockPrice symbol="AAPL" />
</div>
);
}Manual Data Fetching
import { createLoader } from '@metorial/util-react-fetch';
let userLoader = createLoader({
name: 'user',
fetch: async (userId: string) => {
let response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
},
hash: (userId: string) => `user-${userId}`
});
// Manual fetching outside of React components
async function fetchUserData(userId: string) {
// Fetch and wait for data
let user = await userLoader.waitFor(userId);
console.log('User data:', user);
// Or fetch and return immediately
let state = await userLoader.fetch(userId);
console.log('Current state:', state.value);
// Subscribe to changes
let unsubscribe = userLoader.subscribe(userId, state => {
console.log('State changed:', state);
});
// Clean up subscription
unsubscribe();
}API Reference
createLoader(options)
Creates a data loader with caching, polling, and mutation support.
Parameters:
options.name: Unique name for the loaderoptions.fetch: Function to fetch dataoptions.hash: Function to generate cache keysoptions.polling: Optional polling configurationoptions.mutators: Optional mutation functionsoptions.onError: Optional error handleroptions.onSuccess: Optional success handleroptions.parents: Optional parent loaders for refetching
Returns: Loader object with use, fetch, waitFor, etc.
useMutation(mutator, options?)
Creates a mutation hook for handling data mutations.
Parameters:
mutator: Function that performs the mutationoptions.disableToast: Whether to disable automatic toast notifications
Returns: Mutation object with mutate, isLoading, error, etc.
setMetorialReactHandlersGlobal(handlers)
Sets up global error and toast handlers.
Parameters:
handlers.onError: Global error handlerhandlers.onToast: Global toast handler
Loader Instance Methods
use(input): React hook for using the loaderfetch(input): Manually fetch datawaitFor(input): Wait for data to be availablegetState(input): Get current statesubscribe(input, callback): Subscribe to state changesrefetchAll(): Refetch all active instances
Mutation Hook Properties
mutate(input): Execute the mutationisLoading: Whether mutation is in progressisSuccess: Whether last mutation was successfulerror: Error from last mutationdata: Data from last successful mutationRenderError: Component for rendering errors
License
MIT License - see LICENSE file for details.
