Architecting a Modular Product Announcement Framework in React

UI Engineer currently simplifying the process of building generative AI bots @yellow.ai. On a journey to make the web a better place by building engaging and performant UIs.
A streamlined announcement system for showing contextual notifications and announcements across the platform.
Features
๐ฏ Smart Tracking: Tracks view counts and respects user limits
๐ Global Access: Trigger announcements from anywhere
๐ฑ Flexible Content: Any React component
๐๏ธ Conditional Display: Show based on paths or custom conditions
๐พ Persistent Storage: User preferences stored in localStorage (can be extended to a database)
Architecture: Three Simple Pieces
๐ง The Brain (
announcementService) โ Decides when and where to show announcements, tracks user interactions, and prevents overexposure๐ The Connector (
AnnouncementProvider) โ Gives any component a simpleuseAnnouncement()hook to trigger announcements๐๏ธ The Display (
AnnouncementModal) โ Renders announcements consistently using your existing modal system
Why this works:
Pluggable: Any component can show announcements without setup
Flexible: Route targeting, custom conditions, delays, force-show for testing
Safe: Built-in view limits and permanent dismissal options
๐ง The Brain: How AnnouncementService manages announcements?
Singleton service for managing announcement tracking and storage.
This service handles:
Announcement lifecycle: Managing announcement lifecycle (shown, dismissed, reset)
Storing: User preferences and dismissal states in localStorage
When to show?: Determining if announcements can be shown based on conditions and limits
Persistent Memory
// Persists announcement state to localStorage
private saveToStorage(): void {
try {
localStorage.setItem(
ANNOUNCEMENT_TRACKING_DATA_LS_KEY,
JSON.stringify(this.storage)
);
} catch (error) {
console.error("Failed to save announcement tracking data:", error);
}
}
// Load announcement state
private loadFromStorage(): void {
try {
const stored = localStorage.getItem(ANNOUNCEMENT_TRACKING_DATA_LS_KEY);
if (stored) {
this.storage = JSON.parse(stored) ?? {};
}
} catch (error) {
console.error("Failed to load announcement tracking data:", error);
this.storage = {}; // Graceful fallback
}
}
// Records when an announement has been shown
public recordAnnouncementShown(announcementId: string): void {
const trackingData = this.getTrackingData(announcementId);
this.storage[announcementId] = {
...trackingData,
viewedCount: trackingData.viewedCount + 1,
lastShownAt: Date.now(),
};
this.saveToStorage();
}
/**
* Records that an announcement has been dismissed by the user
* Can mark as permanently dismissed or just temporarily closed
*
* @param announcementId - Unique identifier for the announcement
* @param permanent - If true, announcement will never be shown again (default: false)
*/
public recordAnnouncementDismissed(announcementId: string, permanent = false): void {
const trackingData = this.getTrackingData(announcementId);
this.storage[announcementId] = {
...trackingData,
dismissed: permanent,
dismissedAt: Date.now(),
};
this.saveToStorage();
}
Why this matters: Users stay logged in across sessions. Without persistence, they'd see the same announcements every time they visit. In a real-world scenario, this data would be persisted in a database.
Note: saveToStorage and loadFromStorage are private methods, to prevent access from outside.
When to show an announcement?
public canShowAnnouncement(announcement: AnnouncementConfig): boolean {
const trackingData = this.getTrackingData(announcement.id);
// Already permanently dismissed? Respect that choice
if (trackingData.dismissed) return false;
// Hit the view limit? Don't spam
const maxViewCount = announcement.maxViewCount ?? 3;
if (trackingData.viewedCount >= maxViewCount) return false;
// Custom condition failed? (wrong user type, feature disabled, etc)
if (typeof announcement.condition === "function" && !announcement.condition()) {
return false;
}
// Wrong page? Don't show
if (announcement.targetPath) {
const currentPath = window.location.pathname;
if (announcement.targetPath instanceof RegExp) {
if (!announcement.targetPath.test(currentPath)) return false;
} else if (!currentPath.includes(announcement.targetPath)) {
return false;
}
}
return true; // All checks passed!
}
Why this matters: One function prevents all common announcement mistakes - showing too often, on the wrong pages, to the wrong users, or after they've said "never show this again."
Get announcement stats or reset existing state
/**
* Gets complete statistics for a specific announcement
*
* @param announcementId - Unique identifier for the announcement
* @returns Complete tracking data including view count, timestamps, and dismissal status
*/
public getAnnouncementStats(announcementId: string): AnnouncementTrackingData {
return this.getTrackingData(announcementId);
}
/**
* Resets all tracking data for a specific announcement
* Useful for testing or when user requests to see an announcement again
*
* @param announcementId - Unique identifier for the announcement
*/
public resetAnnouncementTracking(announcementId: string): void {
delete this.storage[announcementId];
this.saveToStorage();
}
Why this matters: Developers need to test, campaigns get relaunched, and sometimes you need to override the rules. This gives you an escape hatch without breaking the system.
๐ง The Announcement Configuration
Every announcement is defined by an AnnouncementConfig object. Think of it as your announcement's DNA:
export interface AnnouncementConfig {
id: string; // Unique identifier for tracking
content: ReactNode; // What to show (your React component)
modalProps?: ModalProps; // How to style the modal
maxViewCount?: number; // How many times to show (default: 3)
showDelay?: number; // Delay before showing (milliseconds)
targetPath?: string | RegExp; // Which pages to show on
condition?: () => boolean; // Custom logic for when to show
onShow?: () => void; // Called when shown (for analytics)
onOk?: () => Promise<boolean> | boolean | void; // Handle "OK" button
onDismiss?: (dismissedAfterOk?: boolean) => boolean; // Handle dismissal
}
What Each Field Does (And Why You Need It)
id - Your announcement's unique identifier, consider it like the key from React Query
Used for tracking views, dismissals, and preventing duplicates
Make it descriptive:
"feature-launch-v1"not"popup1"
content - The actual announcement UI
Pass any React component:
<WelcomeMessage />or<div>Hello!</div>Keep it focused - users scan, don't read
maxViewCount - Prevents announcement fatigue
Default is 3 times max per user
Set to 1 for critical one-time messages
Set higher for tips users might need to see multiple times
targetPath - Show only where it matters
String:
"/dashboard"shows on any dashboard pageRegex:
/\/project\/\d+/for specific URL patternsSkip for global announcements
condition - Your smart targeting logic
Check user type:
() => user.isPremiumFeature flags:
() => isFeatureEnabled('newEditor')Time-based:
() => Date.now() < campaignEndDate
showDelay - Reduces jarring interruptions
Give users 500ms to orient before showing
Especially important on page loads
onOk / onDismiss - Handle user actions
onOkreturningtruecloses the modal automaticallyonDismissreturningtruemarks it permanently dismissed
๐ The Connector: The AnnouncementProvider and useAnnouncement Hook
The AnnouncementProvider wraps your app and exposes a dead-simple hook that any component can use. It acts as a bridge between your application and the AnnouncementService and renders a modal to show the announcement content.
// Setup once at your app root
function App() {
return (
<AnnouncementProvider>
<YourAppContent />
</AnnouncementProvider>
);
}
// Use anywhere in your app
function AnyComponent() {
const { showAnnouncement } = useAnnouncement();
}
The Hook
The useAnnouncement hook exposes mainly three essential functions:
1. showAnnouncement()
This is your primary tool - 90% of use cases only need this function.
const handleShowAnnouncement = useCallback(
(announcement: AnnouncementConfig, forceShow?: boolean) => {
if (forceShow) {
setActiveAnnouncement(announcement);
return;
}
// check if announcement can be shown
if (!announcementService.canShowAnnouncement(announcement)) {
console.warn(
`Announcement "${announcement.id}" cannot be shown due to conditions or limits`
);
return;
}
// show the announcement after some delay(if specified)
const delay = announcement.showDelay || 0;
setTimeout(() => {
setActiveAnnouncement((prev) => {
if (prev?.id !== announcement.id) {
// this is to prevent showing the same announcement multiple times
return announcement;
}
return prev;
});
announcementService.recordAnnouncementShown(announcement.id);
announcement.onShow?.();
}, delay);
},
[]
);
Usage:
import { useAnnouncement } from "components/Announcement/AnnouncementProvider";
export function FeatureIntro() {
const { showAnnouncement } = useAnnouncement();
const handleShowWelcome = () => {
showAnnouncement({
id: "dashboard-welcome-v1",
content: <div>Welcome to your new dashboard! Heres what changed...</div>,
maxViewCount: 2, // Show max twice
showDelay: 500, // Wait half second after page load
targetPath: "/dashboard", // Only on dashboard
onOk: () => true, // Close when they click OK
onDismiss: (afterOk) => afterOk, // Permanent only if they clicked OK
});
};
return <button onClick={handleShowWelcome}>Dashboard V2</button>;
}

What happens behind the scenes:
Checks conditions - Validates routes, view limits, custom logic
Respects delays - Waits for
showDelaybefore appearingTracks automatically - Records that the user saw this announcement
Handles conflicts - Won't show if another announcement is active
Fails gracefully - Logs warnings instead of breaking your app
Advanced usage with force override:
// Sometimes you need to bypass all rules (testing, admin overrides)
showAnnouncement(config, true);
2. dismissAnnouncement() - Programmatic Control
Need to close an announcement programmatically before the user dismisses it? This function gives you the control to do that
const handleDismissAnnouncement = useCallback(
(announcementId: string, permanent?: boolean) => {
// track the dismissal
announcementService.recordAnnouncementDismissed(
announcementId,
permanent
);
// close the modal
setActiveAnnouncement(null);
},
[]
);
Usage:
const { dismissAnnouncement } = useAnnouncement();
// Close an announcement
dismissAnnouncement("announcement-id");
// Close permanently (user will never see it again)
dismissAnnouncement("announcement-id", true);
When you need this:
User completes an action that makes the announcement irrelevant
Time-based dismissal (auto-close after 10 seconds)
Error handling (close announcement when something goes wrong)
3. updateAnnouncementConfig() - Dynamic Updates
Perfect for multi-step announcements or when content needs to change for the active announcement on the fly.
const handleUpdateAnnouncementConfig = useCallback(
(announcement: AnnouncementConfig) => {
setActiveAnnouncement((prev) => {
// should only update the existing announcement
if (prev?.id !== announcement.id) {
return prev;
}
return announcement;
});
},
[]
);
Real-World Example: Two-Step Feature Announcement
const TWO_STEP_ANNOUNCEMENT_ID = "two-step-announcement-v1";
function OnboardingFlow() {
const { showAnnouncement, updateAnnouncementConfig } = useAnnouncement();
const moveToStep2 = () => {
updateAnnouncementConfig({
id: TWO_STEP_ANNOUNCEMENT_ID,
content: (
<div className="space-y-2 h-[472px]">
<ul className="list-disc pl-5">
<li>No disruption to current analytics</li>
<li>Existing reports will remain accessible</li>
</ul>
</div>
),
modalProps: {
title: "Review and Accept",
okText: "Confirm Upgrade",
cancelText: "Cancel",
},
onOk: () => true, // close on final step
});
};
const showTwoStepAnnouncement = () => {
const step1Config: AnnouncementConfig = {
id: TWO_STEP_ANNOUNCEMENT_ID,
content: (
<div className="space-y-2">
<p className="font-medium">Introducing Web Analytics</p>
<p>
All new analytics dashboard, built to track your website traffic.
Click Next to review changes.
</p>
</div>
),
modalProps: { title: "New Feature", okText: "Next" },
onOk: () => {
moveToStep2();
return false; // do not close, move to next step
},
onDismiss: () => false,
};
showAnnouncement(step1Config);
};
}

When you need this:
Multi-step tutorials or onboarding flows
Dynamic content based on user actions
Progress indicators that update in real-time
Form wizards within announcements
Complete Hook API
const {
// Core functions (what you'll use 90% of the time)
showAnnouncement, // Show an announcement
dismissAnnouncement, // Close programmatically
updateAnnouncementConfig, // Update content on the fly
// State inspection
activeAnnouncement, // What's currently showing
// Advanced utilities
canShowAnnouncement, // Check if announcement would show
getAnnouncementStats, // View tracking data
resetAnnouncementTracking // Clear all tracking (dev/testing)
} = useAnnouncement();
Common Pitfalls
โ Announcement Fatigue
// Bad: No limits
{ id: "tip-1", content: <Tip />, maxViewCount: 999 }
// Good: Reasonable limits
{ id: "tip-1", content: <Tip />, maxViewCount: 3 }
โ Jarring Interruptions
// Bad: Shows immediately on page load
{ id: "welcome", content: <Welcome />, showDelay: 0 }
// Good: Let them orient first
{ id: "welcome", content: <Welcome />, showDelay: 1000 }
โ Wrong Place, Wrong Time
// Bad: Global announcement for specific feature
{ id: "editor-tip", content: <EditorTip /> }
// Good: Targeted to where it matters
{
id: "editor-tip",
content: <EditorTip />,
targetPath: "/editor",
condition: () => user.hasProject && !user.hasUsedEditor
}
โ Unstable IDs
// Bad: Changes every render
{ id: `welcome-${Math.random()}`, content: <Welcome /> }
// Good: Stable, descriptive IDs
{ id: "welcome-dashboard-v2", content: <Welcome /> }
โ Best Practices
Namespace your IDs:
feature-name-versionnotpopup1Start conservative: Better to show too little than too much
Respect dismissals: If they said no, honor it
Keep content scannable: Users don't read, they glance
Target precisely: Wrong audience = ignored announcements
Route-Based Targeting
// Simple contains check targetPath: "/analytics/dashbaord" // Shows only on dashboard page // Exact regex matching targetPath: /\/project\/\d+\/settings$/ // Only project settings pages // Multiple routes condition: () => { const path = window.location.pathname; return path.includes('/dashboard') || path.includes('/reports'); }User & Feature Targeting
// User type targeting condition: () => user.role === 'admin' && user.isActive // Feature flag integration condition: () => isFeatureEnabled('newEditor') // Time-sensitive campaigns condition: () => { const now = Date.now(); const campaignStart = new Date('2024-01-01').getTime(); const campaignEnd = new Date('2024-02-01').getTime(); return now >= campaignStart && now < campaignEnd; }
Sample Nextjs project code
That's it! You now have a bulletproof system for shipping announcements that users actually appreciate instead of reflexively closing. Keep building ๐๏ธ



