[]previous and next page

Automate it

create-shadow-app codemod react-to-shadow applies the mappings of this page that need no judgment.

For agentsUpdated

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.

sh
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:

todos.before.tsxtsx
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>
  );
}
todos.after.tsxtsx
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>
  );
}