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:

ReactCrankNotes
classNameclassUse HTML attribute names
htmlForforUse HTML attribute names
onClickonclickLowercase event props
onChangeonchangeLowercase event props
useStateGenerator + local variablesState persists in closure
useEffectLifecycle methodsschedule(), after(), cleanup()
useContextthis.consume()Different method names
Context.Providerthis.provide()Different method names
defaultValuecopy="!value"Uncontrolled inputs pattern
dangerouslySetInnerHTMLinnerHTMLDirect prop

Converting Component Types

Function Components

React function components convert directly:

// React
function 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:

// React
class 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>
);
}
}

// Crank
function *Counter() {
let count = 0;

// componentDidMount equivalent
console.log('Mounted');

const increment = () => this.refresh(() => count++);

for ({} of this) {
yield (
<button onclick={increment}>
Count: {count}
</button>
);
}

// componentWillUnmount equivalent
console.log('Unmounting');
}

Components with Complex State

// React
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

// Crank
function *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:

Instead, you get FREEDOM:

Converting Hooks to Vanilla JavaScript

useState → Local Variables

// React
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);

// Crank
function *Component() {
let count = 0;
const increment = () => this.refresh(() => count++);

for ({} of this) {
// component body
}
}

useEffect

// React - componentDidMount
useEffect(() => {
console.log('Mounted');
}, []);

// Crank
function *Component() {
console.log('Mounted'); // Runs on mount

for ({} of this) {
yield <div>Content</div>;
}
}

// React - componentDidUpdate
useEffect(() => {
console.log('Updated');
});

// Crank
function *Component() {
for ({} of this) {
this.after(() => console.log('Updated')); // Runs after each render
yield <div>Content</div>;
}
}

// React - componentWillUnmount
useEffect(() => {
const timer = setInterval(() => {}, 1000);
return () => clearInterval(timer);
}, []);

// Crank
function *Component() {
const timer = setInterval(() => {}, 1000);

for ({} of this) {
yield <div>Content</div>;
}

clearInterval(timer); // Cleanup on unmount
}

useContext

// React
const ThemeContext = React.createContext();

function App() {
return (
<ThemeContext.Provider value="dark">
<Child />
</ThemeContext.Provider>
);
}

function Child() {
const theme = useContext(ThemeContext);
return <div>Theme: {theme}</div>;
}

// Crank
function *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 complexity
const expensiveValue = useMemo(() => computeExpensive(data), [data]);
const handleClick = useCallback(() => doSomething(id), [id]);
const memoizedStyle = useMemo(() => ({color: theme}), [theme]);

// Crank - pure vanilla JavaScript simplicity
function *Component({data, id, theme}) {
// Expensive computation? Just cache it naturally!
let cachedData = null;
let expensiveValue = null;

// Create functions once, use forever
const handleClick = () => doSomething(id);
const style = {color: theme};

for ({data, id, theme} of this) {
// Natural memoization - no hooks needed
if (cachedData !== data) {
expensiveValue = computeExpensive(data);
cachedData = data;
}

yield <div onclick={handleClick} style={style}>{expensiveValue}</div>;
}
}

Why this works:

Custom Hooks → Regular Functions and Classes

// React - forced into hook patterns
function 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 functions
function 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 Crank
ctx.after(() => {
const unmount = virtualizer._didMount();
ctx.cleanup(() => unmount && unmount());
});

// Sync virtualizer updates with Crank's render cycle
const afterUpdate = () => {
virtualizer._willUpdate();
ctx.after(afterUpdate);
};
ctx.after(afterUpdate);

return virtualizer;
}

// Usage in a Crank component
function *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 => (
<div
key={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:


**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
<button
onClick={handleClick}
onMouseOver={handleHover}
onFocus={handleFocus}
onChange={handleChange}
>

// Crank
<button
onclick={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 input
const [value, setValue] = useState('');
<input value={value} onChange={e => setValue(e.target.value)} />

// Crank - controlled input
function *Component() {
let value = '';

for ({} of this) {
yield (
<input
value={value}
onchange={e => this.refresh(() => value = e.target.value)}
/>
);
}
}

Form Patterns

// React
function 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>
);
}

// Crank
function *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

// React
function 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>;
}

// Crank
async function UserProfile({userId}) {
const user = await fetchUser(userId);
return <div>{user.name}</div>;
}

// Usage with Suspense
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userId={123} />
</Suspense>
);
}

Code Splitting

// React
const LazyComponent = React.lazy(() => import('./LazyComponent'));

function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}

// Crank
import {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

// React
class 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;
}
}

// Crank
function *ErrorBoundary({children}) {
for ({children} of this) {
try {
yield children;
} catch (error) {
console.log(error);
yield <h1>Something went wrong.</h1>;
}
}
}

Higher-Order Components

// React
function withLoading(WrappedComponent) {
return function WithLoadingComponent(props) {
if (props.loading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}

// Crank
function withLoading(WrappedComponent) {
return function *WithLoadingComponent(props) {
for (props of this) {
if (props.loading) {
yield <div>Loading...</div>;
} else {
yield <WrappedComponent {...props} />;
}
}
};
}

Render Props

// React
function 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>
);
}

// Crank
function *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

// React
const inputRef = useRef(null);
<input ref={inputRef} />

// Crank
function *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

// React
const ExpensiveComponent = React.memo(({data}) => {
return <div>{expensiveOperation(data)}</div>;
});

// Crank
function *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-renders
const 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

2. Props and Events

3. Form Handling

4. Context API

5. Async Patterns

6. Performance

Common Gotchas

1. Event Handler Binding

// React - need to bind or use arrow functions
class MyComponent extends React.Component {
handleClick = () => { /* this is bound */ }
}

// Crank - handlers naturally have access to generator scope
function *MyComponent() {
const handleClick = () => { /* naturally has access to component scope */ };
}

2. State Updates

// React - setState is async
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1}); // Still count + 1!

// Crank - updates are synchronous within refresh callback
this.refresh(() => {
count = count + 1;
count = count + 1; // Actually count + 2
});

3. Effect Dependencies

// React - need dependency arrays
useEffect(() => {
doSomething(prop);
}, [prop]); // Easy to forget dependencies

// Crank - no dependency arrays needed
function *Component({prop}) {
for ({prop} of this) {
this.after(() => doSomething(prop)); // Always gets latest prop
yield <div />;
}
}

This guide covers all the major patterns needed to convert React codebases to Crank systematically. Keep it handy as your complete migration reference!