Advanced Patterns
An earlier release
These pages document @evanion/react-widget 0.2.0. npm install @evanion/react-widget gives you 0.3.3, documented here. Values on this page were produced by running 0.2.0 in CI at d50a1c6 and are not re-executed.
Advanced usage patterns for the React Widget System including custom chrome, nested widgets, and performance optimization.
Custom Chrome (Wrappers)
Chrome refers to the wrapper components that provide styling and layout structure around your widgets.
Custom Wrapper
import { createWidgets } from '@evanion/react-widget';
const { Widgets } = createWidgets({
components: {
news: NewsTeaser,
userInfo: UserSidebar,
},
chrome: {
wrapper: ({ children }) => (
<aside className="blog-sidebar">
<header>Latest Updates</header>
<div className="widget-container">{children}</div>
</aside>
),
},
});Custom Item Wrapper
const { Widgets } = createWidgets({
components: {
news: NewsTeaser,
},
chrome: {
item: ({ children, ...props }) => (
<div
className="widget-item"
data-widget-id={props['data-widget-id']}
data-widget-type={props['data-widget-type']}
>
<div className="widget-content">{children}</div>
</div>
),
},
});Both Wrappers
const { Widgets } = createWidgets({
components: {
news: NewsTeaser,
userInfo: UserSidebar,
},
chrome: {
wrapper: ({ children }) => (
<section className="widget-section">
<h2>Widget Section</h2>
{children}
</section>
),
item: ({ children }) => (
<article className="widget-article">{children}</article>
),
},
});Nested Widgets with Output
The Output component allows widgets to render nested widgets within themselves.
Basic Nested Structure
import { createWidgets } from '@evanion/react-widget';
const CardWidget = ({ title, Output }) => (
<div className="card">
<h3>{title}</h3>
<div className="card-content">
<Output />
</div>
</div>
);
const TextWidget = ({ content }) => <p>{content}</p>;
const ImageWidget = ({ src, alt }) => <img src={src} alt={alt} />;
const { Widgets } = createWidgets({
components: {
card: CardWidget,
text: TextWidget,
image: ImageWidget,
},
});
// Nested widget data
const nestedItems = [
{
id: 'card1',
type: 'card',
props: {
title: 'My Card',
},
children: [
{
id: 'text1',
type: 'text',
props: { content: 'Nested text content' },
},
{
id: 'image1',
type: 'image',
props: {
src: '/image.jpg',
alt: 'Nested image',
},
},
],
},
];Complex Nested Layouts
const LayoutWidget = ({ title, sidebar, main, Output }) => (
<div className="layout">
<header>{title}</header>
<div className="layout-content">
<aside className="sidebar">
<Output />
</aside>
<main className="main-content">{main}</main>
</div>
</div>
);
const SidebarWidget = ({ items, Output }) => (
<nav className="sidebar-nav">
{items.map((item) => (
<div key={item.id} className="nav-item">
<a href={item.href}>{item.label}</a>
</div>
))}
<Output />
</nav>
);
const { Widgets } = createWidgets({
components: {
layout: LayoutWidget,
sidebar: SidebarWidget,
text: TextWidget,
},
});
const complexItems = [
{
id: 'layout1',
type: 'layout',
props: {
title: 'My App',
main: 'Main content here',
},
children: [
{
id: 'sidebar1',
type: 'sidebar',
props: {
items: [
{ id: 'home', label: 'Home', href: '/' },
{ id: 'about', label: 'About', href: '/about' },
],
},
children: [
{
id: 'text1',
type: 'text',
props: { content: 'Additional sidebar content' },
},
],
},
],
},
];Instance-Specific Overrides
You can override components and chrome for specific instances of the Widgets component.
Component Overrides
const { Widgets } = createWidgets({
components: {
news: NewsTeaser,
},
});
// Default news component
const NewsTeaser = ({ title, publishedAt, body }) => (
<article className="news-teaser">
<h3>{title}</h3>
<time>{publishedAt.toLocaleDateString()}</time>
<p>{body}</p>
</article>
);
// Special featured news component
const FeaturedNews = ({ title, publishedAt, body }) => (
<div className="featured-news">
<h2>Featured: {title}</h2>
<div className="featured-content">
<time>{publishedAt.toLocaleDateString()}</time>
<p>{body}</p>
</div>
</div>
);
// Use different components for different pages
function HomePage() {
return <Widgets items={newsItems} components={{ news: FeaturedNews }} />;
}
function BlogPage() {
return <Widgets items={newsItems} />; // Uses default NewsTeaser
}Chrome Overrides
const { Widgets } = createWidgets({
components: {
news: NewsTeaser,
},
chrome: {
wrapper: ({ children }) => (
<div className="default-wrapper">Default: {children}</div>
),
},
});
function SpecialPage() {
return (
<Widgets
items={newsItems}
chrome={{
wrapper: ({ children }) => (
<div className="special-wrapper">Special: {children}</div>
),
}}
/>
);
}Performance Optimization
Memoized Components
import { memo } from 'react';
const ExpensiveWidget = memo(({ data, onUpdate }) => {
// Expensive computation
const processedData = useMemo(() => {
return data.map((item) => ({
...item,
processed: expensiveProcessing(item),
}));
}, [data]);
return (
<div>
{processedData.map((item) => (
<div key={item.id}>{item.processed}</div>
))}
</div>
);
});
const { Widgets } = createWidgets({
components: {
expensive: ExpensiveWidget,
},
});Lazy Loading
import { lazy, Suspense } from 'react';
const LazyWidget = lazy(() => import('./LazyWidget'));
const { Widgets } = createWidgets({
components: {
lazy: LazyWidget,
},
});
// The DefaultItem wrapper already includes Suspense
// with a loading fallbackContext Optimization
import { createContext, useContext } from 'react';
// Create a custom context for better performance
const CustomContext = createContext();
const { Widgets, WidgetsProvider } = createWidgets({
components: {
news: NewsTeaser,
},
context: CustomContext,
});
// Use the custom context
const useCustomWidgets = () => useContext(CustomContext);Error Boundaries and Error Handling
Custom Error Boundaries
import { Component } from 'react';
class CustomErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log to error reporting service
console.error('Widget Error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h3>Something went wrong</h3>
<p>This widget failed to render.</p>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
const { Widgets } = createWidgets({
components: {
news: NewsTeaser,
},
chrome: {
item: ({ children, ...props }) => (
<CustomErrorBoundary>
<div className="widget-item" {...props}>
{children}
</div>
</CustomErrorBoundary>
),
},
});Error Recovery
const ResilientWidget = ({ data, fallback }) => {
try {
if (!data) {
throw new Error('No data provided');
}
return <div>{data.content}</div>;
} catch (error) {
return (
<div className="error-state">{fallback || 'Failed to load widget'}</div>
);
}
};TypeScript Advanced Patterns
Strict Typing
import { createWidgets, WidgetProps } from '@evanion/react-widget';
// Define strict widget types
type MyWidgets = {
news: WidgetProps<
'news',
{
title: string;
publishedAt: Date;
body: string;
}
>;
userInfo: WidgetProps<
'userInfo',
{
username: string;
avatar: string;
messages: number;
}
>;
card: WidgetProps<
'card',
{
title: string;
Output: React.ComponentType;
}
>;
};
// Create typed widget system
const { Widgets } = createWidgets<MyWidgets>({
components: {
news: NewsTeaser,
userInfo: UserSidebar,
card: CardWidget,
},
});
// Type-safe widget data
const typedItems: WidgetProps<string>[] = [
{
id: 'news1',
type: 'news', // TypeScript will enforce this matches MyWidgets
props: {
title: 'Breaking News',
publishedAt: new Date(),
body: 'Content here',
},
},
];Generic Widget Components
interface GenericWidgetProps<T> {
data: T;
render: (data: T) => React.ReactNode;
}
const GenericWidget = <T,>({ data, render }: GenericWidgetProps<T>) => {
return <div className="generic-widget">{render(data)}</div>;
};
const { Widgets } = createWidgets({
components: {
generic: GenericWidget,
},
});Last updated on