createComponent
The createComponent function helps you build interactive parts of your app in Refract. Think of components as building blocks - like buttons, forms, or entire pages. When you create a component, it automatically updates when its data changes.
How to use it
createComponent((props) => JSX.Element)
What you need to provide
You need to give createComponent a function that describes what your component should look like and do.
The component function
- What it is: A function that gets props (including a special
lensobject) and returns JSX - Required: Yes
- Type:
(props: Props & { lens: Lens }) => JSX.Element
The lens object gives you access to Refract's reactive features like state management and effects.
What you get back
You get a component that you can use anywhere in your app - in JSX code or pass to other Refract functions.
Let's see some examples
Here are some ways to use createComponent in real projects.
A simple greeting component
import { createComponent } from 'refract';
const Greeting = createComponent(({ lens, name }) => {
return <h1>Hello, {name}!</h1>;
});
// Usage
<Greeting name="World" />
A counter with changing numbers
This example shows how to create a component that remembers and changes data.
const Counter = createComponent(({ lens, initialCount = 0 }) => {
// Create a number that can change over time
const count = lens.useRefraction(initialCount);
// Functions to change the number
const increment = () => count.set(count.value + 1);
const decrement = () => count.set(count.value - 1);
return (
<div>
<h2>Count: {count.value}</h2>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
});
A component that loads data from the internet
This shows how to fetch user information when the component starts up.
const UserProfile = createComponent(({ lens, userId }) => {
// Store the user data, loading state, and any errors
const user = lens.useRefraction(null);
const loading = lens.useRefraction(true);
const error = lens.useRefraction(null);
// This runs when the component starts or when userId changes
lens.useEffect(() => {
const fetchUser = async () => {
try {
loading.set(true);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
user.set(userData);
} catch (err) {
error.set(err.message);
} finally {
loading.set(false);
}
};
fetchUser();
}, [userId]); // Only run when userId changes
// Show different things based on the current state
if (loading.value) return <div>Loading...</div>;
if (error.value) return <div>Error: {error.value}</div>;
return (
<div>
<h2>{user.value?.name}</h2>
<p>{user.value?.email}</p>
</div>
);
});
Component Props
Lens Parameter
Every component receives a lens parameter that provides access to reactive features:
const MyComponent = createComponent(({ lens, ...otherProps }) => {
// lens.useRefraction() - Create reactive state
// lens.useEffect() - Handle side effects
// lens.useOptic() - Use reusable logic
// lens.useDerived() - Create computed values
// lens.batch() - Batch multiple updates
return <div>Component content</div>;
});
Custom Props
Components can receive any custom props:
const BlogPost = createComponent(({ lens, title, content, author, publishedAt }) => {
const isExpanded = lens.useRefraction(false);
return (
<article>
<h2>{title}</h2>
<p>By {author} on {publishedAt}</p>
<div>
{isExpanded.value ? content : `${content.substring(0, 100)}...`}
<button onClick={() => isExpanded.set(!isExpanded.value)}>
{isExpanded.value ? 'Show Less' : 'Show More'}
</button>
</div>
</article>
);
});
// Usage
<BlogPost
title="Getting Started with Refract"
content="Refract is a reactive JavaScript framework..."
author="Jane Doe"
publishedAt="2024-01-15"
/>
Advanced Patterns
Higher-Order Components
const withLoading = (WrappedComponent) => {
return createComponent((props) => {
const { lens, isLoading, loadingText = 'Loading...', ...otherProps } = props;
if (isLoading) {
return <div className="loading">{loadingText}</div>;
}
return <WrappedComponent lens={lens} {...otherProps} />;
});
};
// Usage
const DataDisplay = createComponent(({ lens, data }) => {
return <div>{JSON.stringify(data)}</div>;
});
const DataDisplayWithLoading = withLoading(DataDisplay);
<DataDisplayWithLoading
isLoading={!data}
data={data}
loadingText="Fetching data..."
/>
Render Props Pattern
const DataFetcher = createComponent(({ lens, url, children }) => {
const data = lens.useRefraction(null);
const loading = lens.useRefraction(true);
const error = lens.useRefraction(null);
lens.useEffect(() => {
fetch(url)
.then(response => response.json())
.then(result => {
data.set(result);
loading.set(false);
})
.catch(err => {
error.set(err);
loading.set(false);
});
}, [url]);
return children({
data: data.value,
loading: loading.value,
error: error.value
});
});
// Usage
<DataFetcher url="/api/users">
{({ data, loading, error }) => {
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error.message}</div>;
return <UserList users={data} />;
}}
</DataFetcher>
Compound Components
const Tabs = createComponent(({ lens, children, defaultTab = 0 }) => {
const activeTab = lens.useRefraction(defaultTab);
return (
<div className="tabs">
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
isActive: index === activeTab.value,
onActivate: () => activeTab.set(index)
})
)}
</div>
);
});
const Tab = createComponent(({ lens, title, children, isActive, onActivate }) => {
return (
<div className="tab">
<button
className={`tab-button ${isActive ? 'active' : ''}`}
onClick={onActivate}
>
{title}
</button>
{isActive && (
<div className="tab-content">
{children}
</div>
)}
</div>
);
});
// Usage
<Tabs defaultTab={0}>
<Tab title="Profile">
<UserProfile />
</Tab>
<Tab title="Settings">
<UserSettings />
</Tab>
</Tabs>
Performance Optimization
memo()
Prevent unnecessary re-renders with memoization:
import { memo } from 'refract';
const ExpensiveComponent = memo(createComponent(({ lens, data }) => {
// Expensive computation or rendering
const processedData = lens.useDerived(() => {
return data.map(item => expensiveTransform(item));
}, [data]);
return (
<div>
{processedData.value.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}));
// Custom comparison function
const CustomMemoComponent = memo(
createComponent(({ lens, user }) => {
return <div>{user.name}</div>;
}),
(prevProps, nextProps) => {
// Only re-render if user ID changes
return prevProps.user.id === nextProps.user.id;
}
);
lazy()
Code-split components for better performance:
import { lazy, Suspense } from 'refract';
const LazyComponent = lazy(() => import('./HeavyComponent'));
const App = createComponent(({ lens }) => {
const showHeavy = lens.useRefraction(false);
return (
<div>
<h1>My App</h1>
<button onClick={() => showHeavy.set(!showHeavy.value)}>
Toggle Heavy Component
</button>
{showHeavy.value && (
<Suspense fallback={<div>Loading heavy component...</div>}>
<LazyComponent />
</Suspense>
)}
</div>
);
});
Component Lifecycle
Mount Phase
const LifecycleComponent = createComponent(({ lens }) => {
const data = lens.useRefraction(null);
// Runs once when component mounts
lens.useEffect(() => {
console.log('Component mounted');
// Fetch initial data
fetchData().then(data.set);
// Cleanup on unmount
return () => {
console.log('Component unmounting');
};
}, []); // Empty dependency array
return <div>{data.value || 'Loading...'}</div>;
});
Update Phase
const UpdateComponent = createComponent(({ lens, userId }) => {
const user = lens.useRefraction(null);
// Runs when userId changes
lens.useEffect(() => {
console.log('UserId changed:', userId);
fetchUser(userId).then(user.set);
}, [userId]); // Dependency array with userId
return <div>{user.value?.name}</div>;
});
Error Handling
Component Error Boundaries
const ErrorBoundary = createComponent(({ lens, children }) => {
const hasError = lens.useRefraction(false);
const error = lens.useRefraction(null);
// This would be handled by Refract's error system
if (hasError.value) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{error.value?.stack}</pre>
</details>
<button onClick={() => hasError.set(false)}>
Try again
</button>
</div>
);
}
return children;
});
Safe Async Operations
const SafeAsyncComponent = createComponent(({ lens }) => {
const data = lens.useRefraction(null);
const error = lens.useRefraction(null);
lens.useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
const result = await apiCall();
if (!cancelled) {
data.set(result);
}
} catch (err) {
if (!cancelled) {
error.set(err.message);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, []);
if (error.value) {
return <div>Error: {error.value}</div>;
}
return <div>{data.value || 'Loading...'}</div>;
});
Testing Components
Unit Testing
import { render, fireEvent } from '@refract/testing-utils';
describe('Counter Component', () => {
test('increments count when button clicked', () => {
const { getByText, getByRole } = render(<Counter initialCount={0} />);
expect(getByText('Count: 0')).toBeInTheDocument();
fireEvent.click(getByRole('button', { name: '+' }));
expect(getByText('Count: 1')).toBeInTheDocument();
});
test('decrements count when button clicked', () => {
const { getByText, getByRole } = render(<Counter initialCount={5} />);
expect(getByText('Count: 5')).toBeInTheDocument();
fireEvent.click(getByRole('button', { name: '-' }));
expect(getByText('Count: 4')).toBeInTheDocument();
});
});
Integration Testing
test('UserProfile fetches and displays user data', async () => {
const mockUser = { id: 1, name: 'John Doe', email: 'john@example.com' };
fetch.mockResolvedValueOnce({
json: async () => mockUser
});
const { getByText, queryByText } = render(<UserProfile userId={1} />);
// Initially shows loading
expect(getByText('Loading...')).toBeInTheDocument();
// Wait for data to load
await waitFor(() => {
expect(queryByText('Loading...')).not.toBeInTheDocument();
expect(getByText('John Doe')).toBeInTheDocument();
expect(getByText('john@example.com')).toBeInTheDocument();
});
});
Best Practices
1. Keep Components Pure
// ✅ Good - Pure component
const PureComponent = createComponent(({ lens, name }) => {
return <h1>Hello, {name}!</h1>;
});
// ❌ Bad - Side effects in render
const ImpureComponent = createComponent(({ lens, name }) => {
console.log('Rendering...'); // Side effect!
return <h1>Hello, {name}!</h1>;
});
2. Use Descriptive Names
// ✅ Good
const UserProfileCard = createComponent(({ lens, user }) => {
// Component logic
});
// ❌ Bad
const Component1 = createComponent(({ lens, data }) => {
// Component logic
});