| name | react-composition |
| description | Build composable React components using Effect Atom for state management. Use this skill when implementing React UIs that avoid boolean props, embrace component composition, and integrate with Effect's reactive state system. |
React Composition Skill
Build React UIs using compositional patterns, Effect Atom for state management, and the component module pattern. Use this skill when creating React applications that integrate with Effect's ecosystem.
When to Use This Skill
- Building React components that integrate with Effect Atom state
- Refactoring components away from boolean prop anti-patterns
- Implementing complex UIs through composition of simple pieces
- Creating reusable component libraries with flexible APIs
- Managing shared state across multiple React components
- Lifting state to appropriate levels in component trees
Core Principles
1. Composition over Configuration
Build complex UIs from simple, composable pieces rather than configuring behavior through props.
Anti-Pattern: Boolean Props
interface FormProps {
isUpdate?: boolean
hideWelcome?: boolean
showEmail?: boolean
redirectOnSuccess?: boolean
enableValidation?: boolean
}
function UserForm({
isUpdate,
hideWelcome,
showEmail,
redirectOnSuccess,
enableValidation
}: FormProps) {
return (
<form>
{!hideWelcome && <WelcomeMessage />}
<NameField />
{showEmail && <EmailField />}
<button type="submit">
{isUpdate ? "Update" : "Create"}
</button>
</form>
)
}
<UserForm
isUpdate
hideWelcome
showEmail
redirectOnSuccess
/>
Correct Pattern: Composition
export namespace UserForm {
export const Frame: React.FC<{ children: React.ReactNode }> =
({ children }) => <form className="user-form">{children}</form>
export const WelcomeMessage: React.FC = () =>
<div className="welcome">Welcome!</div>
export const NameField: React.FC = () =>
<input name="name" placeholder="Name" />
export const EmailField: React.FC = () =>
<input type="email" name="email" placeholder="Email" />
export const SubmitButton: React.FC<{ children: React.ReactNode }> =
({ children }) => <button type="submit">{children}</button>
}
function CreateUserForm() {
return (
<UserForm.Frame>
<UserForm.WelcomeMessage />
<UserForm.NameField />
<UserForm.EmailField />
<UserForm.SubmitButton>Create</UserForm.SubmitButton>
</UserForm.Frame>
)
}
function UpdateUserForm() {
return (
<UserForm.Frame>
<UserForm.NameField />
<UserForm.SubmitButton>Update</UserForm.SubmitButton>
</UserForm.Frame>
)
}
<CreateUserForm />
<UpdateUserForm />
2. Component Module Pattern
Treat components like Effect modules with namespace imports and exported sub-components.
import * as React from "react"
export interface ComposerState {
readonly content: string
readonly attachments: ReadonlyArray<Attachment>
readonly isSubmitting: boolean
}
const ComposerContext = React.createContext<ComposerState | null>(null)
export const useComposer = (): ComposerState => {
const context = React.useContext(ComposerContext)
if (!context) {
throw new Error("useComposer must be used within Composer.Provider")
}
return context
}
export const Provider: React.FC<{
children: React.ReactNode
state: ComposerState
}> = ({ children, state }) => (
<ComposerContext.Provider value={state}>
{children}
</ComposerContext.Provider>
)
export const Frame: React.FC<{
children: React.ReactNode
}> = ({ children }) => (
<div className="composer-frame">
{children}
</div>
)
export const Input: React.FC = () => {
const { content } = useComposer()
return (
<textarea
value={content}
className="composer-input"
placeholder="Type a message..."
/>
)
}
export const Footer: React.FC<{
children: React.ReactNode
}> = ({ children }) => (
<div className="composer-footer">
{children}
</div>
)
export const Submit: React.FC = () => {
const { isSubmitting } = useComposer()
return (
<button
type="submit"
disabled={isSubmitting}
className="composer-submit"
>
{isSubmitting ? "Sending..." : "Send"}
</button>
)
}
Usage with Namespace Import
import * as Composer from "@/components/Composer"
function MessageComposer() {
const [state, setState] = useState<Composer.ComposerState>({
content: "",
attachments: [],
isSubmitting: false
})
return (
<Composer.Provider state={state}>
<Composer.Frame>
<Composer.Input />
<Composer.Footer>
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
</Composer.Provider>
)
}
3. State Lifting Pattern
Lift state ABOVE all components that need access, not below.
Anti-Pattern: State Too Low
function Modal() {
return (
<ModalFrame>
<ModalContent>
<Composer /> {/* State is here */}
</ModalContent>
<ModalFooter>
<ExternalButton /> {/* Cannot access Composer state! */}
</ModalFooter>
</ModalFrame>
)
}
Correct Pattern: Lift State Above
function Modal() {
const [state, setState] = useState(initialComposerState)
return (
<Composer.Provider state={state}>
<ModalFrame>
<ModalContent>
<Composer.Input />
</ModalContent>
<ModalFooter>
<ExternalButton /> {/* Can access state via useComposer! */}
<Composer.Submit />
</ModalFooter>
</ModalFrame>
</Composer.Provider>
)
}
function ExternalButton() {
const { content } = Composer.useComposer()
const hasContent = content.length > 0
return (
<button disabled={!hasContent}>
Preview
</button>
)
}
Effect Atom Integration
Effect Atom provides reactive state management that integrates seamlessly with React.
Pattern: Basic Atom State
import * as Atom from "@effect-atom/atom-react"
import { Effect } from "effect"
export interface CartItem {
readonly id: string
readonly name: string
readonly price: number
readonly quantity: number
}
export interface CartState {
readonly items: ReadonlyArray<CartItem>
readonly total: number
}
export const cart = Atom.make<CartState>({
items: [],
total: 0
})
export const itemCount = Atom.map(cart, (c) => c.items.length)
export const isEmpty = Atom.map(cart, (c) => c.items.length === 0)
export const addItem = Atom.fn(
Effect.fnUntraced(function* (item: CartItem) {
const current = yield* Atom.get(cart)
const existingIndex = current.items.findIndex(i => i.id === item.id)
if (existingIndex >= 0) {
const updatedItems = [...current.items]
updatedItems[existingIndex] = {
...updatedItems[existingIndex],
quantity: updatedItems[existingIndex].quantity + item.quantity
}
yield* Atom.set(cart, {
items: updatedItems,
total: current.total + (item.price * item.quantity)
})
} else {
yield* Atom.set(cart, {
items: [...current.items, item],
total: current.total + (item.price * item.quantity)
})
}
})
)
export const removeItem = Atom.fn(
Effect.fnUntraced(function* (itemId: string) {
const current = yield* Atom.get(cart)
const item = current.items.find(i => i.id === itemId)
if (!item) return
yield* Atom.set(cart, {
items: current.items.filter(i => i.id !== itemId),
total: current.total - (item.price * item.quantity)
})
})
)
export const clearCart = Atom.fn(
Effect.fnUntraced(function* () {
yield* Atom.set(cart, { items: [], total: 0 })
})
)
Pattern: React Component with Atoms
import { useAtomValue, useAtomSet } from "@effect-atom/atom-react"
import * as Cart from "@/state/Cart"
export function CartView() {
const cartData = useAtomValue(Cart.cart)
const count = useAtomValue(Cart.itemCount)
const empty = useAtomValue(Cart.isEmpty)
const removeItem = useAtomSet(Cart.removeItem)
const clearCart = useAtomSet(Cart.clearCart)
if (empty) {
return <div className="cart-empty">Your cart is empty</div>
}
return (
<div className="cart">
<h2>Cart ({count} items)</h2>
<ul className="cart-items">
{cartData.items.map(item => (
<li key={item.id} className="cart-item">
<span>{item.name}</span>
<span>{item.quantity} x ${item.price}</span>
<button onClick={() => removeItem(item.id)}>
Remove
</button>
</li>
))}
</ul>
<div className="cart-total">
Total: ${cartData.total}
</div>
<button onClick={() => clearCart()}>
Clear Cart
</button>
</div>
)
}
Pattern: Separation of Concerns
Different components can read/write the same atom reactively:
function CartBadge() {
const count = useAtomValue(Cart.itemCount)
return (
<div className="cart-badge">
{count > 0 && <span>{count}</span>}
</div>
)
}
function AddToCartButton({ item }: { item: CartItem }) {
const addItem = useAtomSet(Cart.addItem)
return (
<button onClick={() => addItem(item)}>
Add to Cart
</button>
)
}
function CartControls() {
const [cartState, setCart] = useAtom(Cart.cart)
return (
<div>
<span>Items: {cartState.items.length}</span>
<button onClick={() => setCart({ items: [], total: 0 })}>
Reset
</button>
</div>
)
}
Pattern: Async Operations with Atoms
import * as Atom from "@effect-atom/atom-react"
import * as Result from "@effect-atom/atom/Result"
import { Effect, Layer } from "effect"
import type { UserService } from "@/services/UserService"
export const userData = Atom.make<Result.Result<User, Error>>(
Result.initial
)
const runtime = Atom.runtime(UserService.Live)
export const loadUser = runtime.fn(
Effect.fnUntraced(function* (userId: string) {
const userService = yield* UserService
yield* Atom.set(userData, Result.initial)
const result = yield* Effect.either(
userService.getUser(userId)
)
yield* Atom.set(
userData,
result._tag === "Right"
? Result.success(result.right)
: Result.failure(result.left)
)
})
)
Component with Result Handling
import { useAtomValue, useAtomSetPromise } from "@effect-atom/atom-react"
import * as Result from "@effect-atom/atom/Result"
import * as User from "@/state/User"
function UserProfile({ userId }: { userId: string }) {
const result = useAtomValue(User.userData)
const loadUser = useAtomSetPromise(User.loadUser)
React.useEffect(() => {
loadUser(userId)
}, [userId, loadUser])
return Result.match(result, {
Initial: () => <Loading />,
Failure: (error) => <Error message={error.message} />,
Success: (user) => (
<div className="user-profile">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
})
}
Avoiding useEffect
Most useEffect usage is wrong. Consider these alternatives:
Anti-Pattern: Unnecessary Effects
function UserCard({ user }: { user: User }) {
const [fullName, setFullName] = useState("")
useEffect(() => {
setFullName(`${user.firstName} ${user.lastName}`)
}, [user])
return <div>{fullName}</div>
}
function UserCard({ user }: { user: User }) {
const fullName = `${user.firstName} ${user.lastName}`
return <div>{fullName}</div>
}
Anti-Pattern: Effect for Expensive Computation
function ProductList({ products }: { products: Product[] }) {
const [filtered, setFiltered] = useState<Product[]>([])
useEffect(() => {
setFiltered(products.filter(expensiveFilter))
}, [products])
return <div>{filtered.map(renderProduct)}</div>
}
function ProductList({ products }: { products: Product[] }) {
const filtered = useMemo(
() => products.filter(expensiveFilter),
[products]
)
return <div>{filtered.map(renderProduct)}</div>
}
Pattern: useTransition for Non-Blocking Updates
import { useTransition } from "react"
function SearchResults() {
const [query, setQuery] = useState("")
const [isPending, startTransition] = useTransition()
const handleSearch = (value: string) => {
setQuery(value)
startTransition(() => {
updateSearchResults(value)
})
}
return (
<div>
<input onChange={e => handleSearch(e.target.value)} />
{isPending && <LoadingSpinner />}
<Results />
</div>
)
}
Pattern: Keys for Resetting State
function UserEditor({ userId }: { userId: string }) {
const [formData, setFormData] = useState(initialData)
useEffect(() => {
setFormData(initialData)
}, [userId])
return <form>...</form>
}
function UserEditor({ userId }: { userId: string }) {
return <UserEditorForm key={userId} />
}
function UserEditorForm() {
const [formData, setFormData] = useState(initialData)
return <form>...</form>
}
When useEffect is Appropriate
Use useEffect for:
- Synchronizing with external systems (WebSocket, DOM APIs)
- Side effects that must run after render
- Cleanup of subscriptions
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const connection = connectToRoom(roomId)
return () => {
connection.disconnect()
}
}, [roomId])
return <div>...</div>
}
Advanced Patterns
Pattern: Render Props for Flexibility
export namespace DataTable {
export interface RenderProps<T> {
readonly data: ReadonlyArray<T>
readonly isLoading: boolean
}
export const Frame: React.FC<{
children: React.ReactNode
}> = ({ children }) => (
<div className="data-table">{children}</div>
)
export const Header: React.FC<{
columns: ReadonlyArray<string>
}> = ({ columns }) => (
<thead>
<tr>
{columns.map(col => <th key={col}>{col}</th>)}
</tr>
</thead>
)
export const Body: React.FC<{
children: (props: RenderProps<T>) => React.ReactNode
}> = ({ children, data, isLoading }) => (
<tbody>
{children({ data, isLoading })}
</tbody>
)
}
function UserTable() {
const users = useAtomValue(User.list)
return (
<DataTable.Frame>
<DataTable.Header columns={["Name", "Email"]} />
<DataTable.Body>
{({ data, isLoading }) =>
isLoading ? (
<LoadingRow />
) : (
data.map(user => <UserRow key={user.id} user={user} />)
)
}
</DataTable.Body>
</DataTable.Frame>
)
}
Pattern: Compound Components with Context
export namespace Tabs {
interface TabsContext {
readonly activeTab: string
readonly setActiveTab: (tab: string) => void
}
const Context = React.createContext<TabsContext | null>(null)
export const Provider: React.FC<{
children: React.ReactNode
defaultTab: string
}> = ({ children, defaultTab }) => {
const [activeTab, setActiveTab] = useState(defaultTab)
return (
<Context.Provider value={{ activeTab, setActiveTab }}>
{children}
</Context.Provider>
)
}
export const List: React.FC<{
children: React.ReactNode
}> = ({ children }) => (
<div role="tablist">{children}</div>
)
export const Tab: React.FC<{
id: string
children: React.ReactNode
}> = ({ id, children }) => {
const context = React.useContext(Context)
if (!context) throw new Error("Tab must be within Provider")
const isActive = context.activeTab === id
return (
<button
role="tab"
aria-selected={isActive}
onClick={() => context.setActiveTab(id)}
>
{children}
</button>
)
}
export const Panel: React.FC<{
id: string
children: React.ReactNode
}> = ({ id, children }) => {
const context = React.useContext(Context)
if (!context) throw new Error("Panel must be within Provider")
if (context.activeTab !== id) return null
return (
<div role="tabpanel">{children}</div>
)
}
}
function Settings() {
return (
<Tabs.Provider defaultTab="general">
<Tabs.List>
<Tabs.Tab id="general">General</Tabs.Tab>
<Tabs.Tab id="security">Security</Tabs.Tab>
<Tabs.Tab id="notifications">Notifications</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="general">
<GeneralSettings />
</Tabs.Panel>
<Tabs.Panel id="security">
<SecuritySettings />
</Tabs.Panel>
<Tabs.Panel id="notifications">
<NotificationSettings />
</Tabs.Panel>
</Tabs.Provider>
)
}
Testing Compositional Components
Pattern: Test Atomic Components
import { render, screen } from "@testing-library/react"
import * as Composer from "@/components/Composer"
describe("Composer.Input", () => {
it("displays content from context", () => {
const state: Composer.ComposerState = {
content: "Hello world",
attachments: [],
isSubmitting: false
}
render(
<Composer.Provider state={state}>
<Composer.Input />
</Composer.Provider>
)
expect(screen.getByDisplayValue("Hello world")).toBeInTheDocument()
})
})
Pattern: Test Composed Features
describe("MessageComposer", () => {
it("composes correctly", () => {
render(<MessageComposer />)
expect(screen.getByRole("textbox")).toBeInTheDocument()
expect(screen.getByRole("button", { name: "Send" })).toBeInTheDocument()
})
it("disables submit when submitting", () => {
const state: Composer.ComposerState = {
content: "Hello",
attachments: [],
isSubmitting: true
}
render(
<Composer.Provider state={state}>
<Composer.Submit />
</Composer.Provider>
)
expect(screen.getByRole("button")).toBeDisabled()
})
})
Quality Checklist
When implementing React components with Effect Atom, ensure:
Common Mistakes
Mistake: Mixing Context and Atoms
const AppContext = React.createContext<AppState | null>(null)
function App() {
const [state, setState] = useState(appState)
return (
<AppContext.Provider value={state}>
{/* Deep component tree */}
</AppContext.Provider>
)
}
export const appState = Atom.make<AppState>(initialState)
function DeepComponent() {
const state = useAtomValue(appState)
return <div>{state.value}</div>
}
Mistake: Not Lifting State High Enough
function Modal() {
return (
<div className="modal">
<Editor /> {/* State is here */}
<Footer>
<SaveButton /> {/* Cannot access Editor state */}
</Footer>
</div>
)
}
function ModalContainer() {
const editorState = useAtomValue(Editor.state)
return (
<Editor.Provider state={editorState}>
<Modal>
<Editor.Input />
<Footer>
<SaveButton /> {/* Can access state */}
</Footer>
</Modal>
</Editor.Provider>
)
}
Mistake: Overusing useEffect
function OrderSummary({ order }: { order: Order }) {
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(order.items.reduce((sum, item) => sum + item.price, 0))
}, [order])
return <div>Total: ${total}</div>
}
function OrderSummary({ order }: { order: Order }) {
const total = order.items.reduce((sum, item) => sum + item.price, 0)
return <div>Total: ${total}</div>
}
Summary
Build React applications that:
- Compose simple components into complex features
- Avoid configuration through boolean props
- Lift state to appropriate levels
- Use Effect Atom for reactive state management
- Organize components in namespaces like Effect modules
- Minimize
useEffect usage in favor of direct calculation
- Handle errors explicitly with Result types
- Test components in isolation
This approach creates flexible, maintainable UIs that integrate seamlessly with Effect's ecosystem while following React best practices.