Overview
The Utils library (@vanilla /injectables/Utils) is a curated collection of utility functions, React hooks, and helper methods available to custom widgets and fragments in Vanilla's Widget Builder system. This library provides a safe, standardized way to access common functionality while building custom widgets.
What is the Utils Library?
The Utils library is an injectable dependency that gets automatically exposed to your custom widget fragments through the @vanilla /injectables/Utilsimport. It contains carefully selected methods built by the Vanilla team that are safe to use in the widget builder environment and provides access to:
- User context and permissions
- Navigation and routing
- Internationalization and formatting
- Data fetching capabilities
- DOM measurement utilities
- Styling helpers
- Application metadata
How to Use the Utils Library
In your custom fragment templates, import the Utils library like this:
import Utils from "@vanilla /injectables/Utils";
export default function MyCustomFragment(props) {
// Use any Utils method
const currentUser = Utils.useCurrentUser();
const navigate = Utils.useLinkNavigator();
return ( <div>
<h1>{Utils.t("Hello, {name}!", { name: currentUser.name })}</h1>
<button onClick={() => navigate("/discussions")}>{Utils.t("Go to Discussions")}</button>
</div> );
}
Available Methods
Navigation & Routing
useLinkNavigator()
Type
: () => (url: string) => void
What it does: Returns a smart navigation function that automatically determines whether to use fast in-app navigation or a full page refresh based on the destination URL.
Why it's useful for forum widgets: When building interactive forum widgets, you'll often want to navigate users to different pages (discussions, categories, user profiles, etc.). This function provides the best user experience by using fast client-side navigation when possible (staying within the same section of the forum) and falling back to full page loads when necessary (external links or different sections).
Perfect for: Creating clickable discussion lists, category navigation, user profile links, "Read More" buttons, and any other interactive elements that need to redirect users.
Usage:
```tsx
const navigate = Utils.useLinkNavigator();
// Fast navigation within the forum
navigate("/discussions/123/my-discussion");
navigate("/categories/general");
navigate("/profile/john-doe");
// Full page load for external links
navigate("https://external-site.com");
// Example in a discussion widget
const handleDiscussionClick = (discussionID) => {
navigate(`/discussions/${discussionID}`);
};
```
User Context & Permissions
useCurrentUser() Type: () => IUser | null
What it does: Returns the complete user object for the currently logged-in user, or null if no user is signed in.
Why it's useful for forum widgets: Many forum widgets need to personalize content based on who's viewing them. This hook gives you access to the user's profile information, preferences, and metadata so you can create personalized experiences.
Perfect for: Welcome messages, user avatars, personalized content recommendations, displaying user-specific stats, showing "My Discussions" vs "All Discussions", and any widget that needs to adapt to the current user.
Usage
const currentUser = Utils.useCurrentUser();
if (currentUser) {
return (
<div>
<img src={currentUser.photoUrl} alt={currentUser.name} />
<span>Welcome back, {currentUser.name}!</span>
<p>You have {currentUser.countDiscussions} discussions</p>
</div>
);
} else {
return <div>Please sign in to see personalized content</div>;
}
useCurrentUserSignedIn() Type: () => boolean
What it does: Returns a simple boolean indicating whether someone is currently logged in, without loading the full user object.
Why it's useful for forum widgets: This is more efficient than useCurrentUser() when you only need to know if someone is signed in. It's perfect for conditional rendering of content, buttons, or features that require authentication.
Perfect for: Showing/hiding "Sign In" buttons, displaying different content for guests vs members, enabling/disabling interactive features, and any simple authentication checks.
Usage:
const isSignedIn = Utils.useCurrentUserSignedIn();
return (
<div>
{isSignedIn ? (
<button onClick={handleCreateDiscussion}>Start New Discussion</button>
) : (
<p>
<a href="/entry/signin">Sign in</a> to start discussions
</p>
)}
</div>
);
usePermissionsContext() Type: () => IPermissionsContext
What it does: Returns the current user's permissions context, allowing you to check what specific actions they're allowed to perform in the forum.
Why it's useful for forum widgets: Different users have different permissions in a forum (guests, members, moderators, admins). This hook lets you show/hide buttons, features, or content based on what the user is actually allowed to do.
Perfect for: Hiding "Delete" buttons from non-moderators, showing admin-only widgets, enabling/disabling comment features, displaying moderation tools, and any permission-based functionality.
Usage:
const permissions = Utils.usePermissionsContext();
return (
<div>
{permissions.hasPermission("discussions.add") && <button>Create Discussion</button>}
{permissions.hasPermission("discussions.edit") && <button>Edit Discussion</button>}
{permissions.hasPermission("community.moderate") && (
<div className="moderation-tools">
<button>Pin Discussion</button>
<button>Close Discussion</button>
</div>
)}
</div>
);
Internationalization & Formatting
t(translationKey, options?) Type: (key: string, options?: Record<string, any>) => string
What it does: Translates text into the user's preferred language using the forum's translation system. Supports variable substitution and pluralization rules. Note the string must already exist in our translation library for the translation to work.
Why it's useful for forum widgets: Forums often serve international communities with users speaking different languages. This function ensures your widget displays text in the user's chosen language, making it accessible to a global audience.
Perfect for: All user-facing text in your widgets - button labels, headings, error messages, descriptions, tooltips, and any content that users will read. Never hardcode English text!
// Basic translation
const title = Utils.t("Welcome");
// With variables - great for personalized messages
const greeting = Utils.t("Hello, {name}!", { name: currentUser.name });
// With pluralization - handles singular/plural correctly in any language
const postCount = Utils.t("You have {count} {count, plural, one {post} other {posts}}", { count: userPosts });
// Common forum examples
const labels = {
discussions: Utils.t("Discussions"),
comments: Utils.t("Comments"),
loadMore: Utils.t("Load More"),
signIn: Utils.t("Sign In"),
lastActivity: Utils.t("Last Activity: {date}", { date: lastActiveDate }),
}
formatNumber(number, options?) Type: (number: number, options?: Intl.NumberFormatOptions) => string
What it does: Formats numbers according to the user's locale and your specified formatting rules (currency, percentages, decimals, etc.).
Why it's useful for forum widgets: Forums deal with lots of numbers - view counts, user counts, dates, scores, etc. This ensures numbers are displayed in the format users expect in their region.
Perfect for: Displaying view counts, user statistics, currency amounts, percentages, ratings, and any numerical data in your widgets.
Usage:
// Basic number formatting
const views = Utils.formatNumber(1234); // "1,234" (US) or "1.234" (EU)
// Currency formatting
const price = Utils.formatNumber(1234.56, {
style: "currency",
currency: "USD",
}); // "$1,234.56"
// Percentage formatting
const successRate = Utils.formatNumber(0.75, {
style: "percent",
}); // "75%"
// Forum-specific examples
const stats = {
members: Utils.formatNumber(userCount),
discussions: Utils.formatNumber(discussionCount),
upvotePercentage: Utils.formatNumber(upvotes / totalVotes, { style: "percent" }),
};
formatNumberCompact(number)
Type: (number: number) => string
What it does: Formats large numbers in a compact, human-readable form using abbreviations (K for thousands, M for millions, etc.).
Why it's useful for forum widgets: Forums often have large numbers that look overwhelming when fully displayed. This makes big numbers more digestible and saves space in your widget UI.
Perfect for: View counts, member counts, post counts, likes, and any large numbers where space is limited or you want to improve readability.
Usage:
// Large numbers become readable
const views = Utils.formatNumberCompact(1234); // "1.2K"
const members = Utils.formatNumberCompact(15000); // "15K"
const megaViews = Utils.formatNumberCompact(1500000); // "1.5M"
// Perfect for compact displays
return (
<div className="forum-stats">
<span>{Utils.formatNumberCompact(discussion.countViews)} views</span>
<span>{Utils.formatNumberCompact(discussion.countComments)} replies</span>
<span>{Utils.formatNumberCompact(category.countDiscussions)} discussions</span>
</div>
);
getCurrentLocale() Type: `() => string`
What it does: Returns the current user's locale/language code (e.g., "en", "fr", "es", "de").
Why it's useful for forum widgets: Sometimes you need to adjust widget behavior based on the user's language - different date formats, text direction, or locale-specific features.
Perfect for: Conditional logic based on language, integrating with third-party services that need locale information, debugging translation issues, or customizing widget behavior per region.
Usage:
const locale = Utils.getCurrentLocale(); // "en"
// Conditional behavior based on locale
const isRTL = ["ar", "he", "ur"].includes(locale);
const dateFormat = locale === "en" ? "MM/DD/YYYY" : "DD/MM/YYYY";
// Integrating with external services
const mapWidget = <ExternalMap language={locale} region={locale.split("-")[1]} />;
Data Fetching (React Query)
useQuery(options) Type: (options: UseQueryOptions) => UseQueryResult
What it does: Fetches data from APIs with automatic caching, background updates, retry logic, and loading states. This is the primary way to get data in your widgets.
Why it's useful for forum widgets: Forum widgets need to display live data - discussions, comments, user info, categories, etc. This hook automatically handles all the complex parts of data fetching: loading states, error handling, caching for performance, and keeping data fresh.
Perfect for: Loading discussion lists, user profiles, category information, search results, notifications, recent activity, and any data that comes from the forum's API.
Usage:
// Basic data fetching for a discussions widget
const {
data: discussions,
isLoading,
error,
refetch,
} = Utils.useQuery({
queryKey: ["discussions", "recent"],
queryFn: async () => {
const response = await fetch("/api/v2/discussions?limit=10&sort=-dateLastComment");
if (!response.ok) throw new Error("Failed to fetch discussions");
return response.json();
},
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
refetchOnWindowFocus: true, // Refetch when user returns to tab
});
// Handle different states
if (isLoading) return <div>{Utils.t("Loading discussions...")}</div>;
if (error) return <div>{Utils.t("Error loading discussions")}</div>;
// Render the data
return (
<div>
<button onClick={refetch}>{Utils.t("Refresh")}</button>
{discussions.map((discussion) => (
<div key={discussion.discussionID}>
<h3>{discussion.name}</h3>
<p>{Utils.formatNumberCompact(discussion.countViews)} views</p>
</div>
))}
</div>
);
useMutation(options) Type: (options: UseMutationOptions) => UseMutationResult
What it does: Handles data modifications (creating, updating, deleting) with automatic loading states, error handling, and optimistic updates.
Why it's useful for forum widgets: Interactive widgets need to let users perform actions - like posts, vote on content, follow categories, etc. This hook manages all the complexity of sending data to the server and handling success/failure states.
Perfect for: Creating posts, voting/liking content, following users, bookmarking discussions, reporting content, and any user actions that modify data.
Usage:
// Example: Like/unlike a discussion
const likeMutation = Utils.useMutation({
mutationFn: async ({ discussionID, liked }) => {
const method = liked ? "PUT" : "DELETE";
const response = await fetch(`/api/v2/discussions/${discussionID}/reactions`, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reactionType: "like" }),
});
if (!response.ok) throw new Error("Failed to update reaction");
return response.json();
},
onSuccess: () => {
// Refetch discussions to show updated like count
queryClient.invalidateQueries(["discussions"]);
},
onError: (error) => {
alert(Utils.t("Error: {message}", { message: error.message }));
},
});
// Use in your widget
const handleLike = (discussionID, currentlyLiked) => {
likeMutation.mutate({
discussionID,
liked: !currentlyLiked,
});
};
return (
<button
onClick={() => handleLike(discussion.discussionID, discussion.userReacted)}
disabled={likeMutation.isLoading}
>
{likeMutation.isLoading ? Utils.t("Updating...") : discussion.userReacted ? Utils.t("Unlike") : Utils.t("Like")}
</button>
);
useQueryClient() Type: () => QueryClient
What it does: Returns the React Query client instance for advanced cache management, allowing you to manually control cached data.
Why it's useful for forum widgets: Sometimes you need fine-grained control over cached data - updating it after mutations, prefetching data before it's needed, or clearing stale data
Perfect for: Optimistic updates, prefetching data on hover, clearing cache after user actions, and coordinating data between multiple widgets.
Usage:
const optimisticLike = Utils.useMutation({
mutationFn: likeDiscussion,
onMutate: async (variables) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries(["discussions"]);
// Snapshot the previous value
const previousDiscussions = queryClient.getQueryData(["discussions"]);
// Optimistically update the UI
queryClient.setQueryData(["discussions"], (old) => {
return old.map((d) =>
d.discussionID === variables.discussionID
? { ...d, userReacted: !d.userReacted, score: d.score + (d.userReacted ? -1 : 1) }
: d,
);
});
return { previousDiscussions };
},
onError: (err, variables, context) => {
// Rollback on error
queryClient.setQueryData(["discussions"], context.previousDiscussions);
},
});
// Prefetch data on hover for better UX
const handleDiscussionHover = (discussionID) => {
queryClient.prefetchQuery({
queryKey: ["discussion", discussionID],
queryFn: () => fetch(`/api/v2/discussions/${discussionID}`).then((r) => r.json()),
staleTime: 10 * 60 * 1000, // 10 minutes
});
};
DOM & Measurement
useMeasure() Type: () => [React.RefCallback<Element>, DOMRect]
What it does: Measures the actual dimensions and position of a DOM element in real-time, automatically updating when the element resizes.
Why it's useful for forum widgets: Forum widgets need to adapt to different layouts, screen sizes, and content lengths. This hook helps you build responsive widgets that adjust their behavior based on available space.
Perfect for: Responsive layouts, conditional rendering based on size, positioning tooltips/dropdowns, creating adaptive navigation, and optimizing content display for different widget sizes.
Usage:
const [measureRef, bounds] = Utils.useMeasure();
// Adapt widget layout based on available width
const isCompact = bounds.width < 300;
const showFullDetails = bounds.width > 600;
return (
<div ref={measureRef} className="discussion-widget">
{/* Responsive layout based on measured width */}
{isCompact ? (
// Compact view for narrow spaces
<div className="compact-view">
<h4>{discussion.name}</h4>
<span>{Utils.formatNumberCompact(discussion.countViews)}</span>
</div>
) : (
// Full view for wider spaces
<div className="full-view">
<h3>{discussion.name}</h3>
<p>{discussion.excerpt}</p>
<div className="meta">
<span>{Utils.formatNumber(discussion.countViews)} views</span>
<span>{Utils.formatNumber(discussion.countComments)} replies</span>
{showFullDetails && <span>Last activity: {discussion.dateLastComment}</span>}
</div>
</div>
)}
{/* Debug info (remove in production) */}
<small>
Widget size: {Math.round(bounds.width)} x {Math.round(bounds.height)}
</small>
</div>
);
useIsOverflowing() Type: () => [React.RefCallback<Element>, boolean]
What it does: Detects when an element's content is overflowing its container, allowing you to react to content that doesn't fit.
Why it's useful for forum widgets: Forum content varies greatly in length - discussion titles, user names, descriptions, etc. This hook helps you handle overflow gracefully with ellipsis, expandable sections, or scroll areas.
Perfect for: Adding "Show More" buttons, displaying ellipsis indicators, creating expandable content areas, showing scroll hints, and preventing layout breaking from long content.
Usage:
const [contentRef, isOverflowing] = Utils.useIsOverflowing();
const [expanded, setExpanded] = useState(false);
return (
<div className="discussion-card">
<h3>{discussion.name}</h3>
{/* Content that might overflow */}
<div
ref={contentRef}
className={`description ${expanded ? "expanded" : "limited"}`}
style={
expanded
? {}
: {
maxHeight: "60px",
overflow: "hidden",
}
}
>
{discussion.body}
</div>
{/* Show expand/collapse button only when content overflows */}
{isOverflowing && (
<button onClick={() => setExpanded(!expanded)} className="show-more-btn">
{expanded ? Utils.t("Show Less") : Utils.t("Show More")}
</button>
)}
{/* Alternative: Show ellipsis indicator */}
{isOverflowing && !expanded && <span className="overflow-indicator">...</span>}
</div>
// Example: User name that might be too long
const [nameRef, nameOverflowing] = Utils.useIsOverflowing();
return (
<div className="user-info">
<span
ref={nameRef}
className="username"
style={{ maxWidth: "150px", whiteSpace: "nowrap", overflow: "hidden" }}
title={nameOverflowing ? user.name : undefined} // Show full name in tooltip
>
{user.name}
</span>
{nameOverflowing && <span className="overflow-hint">...</span>}
</div>
);
Styling
Css.background(background)
Type: (background: Partial<IBackground> | undefined) => CSSProperties
What it does: Converts a background configuration object into proper CSS style properties, handling all the CSS background properties in a consistent way.
Why it's useful for forum widgets: Forums often allow customizable backgrounds for different sections, categories, or themes. This function ensures your widget backgrounds are applied correctly and consistently with the forum's theming system.
Perfect for: Custom widget backgrounds, category-specific styling, theme-aware widgets, banner backgrounds, and any dynamic background styling based on user preferences or content.
Usage:
// Basic background styling
const backgroundStyle = Utils.Css.background({
color: "#f8f9fa",
image: "url('/uploads/category-banner.jpg')",
repeat: "no-repeat",
position: "center",
size: "cover",
});
// Dynamic background based on category
const categoryBackground = Utils.Css.background({
color: category.backgroundColor || "#ffffff",
image: category.bannerImage ? `url(${category.bannerImage})` : undefined,
attachment: "fixed",
});
// Theme-aware widget background
const getWidgetBackground = (isDark) =>
Utils.Css.background({
color: isDark ? "#1a1a1a" : "#ffffff",
image: isDark ? "url('/dark-pattern.png')" : "url('/light-pattern.png')",
});
return (
<div
className="category-widget"
style={{
...backgroundStyle,
padding: "20px",
borderRadius: "8px",
}}
>
<h3 style={{ color: category.textColor }}>{category.name}</h3>
<p>{category.description}</p>
</div>
);
classnames(...args) Type: (...args: any[]) => string
What it does: Combines multiple CSS class names intelligently, handling conditional classes, arrays, and objects. Filters out falsy values automatically.
Why it's useful for forum widgets: Widgets need dynamic styling based on state, user permissions, content type, etc. This function makes it easy to conditionally apply classes without messy string concatenation.
Perfect for: State-based styling, permission-based classes, responsive classes, theme classes, and any dynamic CSS class management.
Useage:
const currentUser = Utils.useCurrentUser();
const permissions = Utils.usePermissionsContext();
// Dynamic classes based on state and conditions
const discussionClasses = Utils.classnames(
"discussion-item", // Base class
"widget-content", // Widget-specific class
{
"discussion-pinned": discussion.pinned, // Conditional classes
"discussion-closed": discussion.closed,
"discussion-unread": !discussion.read,
"discussion-own": discussion.insertUserID === currentUser?.userID,
"can-moderate": permissions.hasPermission("community.moderate"),
},
discussion.featured && "discussion-featured", // Another conditional approach
`discussion-category-${discussion.categoryID}`, // Dynamic class
);
// Button styling with multiple states
const buttonClasses = Utils.classnames("btn", "btn-discussion", {
"btn-primary": !isLoading && !hasError,
"btn-loading": isLoading,
"btn-error": hasError,
"btn-disabled": !canPost,
});
// Responsive classes
const widgetClasses = Utils.classnames("forum-widget", {
"widget-compact": bounds.width < 400,
"widget-full": bounds.width > 800,
"widget-dark": theme === "dark",
});
return (
<div className={widgetClasses}>
<div className={discussionClasses}>
<h3>{discussion.name}</h3>
<button className={buttonClasses}>{isLoading ? Utils.t("Loading...") : Utils.t("Reply")}</button>
</div>
</div>
);
Application Context
getMeta(key, defaultValue?) Type: (key: string, defaultValue?: any) => any
What it does: Retrieves configuration values, settings, and metadata from the forum's application context - essentially reading the forum's configuration and state.
Why it's useful for forum widgets: Widgets often need to adapt to the forum's configuration - site name, theme settings, feature flags, API endpoints, etc. This function gives you access to the same configuration the forum uses.
Perfect for: Adapting to site settings, checking feature flags, getting API endpoints, customizing behavior per site, and accessing theme/brand information.
Usage:
// Common forum configuration
const siteName = Utils.getMeta("ui.siteName", "Community");
const siteDescription = Utils.getMeta("ui.description", "");
const baseUrl = Utils.getMeta("context.host");
const currentTheme = Utils.getMeta("ui.themeKey");
// Feature flags - check if features are enabled
const isSearchEnabled = Utils.getMeta("ui.search.enabled", true);
const allowGuestPosting = Utils.getMeta("Garden.Registration.Method") !== "Invitation";
const isKnowledgeEnabled = Utils.getMeta("ui.knowledge.enabled", false);
// User/session information
const currentUserID = Utils.getMeta("context.userID", 0);
const userPermissions = Utils.getMeta("context.permissions", []);
// Branding and customization
const logoUrl = Utils.getMeta("ui.logo.desktop.url");
const primaryColor = Utils.getMeta("ui.color.primary");
const customCSS = Utils.getMeta("ui.customCSS");
// Widget adaptation based on configuration
return (
<div className="forum-widget">
<header>
<h2>
{siteName} - {Utils.t("Latest Discussions")}
</h2>
{siteDescription && <p>{siteDescription}</p>}
</header>
{isSearchEnabled && (
<div className="search-bar">
<input placeholder={Utils.t("Search discussions...")} />
</div>
)}
<div style={{ color: primaryColor }}>
{discussions.map((discussion) => (
<div key={discussion.discussionID}>
<h3>{discussion.name}</h3>
{allowGuestPosting || currentUserID > 0 ? (
<button>{Utils.t("Reply")}</button>
) : (
<span>{Utils.t("Sign in to reply")}</span>
)}
</div>
))}
</div>
</div>
);
getSiteSection() Type: () => ISiteSection | null
What it does: Returns information about the current section of the forum the user is viewing (like different communities, subcommunities, or site sections).
Why it's useful for forum widgets: Multi-community or multi-section forums need widgets that adapt to the current context. This helps widgets show section-specific content or branding.
Perfect for: Section-specific content, navigation breadcrumbs, context-aware widgets, multi-tenant customization, and section-based filtering.
Useage:
const siteSection = Utils.getSiteSection();
// Adapt widget content based on current section
if (siteSection) {
return (
<div className="section-widget">
<header style={{ backgroundColor: siteSection.color }}>
<h2>
{siteSection.name} - {Utils.t("Recent Activity")}
</h2>
{siteSection.description && <p>{siteSection.description}</p>}
</header>
{/* Show section-specific content */}
<div className="section-content">
<p>{Utils.t("Welcome to {sectionName}!", { sectionName: siteSection.name })}</p>
{/* Section-specific navigation */}
<nav>
<a href={`${siteSection.basePath}/discussions`}>{Utils.t("Discussions")}</a>
<a href={`${siteSection.basePath}/categories`}>{Utils.t("Categories")}</a>
</nav>
</div>
</div>
);
} else {
// Fallback for when not in a specific section
return (
<div className="global-widget">
<h2>{Utils.t("Community Activity")}</h2>
</div>
);
}
createSourceSetValue(imageUrl, sizes?) Type: (imageUrl: string, sizes?: number[]) => string
What it does: Generates responsive image source sets for different screen densities (1x, 2x, 3x) to ensure images look crisp on all devices.
Why it's useful for forum widgets: Forums display lots of images - user avatars, category images, post attachments, etc. This ensures images look great on both regular and high-DPI screens (like Retina displays).
Perfect for: User avatars, category banners, post images, icons, and any images in your widgets that should scale for different screen densities.
Usage:
// Basic responsive image
const avatarSrcSet = Utils.createSourceSetValue(user.photoUrl, [1, 2, 3]);
// Category banner with multiple densities
const bannerSrcSet = Utils.createSourceSetValue(category.bannerImage);
// Custom sizes for different breakpoints
const logoSrcSet = Utils.createSourceSetValue("/uploads/logo.png", [1, 1.5, 2, 3]);
return (
<div className="user-card">
{/* Responsive user avatar */}
<img src={user.photoUrl} srcSet={avatarSrcSet} alt={user.name} className="user-avatar" loading="lazy" />
<div className="user-info">
<h3>{user.name}</h3>
<p>{user.title}</p>
</div>
{/* Category banner with responsive sizing */}
{category.bannerImage && (
<div className="category-banner">
<img
src={category.bannerImage}
srcSet={bannerSrcSet}
alt={category.name}
style={{ width: "100%", height: "auto" }}
/>
</div>
)}
</div>
);
// Advanced usage with custom image processing
const getResponsiveImage = (imageUrl, alt, className) => {
if (!imageUrl) return null;
const srcSet = Utils.createSourceSetValue(imageUrl, [1, 2]);
return (
<img
src={imageUrl}
srcSet={srcSet}
alt={alt}
className={className}
loading="lazy"
onError={(e) => {
// Fallback for broken images
e.target.src = "/default-avatar.png";
}}
/>
);
};
Best Practices
- Always use Utils methods instead of direct imports - This ensures compatibility and security within the widget builder environment.
- Handle loading and error states - When using data fetching methods, always handle loading and error states appropriately.
- Use internationalization - Always use Utils.t() for text that users will see to ensure proper localization.
- Check permissions - Use Utils.usePermissionsContext() to conditionally render content based on user permissions.
- Optimize performance - Use React Query's caching features through Utils.useQuery() to avoid unnecessary API calls.
Example: Complete Widget Using Utils
import Utils from "@vanilla/injectables/Utils";
import Components from "@vanilla/injectables/Components";
export default function MyWidget(props) {
const currentUser = Utils.useCurrentUser();
const navigate = Utils.useLinkNavigator();
const permissions = Utils.usePermissionsContext();
const { data: discussions, isLoading } = Utils.useQuery({
queryKey: ["recent-discussions"],
queryFn: async () => {
const response = await fetch("/api/v2/discussions?limit=5");
return response.json();
},
});
if (isLoading) {
return <div>{Utils.t("Loading...")}</div>;
}
return (
<Components.LayoutWidget>
<h2>{Utils.t("Recent Discussions")}</h2>
{currentUser && <p>{Utils.t("Welcome back, {name}!", { name: currentUser.name })}</p>}
<ul>
{discussions?.map((discussion) => (
<li key={discussion.discussionID}>
<button onClick={() => navigate(discussion.url)}>{discussion.name}</button>
<span>{Utils.formatNumberCompact(discussion.countViews)} views</span>
</li>
))}
</ul>
{permissions.hasPermission("discussions.add") && (
<button onClick={() => navigate("/post/discussion")}>{Utils.t("Start New Discussion")}</button>
)}
</Components.LayoutWidget>
);
}