---
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**
```tsx
// L WRONG - Configuration through boolean props
interface FormProps {
isUpdate?: boolean
hideWelcome?: boolean
showEmail?: boolean
redirectOnSuccess?: boolean
enableValidation?: boolean
}
function UserForm({
isUpdate,
hideWelcome,
showEmail,
redirectOnSuccess,
enableValidation
}: FormProps) {
return (
)
/**
* Submit button component
*/
export const Submit: React.FC = () => {
const { isSubmitting } = useComposer()
return (
)
}
```
**Usage with Namespace Import**
```tsx
import * as Composer from "@/components/Composer"
function MessageComposer() {
const [state, setState] = useState({
content: "",
attachments: [],
isSubmitting: false
})
return (
)
}
```
### 3. State Lifting Pattern
Lift state ABOVE all components that need access, not below.
**Anti-Pattern: State Too Low**
```tsx
// L WRONG - State below components that need it
function Modal() {
return (
{/* State is here */}
{/* Cannot access Composer state! */}
)
}
```
**Correct Pattern: Lift State Above**
```tsx
// CORRECT - State above everything that needs it
function Modal() {
const [state, setState] = useState(initialComposerState)
// Provider wraps EVERYTHING that needs access
return (
{/* Can access state via useComposer! */}
)
}
function ExternalButton() {
const { content } = Composer.useComposer()
const hasContent = content.length > 0
return (
)
}
```
## Effect Atom Integration
Effect Atom provides reactive state management that integrates seamlessly with React.
### Pattern: Basic Atom State
```typescript
// state/Cart.ts
import * as Atom from "@effect-atom/atom-react"
import { Effect } from "effect"
/**
* Cart item interface
*/
export interface CartItem {
readonly id: string
readonly name: string
readonly price: number
readonly quantity: number
}
/**
* Cart state interface
*/
export interface CartState {
readonly items: ReadonlyArray
readonly total: number
}
/**
* Main cart atom
*/
export const cart = Atom.make({
items: [],
total: 0
})
/**
* Derived atom: item count
*/
export const itemCount = Atom.map(cart, (c) => c.items.length)
/**
* Derived atom: is cart empty
*/
export const isEmpty = Atom.map(cart, (c) => c.items.length === 0)
/**
* Add item to cart
*/
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) {
// Update quantity
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 {
// Add new item
yield* Atom.set(cart, {
items: [...current.items, item],
total: current.total + (item.price * item.quantity)
})
}
})
)
/**
* Remove item from cart
*/
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)
})
})
)
/**
* Clear cart
*/
export const clearCart = Atom.fn(
Effect.fnUntraced(function* () {
yield* Atom.set(cart, { items: [], total: 0 })
})
)
```
### Pattern: React Component with Atoms
```tsx
// components/Cart/CartView.tsx
import { useAtomValue, useAtomSet } from "@effect-atom/atom-react"
import * as Cart from "@/state/Cart"
/**
* Cart display component
*/
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
Your cart is empty
}
return (
Cart ({count} items)
{cartData.items.map(item => (
{item.name}{item.quantity} x ${item.price}
))}
Total: ${cartData.total}
)
}
```
### Pattern: Separation of Concerns
Different components can read/write the same atom reactively:
```tsx
// Component A - Read only
function CartBadge() {
const count = useAtomValue(Cart.itemCount)
return (
{count > 0 && {count}}
)
}
// Component B - Write only
function AddToCartButton({ item }: { item: CartItem }) {
const addItem = useAtomSet(Cart.addItem)
return (
)
}
// Component C - Read and write
function CartControls() {
const [cartState, setCart] = useAtom(Cart.cart)
return (
Items: {cartState.items.length}
)
}
// All components update reactively when atom changes
```
### Pattern: Async Operations with Atoms
```typescript
// state/User.ts
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"
/**
* User data with Result type for error handling
*/
export const userData = Atom.make>(
Result.initial
)
/**
* Runtime with UserService
*/
const runtime = Atom.runtime(UserService.Live)
/**
* Load user data
*/
export const loadUser = runtime.fn(
Effect.fnUntraced(function* (userId: string) {
const userService = yield* UserService
// Set loading state
yield* Atom.set(userData, Result.initial)
// Fetch data
const result = yield* Effect.either(
userService.getUser(userId)
)
// Update atom with result
yield* Atom.set(
userData,
result._tag === "Right"
? Result.success(result.right)
: Result.failure(result.left)
)
})
)
```
**Component with Result Handling**
```tsx
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: () => ,
Failure: (error) => ,
Success: (user) => (
{user.name}
{user.email}
)
})
}
```
## Avoiding useEffect
Most `useEffect` usage is wrong. Consider these alternatives:
### Anti-Pattern: Unnecessary Effects
```tsx
// L WRONG - Using effect for derived state
function UserCard({ user }: { user: User }) {
const [fullName, setFullName] = useState("")
useEffect(() => {
setFullName(`${user.firstName} ${user.lastName}`)
}, [user])
return
{fullName}
}
// CORRECT - Calculate during render
function UserCard({ user }: { user: User }) {
const fullName = `${user.firstName} ${user.lastName}`
return
{fullName}
}
```
### Anti-Pattern: Effect for Expensive Computation
```tsx
// L WRONG - Effect for memoization
function ProductList({ products }: { products: Product[] }) {
const [filtered, setFiltered] = useState([])
useEffect(() => {
setFiltered(products.filter(expensiveFilter))
}, [products])
return
)
}
```
### Pattern: Keys for Resetting State
```tsx
// L WRONG - Effect to reset state
function UserEditor({ userId }: { userId: string }) {
const [formData, setFormData] = useState(initialData)
useEffect(() => {
setFormData(initialData) // Reset on user change
}, [userId])
return
}
// CORRECT - Use key to reset component
function UserEditor({ userId }: { userId: string }) {
return
}
function UserEditorForm() {
const [formData, setFormData] = useState(initialData)
// State automatically resets when key changes
return
}
```
### When useEffect is Appropriate
Use `useEffect` for:
- Synchronizing with external systems (WebSocket, DOM APIs)
- Side effects that must run after render
- Cleanup of subscriptions
```tsx
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
// Connect to external system
const connection = connectToRoom(roomId)
// Cleanup on unmount or roomId change
return () => {
connection.disconnect()
}
}, [roomId])
return
)
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 (
)
}
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 (
{children}
)
}
}
// Usage
function Settings() {
return (
GeneralSecurityNotifications
)
}
```
## Testing Compositional Components
### Pattern: Test Atomic Components
```tsx
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(
)
expect(screen.getByDisplayValue("Hello world")).toBeInTheDocument()
})
})
```
### Pattern: Test Composed Features
```tsx
describe("MessageComposer", () => {
it("composes correctly", () => {
render()
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(
)
expect(screen.getByRole("button")).toBeDisabled()
})
})
```
## Quality Checklist
When implementing React components with Effect Atom, ensure:
- [ ] No boolean props - use composition instead
- [ ] Components organized in namespaces with namespace imports
- [ ] State lifted to appropriate level (above all components that need it)
- [ ] Atomic components compose into features
- [ ] Effect Atom used for shared/complex state
- [ ] Avoid unnecessary `useEffect` - prefer direct calculation, `useMemo`, or `useTransition`
- [ ] Result types used for async operations with explicit error handling
- [ ] Context only for component-specific state, Atoms for app-wide state
- [ ] All exports documented with JSDoc
- [ ] Components testable in isolation
- [ ] Render props or compound components for maximum flexibility
## Common Mistakes
### Mistake: Mixing Context and Atoms
```tsx
// L WRONG - Using Context for app-wide state
const AppContext = React.createContext(null)
function App() {
const [state, setState] = useState(appState)
return (
{/* Deep component tree */}
)
}
// CORRECT - Use Atoms for app-wide state
// state/App.ts
export const appState = Atom.make(initialState)
// Components access directly
function DeepComponent() {
const state = useAtomValue(appState)
return
{state.value}
}
```
### Mistake: Not Lifting State High Enough
```tsx
// L WRONG - State trapped in Modal
function Modal() {
return (
{/* State is here */}
)
}
// CORRECT - Lift state above Modal
function ModalContainer() {
const editorState = useAtomValue(Editor.state)
return (
)
}
```
### Mistake: Overusing useEffect
```tsx
// L WRONG - Effect for derived data
function OrderSummary({ order }: { order: Order }) {
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(order.items.reduce((sum, item) => sum + item.price, 0))
}, [order])
return
Total: ${total}
}
// CORRECT - Calculate during render
function OrderSummary({ order }: { order: Order }) {
const total = order.items.reduce((sum, item) => sum + item.price, 0)
return
Total: ${total}
}
```
## 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.