with one click
evo-migrate-react
// Migrate a component from @ebay/ebayui-core-react to @evo-web/react. Receives the ebayui-core-react component name as the argument (e.g. /evo-migrate-react ebay-button).
// Migrate a component from @ebay/ebayui-core-react to @evo-web/react. Receives the ebayui-core-react component name as the argument (e.g. /evo-migrate-react ebay-button).
| name | evo-migrate-react |
| description | Migrate a component from @ebay/ebayui-core-react to @evo-web/react. Receives the ebayui-core-react component name as the argument (e.g. /evo-migrate-react ebay-button). |
You are migrating $ARGUMENTS from packages/ebayui-core-react/src/$ARGUMENTS/ to a new packages/evo-react/src/${ARGUMENTS#ebay-}/ directory.
packages/ebayui-core-react/src/$ARGUMENTS/.style.ts for the same component in either:
packages/evo-marko/src/tags/evo-${ARGUMENTS#ebay-}/style.ts, orpackages/ebayui-core/src/components/$ARGUMENTS/style.ts
This gives you the correct skin module name to import (e.g. @ebay/skin/button → used as @ebay/skin/button.mjs).packages/evo-marko/src/tags/evo-${ARGUMENTS#ebay-}/index.marko and its marko-tag.json or tag definition) to extract the full list of supported props and their types.packages/evo-react/src/button/ as the canonical reference for all conventions.| ebayui-core-react | evo-react |
|---|---|
ebay-button (dir) | button (dir) |
EbayButton (component) | EvoButton (component) |
EbayButtonProps (type) | EvoButtonProps (type) |
Story title mirrors the ebayui-core-react story title with ebay replaced by evo:
"buttons/ebay-button" → "buttons/evo-button""graphics & icons/ebay-avatar" → "graphics & icons/evo-avatar"packages/evo-react/src/{name}/
index.ts ← named re-exports only (no default exports)
{name}.tsx ← main component
{subcomponent-name}.tsx ← sub-components if present (named after actual sub-component, e.g. button-cell.tsx)
types.ts ← all exported types
context.ts ← React context + hook (only if component uses context)
README.md ← component name + Documentation section with Storybook link only
{name}.stories.tsx ← Storybook stories (co-located, NOT in __tests__/)
test/
test.browser.tsx ← browser interaction tests (vitest-browser-react)
test.server.tsx ← SSR snapshot tests (renderToString)
Key difference from ebayui-core-react: tests live in test/ (not __tests__/), stories co-located with source (not inside __tests__/).
README.md format — keep it minimal, just a Storybook link (props and usage docs live in the story):
# EvoButton
## Documentation
[Storybook](https://opensource.ebay.com/evo-web/react/main/?path=/docs/buttons-evo-button--documentation)
import React from "react" unless required for typing@evo-web/react uses the automatic JSX transform — the JSX runtime is injected automatically and React does not need to be imported for JSX. Import only what you actually use as named imports:
// ✅ evo-react — import only what is needed
import type { ComponentProps, SyntheticEvent } from "react";
import classNames from "classnames";
// ❌ do NOT import the default React object unless unavoidable
import React from "react";
The only time import React from "react" is acceptable is when you need the namespace for a specific type like React.JSX.Element in overloaded signatures, and even then prefer import type { JSX } from "react" with JSX.Element.
FC, no arrow function components// ✅ evo-react
export function EvoButton(props: NativeButtonProps) { ... }
// ❌ do NOT copy from ebayui-core-react
const EbayButton: FC<Props> = (props) => { ... }
For overloaded signatures, annotate overloads with React.JSX.Element but let the implementation infer:
export function EvoButton(props: AnchorButtonProps): React.JSX.Element;
export function EvoButton(props: NativeButtonProps): React.JSX.Element;
export function EvoButton(props: AnchorButtonProps | NativeButtonProps) { ... }
For non-overloaded components, omit the return type entirely and let TypeScript infer it.
forwardRef — React 19 native ref// ✅ evo-react: ref works natively as a normal prop
export function EvoButton({ ref, ...props }: NativeButtonProps) { ... }
// ❌ do NOT use
React.forwardRef(...)
withForwardRef(...)
forwardedRef prop
// ✅
export function EvoButton(...) { ... }
// ❌
export default EvoButton;
.mjs extensionimport "@ebay/skin/button.mjs"; // ✅
import "@ebay/skin/button"; // ❌
import "@ebay/skin/button.css"; // ❌
Derive the module name from the style.ts file you read in Step 0 and append .mjs.
EvoIcon* components, not <EbayIcon name="..." />// ✅ evo-react
import { EvoIconChevronDown16 } from "../icon/icons/chevron-down-16";
<EvoIconChevronDown16 />
// ❌ ebayui-core-react pattern — do not copy
<EbayIcon name="chevron-down-16" />
This applies to stories and tests as well — never use inline <svg> or custom placeholder icons. Always use an existing EvoIcon* component. Available icons are in packages/evo-react/src/evo-icon/icons/.
() => {}// ✅
onEscape?: (e: KeyboardEvent<HTMLButtonElement>) => void;
onEscape?.(event);
// ❌
onEscape = () => {}
onEscape(event);
Before finalising the prop surface, compare the ebayui-core-react props against the evo-marko component you read in Step 0:
noSkinClasses on EbayIcon existed to opt out of skin class generation, but evo-react icon components always apply skin classes — the prop is unnecessary and should be dropped.Do not silently carry over every prop from ebayui-core-react. Each prop must have a clear purpose in the evo-react context.
a11yText over aria-labelevo-react standardises accessible label props as a11yText instead of raw aria-label for consistency across components. Before deciding the prop name:
marko-tag.json / tag definition you read in Step 0).a11yText, use a11yText in evo-react and map it to aria-label on the underlying element internally:// ✅ evo-react
export function EvoButton({ a11yText, ...rest }: EvoButtonProps) {
return <button aria-label={a11yText} {...rest} />;
}
// ❌ do not expose aria-label as a custom prop name when a11yText is the standard
use() instead of useContext()Always consume React context with the use() hook from React 19, never useContext:
// ✅ evo-react
import { use } from "react";
const value = use(FooContext);
// ❌ do NOT use
import { useContext } from "react";
const value = useContext(FooContext);
Never pass an inline object literal to a context value prop. Instead, define a <XxxProvider> component in context.tsx that accepts each value as a named prop and memos the object internally:
// ✅ evo-react — context.tsx
export function FooProvider({ size, onToggle, children }: FooProviderProps) {
const value = useMemo(() => ({ size, onToggle }), [size, onToggle]);
return <FooContext value={value}>{children}</FooContext>;
}
// ✅ evo-react — foo.tsx
<FooProvider size={size} onToggle={onToggle}>
// ❌ do NOT inline the object
<FooContext value={{ size, onToggle }}>
This keeps memoization out of the component file and prevents unnecessary re-renders of all context consumers.
React.Children APIsDo not use Children.map, Children.toArray, findComponent, filterComponent, or any child-scanning pattern.
The preferred approach is named sub-components with React context (see ADR 0005). This is the established pattern in @evo-web/react and should be the default proposal for consistency.
If the ebayui-core-react component uses children composition (e.g. finding a sub-component in children), stop and ask before proceeding. Lead with the sub-component approach as the recommendation, but present the full picture so the user can confirm:
footer, header, title) if the region is a single, unstructured slot with no BEM class injection needed.Do not guess — get alignment before migrating this pattern.
Keep all custom types in types.ts. Export them from index.ts. Do not inline complex types inside the component file.
// types.ts
export type Priority = "primary" | "secondary" | "tertiary" | "none";
export type EvoButtonProps = AnchorButtonProps | NativeButtonProps;
// index.ts
export { EvoButton } from "./button";
export type { EvoButtonProps, Priority } from "./types";
test/test.browser.tsxUses vitest-browser-react + userEvent. Tests real DOM interactions.
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { render } from "vitest-browser-react";
import { userEvent } from "vitest/browser";
import { EvoButton } from "../button";
describe("evo-button", () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
});
afterEach(() => {
user.cleanup();
});
it("emits click event when clicked", async () => {
const onClick = vi.fn();
const screen = await render(
<EvoButton onClick={onClick}>Click Me</EvoButton>,
);
await user.click(screen.getByRole("button"));
expect(onClick).toHaveBeenCalledTimes(1);
});
});
Key differences from ebayui-core-react tests:
vitest-browser-react, not @testing-library/reactawait render(...) (async)userEvent from vitest/browserawait expect.element(el).toBeInTheDocument() not expect(el).toBeInTheDocument()test/test.server.tsxUses renderToString for SSR snapshots.
import { it, expect, describe } from "vitest";
import { renderToString } from "react-dom/server";
import { EvoButton } from "../button";
import type { Priority } from "../types";
describe("EvoButton SSR", () => {
it.each<Priority>(["primary", "secondary", "tertiary", "none"])(
"should render button with priority=%s",
(priority) => {
expect(
renderToString(<EvoButton priority={priority}>Button</EvoButton>),
).toMatchSnapshot();
},
);
});
{name}.stories.tsxargs and argTypes controls — not separate stories.title must mirror the ebayui-core-react story title with ebay replaced by evo.## Usage section with the import snippet.subcomponents: If the component has sub-components (e.g. EvoAvatarImage alongside EvoAvatar), declare them in the meta using the subcomponents field. This causes Storybook's autodocs to render a props table for each sub-component as a separate tab.import type { Meta, StoryObj } from "@storybook/react-vite";
import { EvoButton } from "./button";
const meta: Meta<typeof EvoButton> = {
title: "buttons/evo-button",
component: EvoButton,
// Include any exported sub-components so autodocs generates their prop tables:
// subcomponents: { EvoButtonCell },
tags: ["autodocs"],
parameters: {
docs: {
description: {
component: `
A flexible button component that renders as \`<button>\` or \`<a>\` based on the \`href\` prop.
## Usage
\`\`\`tsx
import { EvoButton } from "@evo-web/react/button";
\`\`\`
`,
},
},
},
argTypes: {
// one entry per custom prop with control type + description
},
args: {
// sensible defaults
},
};
export default meta;
type Story = StoryObj<typeof EvoButton>;
export const Default: Story = {
args: { children: "Button" },
};
After completing the component, update .claude/skills/evo-app-migrate-react/:
.claude/skills/evo-app-migrate-react/components/evo-{name}.md where {name} is the evo-react component directory name (for example, evo-accordion.md).ebayui-core-react:
No prop changes. Global renames from Step 2 are sufficient..claude/skills/evo-app-migrate-react/SKILL.md only as an index:
ebay-{name} under Step 3 — Apply per-component prop changes.components/evo-{name}.md.SKILL.md.Keep component entries concise. App owners read these files, not component authors.
export default)React.forwardRef or withForwardRefFC<Props> type annotation on components.mjs extensionEvoIcon* components used (no <EbayIcon name="..." />)?. (no = () => {} defaults)aria-label prop replaced with a11yText if evo-marko uses it (mapped internally to aria-label); asked if naming is unclearReact.Children, findComponent, or child-scanning — asked if encounteredtest/test.browser.tsx uses vitest-browser-reacttest/test.server.tsx uses renderToString + snapshotsREADME.md created with component name and Storybook documentation link only{name}.stories.tsx co-located with source"category/evo-{name}" patterncomponents/evo-{name}.md file and no inline component details in SKILL.mdnpm run build -w packages/evo-react passes.changeset/ with patch bump for @evo-web/react (@evo-web/react is still experimental, so all additions use patch)[HINT] Download the complete skill directory including SKILL.md and all related files