Automate it
create-shadow-app codemod react-to-shadow applies the mappings of this page that need no judgment.
create-shadow-app codemod react-to-shadow applies the mappings of this page that need no judgment. It parses each file,
edits by source range (comments and formatting outside the edited ranges stay as written), and prints what it left for you.
Run it without a flag to see a diff and write nothing, then again with --write. A second run changes nothing. See
Migrate code with codemods for the options and the exit codes.
npx create-shadow-app codemod react-to-shadow src
npx create-shadow-app codemod react-to-shadow src --write
| React | The codemod writes | Why |
|---|---|---|
useState(0), useState(""), useState(null) |
useStore(0); every read of the value gets () |
useState becomes useStore: a single value is an accessor |
useState({ ... }), useState([...]) |
useStore({ ... }); reads stay as written, with a note |
A plain object or array is a store; its setter merges and takes a draft |
useState(props.start), any value of unknown shape |
useSignal(props.start); reads get (), with a note |
useSignal replaces the whole value like React does |
useState(() => expensive()) |
useSignal(expensive()) |
There is no lazy initializer: the body runs once |
useReducer(reducer, initial) |
the same call; reads of the state get () |
The state is an accessor |
useMemo(fn, [deps]) |
useMemo(fn); reads of the value get () |
Dependencies are tracked; the note says the array was removed |
useEffect(fn, []) |
onMount(fn) |
The table of dependency array forms |
useEffect(fn, [a, b]) |
useEffect(fn), with a note that the array was removed |
The effect re-runs for what it reads |
useCallback(fn, [deps]) |
fn |
Handlers are created once |
createContext, useContext, useId, startTransition |
the same names from @shadow-js/core |
Hooks and APIs at a glance |
useEffectEvent |
useEvent from @shadow-js/core |
RENAMED in the API table |
Fragment, Suspense, lazy |
the same names from @shadow-js/web |
className and onChange are not rewritten: the compiler accepts the React names, and onChange on a text input fires on every
edit as in React (class, className, for and htmlFor, Events and forms).
The codemod warns, with the file:line, for what it will not guess: useRef, useLayoutEffect, memo, forwardRef, React.useState
written through the default import, a state variable that is reassigned, useState() without an initial value, a lazy initializer
with a block body, useReducer with an initializer function, and a dependency list that is not an array literal. The file keeps the
React import for those names, and the rest of the file is still migrated. This is the exact output for the input shown:
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
export function Todos({ initial }: { initial: string[] }) {
const [items, setItems] = useState(initial);
const [text, setText] = useState("");
const [filter, setFilter] = useState({ done: false });
const remaining = useMemo(() => items.length, [items]);
const input = useRef<HTMLInputElement>(null);
useEffect(() => {
input.current?.focus();
}, []);
useEffect(() => {
document.title = `${remaining} left`;
}, [remaining]);
const add = useCallback(() => {
setItems([...items, text]);
setText("");
}, [items, text]);
return (
<form onSubmit={add}>
<input ref={input} className="field" value={text} onChange={(event) => setText(event.target.value)} />
<button type="button" onClick={() => setFilter({ done: !filter.done })}>
{filter.done ? "Done" : "Open"}: {remaining}
</button>
</form>
);
}
import { onMount, useEffect, useMemo, useSignal, useStore } from "@shadow-js/core";
import { useRef } from "react";
export function Todos({ initial }: { initial: string[] }) {
const [items, setItems] = useSignal(initial);
const [text, setText] = useStore("");
const [filter, setFilter] = useStore({ done: false });
const remaining = useMemo(() => items().length);
const input = useRef<HTMLInputElement>(null);
onMount(() => {
input.current?.focus();
});
useEffect(() => {
document.title = `${remaining()} left`;
});
const add = () => {
setItems([...items(), text()]);
setText("");
};
return (
<form onSubmit={add}>
<input ref={input} className="field" value={text()} onChange={(event) => setText(event.target.value)} />
<button type="button" onClick={() => setFilter({ done: !filter.done })}>
{filter.done ? "Done" : "Open"}: {remaining()}
</button>
</form>
);
}