[]previous and next page

A worked example: before and after

A realistic React component: controlled form, a context, a memoized count, an effect with cleanup, a memoized row and a list.

For agentsUpdated

A realistic React component: controlled form, a context, a memoized count, an effect with cleanup, a memoized row and a list.

tsx
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";

export const ThemeContext = createContext("light");

type Todo = { id: number; text: string; done: boolean };

const TodoRow = memo(function TodoRow({ todo, onToggle }: { todo: Todo; onToggle: (id: number) => void }) {
  return (
    <li className={todo.done ? "done" : ""}>
      <label>
        <input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
        {todo.text}
      </label>
    </li>
  );
});

export function Todos() {
  const theme = useContext(ThemeContext);
  const [todos, setTodos] = useState<Todo[]>([]);
  const [text, setText] = useState("");
  const remaining = useMemo(() => todos.filter((todo) => !todo.done).length, [todos]);

  useEffect(() => {
    const previous = document.title;
    document.title = `${remaining} left`;
    return () => {
      document.title = previous;
    };
  }, [remaining]);

  const toggle = useCallback((id: number) => {
    setTodos((list) => list.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo)));
  }, []);

  return (
    <section className={`todos ${theme}`}>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          if (text.trim() === "") return;
          setTodos((list) => [...list, { id: list.length + 1, text, done: false }]);
          setText("");
        }}
      >
        <input aria-label="New todo" value={text} onChange={(event) => setText(event.target.value)} />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <TodoRow key={todo.id} todo={todo} onToggle={toggle} />
        ))}
      </ul>
      <p>{remaining} left</p>
    </section>
  );
}

The ShadowJS version has no memo, no useCallback, no useMemo, no dependency array and no key. The theme context is a file of its own, as it would be in an app:

src/todo-theme.tsxtsx
import { createContext } from "@shadow-js/core";

export const Theme = createContext("light");
src/todos.tsxtsx
import { useContext, useEffect, useStore } from "@shadow-js/core";
import { For } from "@shadow-js/web";
import { Theme } from "./todo-theme";

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

export function Todos() {
  console.log("setup");
  const theme = useContext(Theme);
  const [todos, setTodos] = useStore<Todo[]>([]);
  const [text, setText] = useStore("");
  const remaining = todos.filter((todo) => !todo.done).length;

  useEffect(() => {
    const previous = document.title;
    document.title = `${remaining} left`;
    return () => {
      document.title = previous;
    };
  });

  const toggle = (id: number): void => {
    setTodos((todo) => todo.id === id, "done", (done) => !done);
  };

  return (
    <section class={`todos ${theme}`}>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          if (text().trim() === "") return;
          setTodos((list) => {
            list.push({ id: list.length + 1, text: text(), done: false });
          });
          setText("");
        }}
      >
        <input aria-label="New todo" value={text()} onInput={(event) => setText(event.currentTarget.value)} />
        <button type="submit">Add</button>
      </form>
      <ul>
        <For each={todos} key={(todo) => todo.id}>
          {(todo) => (
            <li class={todo().done ? "done" : ""}>
              <label>
                <input type="checkbox" checked={todo().done} onChange={() => toggle(todo().id)} />
                {todo().text}
              </label>
            </li>
          )}
        </For>
      </ul>
      <p>{remaining} left</p>
    </section>
  );
}