React to Crank Migration Guide
This comprehensive guide covers everything you need to convert React codebases to Crank. It's organized by migration patterns rather than API comparisons, making it practical for systematic conversion.
Quick Reference: Key Differences
Before diving into specifics, here are the major differences to keep in mind:
| React | Crank | Notes |
|---|---|---|
className | class | Use HTML attribute names |
htmlFor | for | Use HTML attribute names |
onClick | onclick | Lowercase event props |
onChange | onchange | Lowercase event props |
useState | Generator + local variables | State persists in closure |
useEffect | Lifecycle methods | schedule(), after(), cleanup() |
useContext | this.consume() | Different method names |
Context.Provider | this.provide() | Different method names |
defaultValue | copy="!value" | Uncontrolled inputs pattern |
dangerouslySetInnerHTML | innerHTML | Direct prop |
Converting Component Types
Function Components
React function components convert directly:
// Reactfunction Greeting({name}) {return <div>Hello {name}</div>;}// Crank - identical!function Greeting({name}) {return <div>Hello {name}</div>;}
Class Components
Convert React class components to generator functions:
// Reactclass Counter extends React.Component {constructor(props) {super(props);this.state = {count: 0};}componentDidMount() {console.log('Mounted');}componentWillUnmount() {console.log('Unmounting');}increment = () => {this.setState({count: this.state.count + 1});}render() {return (<button onClick={this.increment}>Count: {this.state.count}</button>);}}// Crankfunction *Counter() {let count = 0;// componentDidMount equivalentconsole.log('Mounted');const increment = () => this.refresh(() => count++);for ({} of this) {yield (<button onclick={increment}>Count: {count}</button>);}// componentWillUnmount equivalentconsole.log('Unmounting');}
Components with Complex State
// Reactconst [user, setUser] = useState(null);const [loading, setLoading] = useState(false);const [error, setError] = useState(null);// Crankfunction *UserComponent() {let user = null;let loading = false;let error = null;const updateState = (updates) => {this.refresh(() => {Object.assign({user, loading, error}, updates);user = updates.user ?? user;loading = updates.loading ?? loading;error = updates.error ?? error;});};// Use updateState({loading: true}) instead of setLoading(true)}
NO HOOKS! 🎉
The best part about migrating from React to Crank? You get to DELETE all your hooks!
Crank doesn't have hooks because you don't need them. Generator functions give you persistent state, natural lifecycles, and direct control over your component logic. No more:
- ❌ Dependency arrays you forget to update
- ❌ Stale closure problems
- ❌ Rules of hooks restrictions
- ❌
useCallbackanduseMemoeverywhere - ❌ Complex custom hooks for simple logic
- ❌
useEffectcleanup functions - ❌ Hook order dependencies
Instead, you get FREEDOM:
- ✅ Write vanilla JavaScript - loops, variables, functions, promises
- ✅ Natural lifecycles - code runs when you expect it to
- ✅ No artificial restrictions - put logic wherever it makes sense
- ✅ No memoization needed - values persist naturally in closure scope
- ✅ Simple async patterns - just use
async/await
Converting Hooks to Vanilla JavaScript
useState → Local Variables
// Reactconst [count, setCount] = useState(0);const increment = () => setCount(count + 1);// Crankfunction *Component() {let count = 0;const increment = () => this.refresh(() => count++);for ({} of this) {// component body}}
useEffect
// React - componentDidMountuseEffect(() => {console.log('Mounted');}, []);// Crankfunction *Component() {console.log('Mounted'); // Runs on mountfor ({} of this) {yield <div>Content</div>;}}// React - componentDidUpdateuseEffect(() => {console.log('Updated');});// Crankfunction *Component() {for ({} of this) {this.after(() => console.log('Updated')); // Runs after each renderyield <div>Content</div>;}}// React - componentWillUnmountuseEffect(() => {const timer = setInterval(() => {}, 1000);return () => clearInterval(timer);}, []);// Crankfunction *Component() {const timer = setInterval(() => {}, 1000);for ({} of this) {yield <div>Content</div>;}clearInterval(timer); // Cleanup on unmount}
useContext
// Reactconst ThemeContext = React.createContext();function App() {return (<ThemeContext.Provider value="dark"><Child /></ThemeContext.Provider>);}function Child() {const theme = useContext(ThemeContext);return <div>Theme: {theme}</div>;}// Crankfunction *App() {this.provide("theme", "dark");for ({} of this) {yield <Child />;}}function Child() {const theme = this.consume("theme");return <div>Theme: {theme}</div>;}
useMemo and useCallback → Just Delete Them!
// React - artificial memoization complexityconst expensiveValue = useMemo(() => computeExpensive(data), [data]);const handleClick = useCallback(() => doSomething(id), [id]);const memoizedStyle = useMemo(() => ({color: theme}), [theme]);// Crank - pure vanilla JavaScript simplicityfunction *Component({data, id, theme}) {// Expensive computation? Just cache it naturally!let cachedData = null;let expensiveValue = null;// Create functions once, use foreverconst handleClick = () => doSomething(id);const style = {color: theme};for ({data, id, theme} of this) {// Natural memoization - no hooks neededif (cachedData !== data) {expensiveValue = computeExpensive(data);cachedData = data;}yield <div onclick={handleClick} style={style}>{expensiveValue}</div>;}}
Why this works:
- Functions are created once and persist in the generator closure
- Variables naturally cache values between renders
- No dependency arrays - you control exactly when things update
- No stale closures - the
for...ofloop gives you fresh props - Just vanilla JavaScript - no framework magic needed!
Custom Hooks → Regular Functions and Classes
// React - forced into hook patternsfunction useCounter(initialValue = 0) {const [count, setCount] = useState(initialValue);const increment = useCallback(() => setCount(c => c + 1), []);const decrement = useCallback(() => setCount(c => c - 1), []);return {count, increment, decrement};}function useLocalStorage(key, defaultValue) {const [value, setValue] = useState(() => {return localStorage.getItem(key) ?? defaultValue;});const setStoredValue = useCallback((newValue) => {setValue(newValue);localStorage.setItem(key, newValue);}, [key]);return [value, setStoredValue];}// Crank - just write normal JavaScript!class Counter {constructor(initialValue = 0) {this.count = initialValue;}increment() {this.count++;}decrement() {this.count--;}}class LocalStorage {static get(key, defaultValue) {return localStorage.getItem(key) ?? defaultValue;}static set(key, value) {localStorage.setItem(key, value);}}// Or simple functionsfunction createCounter(initialValue = 0) {return {count: initialValue,increment() { this.count++; },decrement() { this.count--; }};}// Use in components - no hook restrictions!function *CounterComponent() {const counter = createCounter(0);let stored = LocalStorage.get('count', 0);const save = () => {LocalStorage.set('count', counter.count);this.refresh();};for ({} of this) {yield (<div><p>Count: {counter.count}</p><button onclick={() => { counter.increment(); this.refresh(); }}>+</button><button onclick={() => { counter.decrement(); this.refresh(); }}>-</button><button onclick={save}>Save</button></div>);}}
Real-World Example: Third-Party Library Integration
Here's how to integrate third-party libraries without hooks. This virtualizer utility wraps TanStack Virtual for use in Crank components:
// virtualizer.ts - Utility function, not a hook!import { Virtualizer, VirtualizerOptions, observeElementOffset } from "@tanstack/virtual-core";import type { Context } from "@b9g/crank";export function useVirtualizer<TItemElement extends Element>(ctx: Context,options: VirtualizerOptions<Element, TItemElement>): Virtualizer<Element, TItemElement> {const virtualizer = new Virtualizer({observeElementOffset,observeElementRect,scrollToFn: elementScroll,measureElement: (el, instance) => {return el.getBoundingClientRect()[instance.options.horizontal ? "width" : "height"];},...options,});// Setup lifecycle integration with Crankctx.after(() => {const unmount = virtualizer._didMount();ctx.cleanup(() => unmount && unmount());});// Sync virtualizer updates with Crank's render cycleconst afterUpdate = () => {virtualizer._willUpdate();ctx.after(afterUpdate);};ctx.after(afterUpdate);return virtualizer;}// Usage in a Crank componentfunction *VirtualList({ items }) {const virtualizer = useVirtualizer(this, {count: items.length,getScrollElement: () => document.getElementById('scroll-container'),estimateSize: () => 35,});for ({ items } of this) {yield (<div id="scroll-container" style={{ height: '400px', overflow: 'auto' }}><div style={{height: `${virtualizer.getTotalSize()}px`,position: 'relative'}}>{virtualizer.getVirtualItems().map(virtualItem => (<divkey={virtualItem.key}style={{position: 'absolute',top: 0,left: 0,width: '100%',height: `${virtualItem.size}px`,transform: `translateY(${virtualItem.start}px)`,}}>Item {virtualItem.index}: {items[virtualItem.index]}</div>))}</div></div>);}}
Key Points:
- The "hook" is just a regular function that takes the Crank context as first parameter
- No special React lifecycle management - just vanilla JavaScript working with Crank's lifecycle methods
ctx.after()for DOM-ready operations,ctx.cleanup()for cleanup- Direct instantiation of the Virtualizer class - no wrapper complexity needed
- The function name
useVirtualizeris just convention - it could becreateVirtualizerorsetupVirtualizer
**For more complex reusable logic patterns, see the [Reusable Logic guide](/guides/reusable-logic)** - no hooks needed!## Converting Props and Events### HTML Attribute Names**Always use HTML attribute names, not React's camelCase versions:**```jsx// React<label className="my-label" htmlFor="my-input"><input onChange={handleChange} onClick={handleClick} /></label>// Crank<label class="my-label" for="my-input"><input onchange={handleChange} onclick={handleClick} /></label>
Event Handling
All event props are lowercase:
// React<buttononClick={handleClick}onMouseOver={handleHover}onFocus={handleFocus}onChange={handleChange}>// Crank<buttononclick={handleClick}onmouseover={handleHover}onfocus={handleFocus}onchange={handleChange}>
Style Props
// React<div style={{fontSize: '16px', backgroundColor: 'red'}}>// Crank - use kebab-case in style objects<div style={{'font-size': '16px', 'background-color': 'red'}}>
Class Names
// React - className only<div className={`btn ${isActive ? 'active' : ''}`}>// Crank - class prop with object support<div class={{btn: true, active: isActive}}>// Crank - or strings<div class={`btn ${isActive ? 'active' : ''}`}>
Form Handling
Controlled vs Uncontrolled Inputs
React's controlled/uncontrolled concept doesn't exist in Crank. Use copy prop instead:
// React - uncontrolled input<input defaultValue="initial" />// Crank - uncontrolled input (preserves user changes)<input copy="!value" value="initial" />// React - controlled inputconst [value, setValue] = useState('');<input value={value} onChange={e => setValue(e.target.value)} />// Crank - controlled inputfunction *Component() {let value = '';for ({} of this) {yield (<inputvalue={value}onchange={e => this.refresh(() => value = e.target.value)}/>);}}
Form Patterns
// Reactfunction ContactForm() {const [formData, setFormData] = useState({name: '', email: ''});const handleChange = (e) => {setFormData({...formData, [e.target.name]: e.target.value});};return (<form><input name="name" value={formData.name} onChange={handleChange} /><input name="email" value={formData.email} onChange={handleChange} /></form>);}// Crankfunction *ContactForm() {let formData = {name: '', email: ''};const handleChange = (e) => {this.refresh(() => {formData = {...formData, [e.target.name]: e.target.value};});};for ({} of this) {yield (<form><input name="name" value={formData.name} onchange={handleChange} /><input name="email" value={formData.email} onchange={handleChange} /></form>);}}
Async Patterns
Data Fetching
// Reactfunction UserProfile({userId}) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);useEffect(() => {fetchUser(userId).then(user => {setUser(user);setLoading(false);});}, [userId]);if (loading) return <div>Loading...</div>;return <div>{user.name}</div>;}// Crankasync function UserProfile({userId}) {const user = await fetchUser(userId);return <div>{user.name}</div>;}// Usage with Suspensefunction App() {return (<Suspense fallback={<div>Loading...</div>}><UserProfile userId={123} /></Suspense>);}
Code Splitting
// Reactconst LazyComponent = React.lazy(() => import('./LazyComponent'));function App() {return (<Suspense fallback={<div>Loading...</div>}><LazyComponent /></Suspense>);}// Crankimport {lazy, Suspense} from "@b9g/crank/async";const LazyComponent = lazy(() => import('./LazyComponent'));function App() {return (<Suspense fallback={<div>Loading...</div>}><LazyComponent /></Suspense>);}
Advanced Patterns
Error Boundaries
// Reactclass ErrorBoundary extends React.Component {constructor(props) {super(props);this.state = { hasError: false };}static getDerivedStateFromError(error) {return { hasError: true };}componentDidCatch(error, errorInfo) {console.log(error, errorInfo);}render() {if (this.state.hasError) {return <h1>Something went wrong.</h1>;}return this.props.children;}}// Crankfunction *ErrorBoundary({children}) {for ({children} of this) {try {yield children;} catch (error) {console.log(error);yield <h1>Something went wrong.</h1>;}}}
Higher-Order Components
// Reactfunction withLoading(WrappedComponent) {return function WithLoadingComponent(props) {if (props.loading) {return <div>Loading...</div>;}return <WrappedComponent {...props} />;};}// Crankfunction withLoading(WrappedComponent) {return function *WithLoadingComponent(props) {for (props of this) {if (props.loading) {yield <div>Loading...</div>;} else {yield <WrappedComponent {...props} />;}}};}
Render Props
// Reactfunction MouseTracker({render}) {const [mouse, setMouse] = useState({x: 0, y: 0});const handleMouseMove = (e) => {setMouse({x: e.clientX, y: e.clientY});};return (<div onMouseMove={handleMouseMove}>{render(mouse)}</div>);}// Crankfunction *MouseTracker({render}) {let mouse = {x: 0, y: 0};const handleMouseMove = (e) => {this.refresh(() => {mouse = {x: e.clientX, y: e.clientY};});};for ({render} of this) {yield (<div onmousemove={handleMouseMove}>{render(mouse)}</div>);}}
Special Props Conversion
Key Props
// React and Crank - identical{items.map(item => <Item key={item.id} data={item} />)}
Ref Props
// Reactconst inputRef = useRef(null);<input ref={inputRef} />// Crankfunction *Component() {let inputRef = null;for ({} of this) {yield <input ref={el => inputRef = el} />;}}
innerHTML
// React<div dangerouslySetInnerHTML={{__html: htmlString}} />// Crank<div innerHTML={htmlString} />
Performance Optimization
React.memo equivalent
// Reactconst ExpensiveComponent = React.memo(({data}) => {return <div>{expensiveOperation(data)}</div>;});// Crankfunction *ExpensiveComponent({data}) {let lastData = null;let cachedResult = null;for ({data} of this) {if (data === lastData) {yield <Copy />;} else {cachedResult = <div>{expensiveOperation(data)}</div>;lastData = data;yield cachedResult;}}}
Preventing Re-renders
// React - useMemo to prevent re-rendersconst memoizedChild = useMemo(() =><ExpensiveChild data={data} />, [data]);// Crank - copy prop to prevent re-renders<ExpensiveChild copy={!hasChanged} data={data} />
Migration Checklist
When converting a React component to Crank:
1. Component Structure
- Convert class components to generator functions
- Replace
useStatewith local variables - Replace
useEffectwith lifecycle methods - Move setup code to top of generator
- Move cleanup code to bottom of generator
2. Props and Events
- Change
classNametoclass - Change
htmlFortofor - Lowercase all event props (
onClick→onclick) - Update style objects to use CSS property names
- Replace
dangerouslySetInnerHTMLwithinnerHTML
3. Form Handling
- Replace
defaultValuewithcopy="!value" - Update controlled inputs to use
onchange(lowercase) - Wrap state updates in
this.refresh(() => ...)
4. Context API
- Replace
Context.Providerwiththis.provide() - Replace
useContextwiththis.consume()
5. Async Patterns
- Convert data fetching to async functions
- Wrap with
<Suspense>for loading states - Use
lazy()for code splitting
6. Performance
- Replace
React.memowith manual comparison or<Copy> - Use
copyprop for preventing unnecessary re-renders - Remove
useMemo/useCallback(not needed in Crank)
Common Gotchas
1. Event Handler Binding
// React - need to bind or use arrow functionsclass MyComponent extends React.Component {handleClick = () => { /* this is bound */ }}// Crank - handlers naturally have access to generator scopefunction *MyComponent() {const handleClick = () => { /* naturally has access to component scope */ };}
2. State Updates
// React - setState is asyncthis.setState({count: this.state.count + 1});this.setState({count: this.state.count + 1}); // Still count + 1!// Crank - updates are synchronous within refresh callbackthis.refresh(() => {count = count + 1;count = count + 1; // Actually count + 2});
3. Effect Dependencies
// React - need dependency arraysuseEffect(() => {doSomething(prop);}, [prop]); // Easy to forget dependencies// Crank - no dependency arrays neededfunction *Component({prop}) {for ({prop} of this) {this.after(() => doSomething(prop)); // Always gets latest propyield <div />;}}
This guide covers all the major patterns needed to convert React codebases to Crank systematically. Keep it handy as your complete migration reference!