react-modernization

Upgrade React applications to latest versions, migrate class components to hooks, and adopt concurrent features. Covers version upgrade paths (React 16→17→18) with breaking changes, class-to-hooks migration patterns, and concurrent features like Suspense, transitions, and automatic batching Includes ready-to-run codemods for automating unsafe lifecycle renames, import updates, and class-to-function conversions Demonstrates performance optimization techniques using useMemo, useCallback, code splitting, and React.memo Provides TypeScript migration guidance and a step-by-step checklist for pre-migration, component conversion, and testing phases

INSTALLATION
npx skills add https://github.com/wshobson/agents --skill react-modernization
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

React Modernization

Master React version upgrades, class to hooks migration, concurrent features adoption, and codemods for automated transformation.

When to Use This Skill

  • Upgrading React applications to latest versions
  • Migrating class components to functional components with hooks
  • Adopting concurrent React features (Suspense, transitions)
  • Applying codemods for automated refactoring
  • Modernizing state management patterns
  • Updating to TypeScript
  • Improving performance with React 18+ features

Version Upgrade Path

React 16 → 17 → 18

Breaking Changes by Version:

React 17:

  • Event delegation changes
  • No event pooling
  • Effect cleanup timing
  • JSX transform (no React import needed)

React 18:

  • Automatic batching
  • Concurrent rendering
  • Strict Mode changes (double invocation)
  • New root API
  • Suspense on server

Class to Hooks Migration

State Management

// Before: Class component

class Counter extends React.Component {

  constructor(props) {

    super(props);

    this.state = {

      count: 0,

      name: "",

    };

  }

  increment = () => {

    this.setState({ count: this.state.count + 1 });

  };

  render() {

    return (

      <div>

        <p>Count: {this.state.count}</p>

        <button onClick={this.increment}>Increment</button>

      </div>

    );

  }

}

// After: Functional component with hooks

function Counter() {

  const [count, setCount] = useState(0);

  const [name, setName] = useState("");

  const increment = () => {

    setCount(count + 1);

  };

  return (

    <div>

      <p>Count: {count}</p>

      <button onClick={increment}>Increment</button>

    </div>

  );

}

Lifecycle Methods to Hooks

// Before: Lifecycle methods

class DataFetcher extends React.Component {

  state = { data: null, loading: true };

  componentDidMount() {

    this.fetchData();

  }

  componentDidUpdate(prevProps) {

    if (prevProps.id !== this.props.id) {

      this.fetchData();

    }

  }

  componentWillUnmount() {

    this.cancelRequest();

  }

  fetchData = async () => {

    const data = await fetch(`/api/${this.props.id}`);

    this.setState({ data, loading: false });

  };

  cancelRequest = () => {

    // Cleanup

  };

  render() {

    if (this.state.loading) return <div>Loading...</div>;

    return <div>{this.state.data}</div>;

  }

}

// After: useEffect hook

function DataFetcher({ id }) {

  const [data, setData] = useState(null);

  const [loading, setLoading] = useState(true);

  useEffect(() => {

    let cancelled = false;

    const fetchData = async () => {

      try {

        const response = await fetch(`/api/${id}`);

        const result = await response.json();

        if (!cancelled) {

          setData(result);

          setLoading(false);

        }

      } catch (error) {

        if (!cancelled) {

          console.error(error);

        }

      }

    };

    fetchData();

    // Cleanup function

    return () => {

      cancelled = true;

    };

  }, [id]); // Re-run when id changes

  if (loading) return <div>Loading...</div>;

  return <div>{data}</div>;

}

Context and HOCs to Hooks

// Before: Context consumer and HOC

const ThemeContext = React.createContext();

class ThemedButton extends React.Component {

  static contextType = ThemeContext;

  render() {

    return (

      <button style={{ background: this.context.theme }}>

        {this.props.children}

      </button>

    );

  }

}

// After: useContext hook

function ThemedButton({ children }) {

  const { theme } = useContext(ThemeContext);

  return <button style={{ background: theme }}>{children}</button>;

}

// Before: HOC for data fetching

function withUser(Component) {

  return class extends React.Component {

    state = { user: null };

    componentDidMount() {

      fetchUser().then((user) => this.setState({ user }));

    }

    render() {

      return <Component {...this.props} user={this.state.user} />;

    }

  };

}

// After: Custom hook

function useUser() {

  const [user, setUser] = useState(null);

  useEffect(() => {

    fetchUser().then(setUser);

  }, []);

  return user;

}

function UserProfile() {

  const user = useUser();

  if (!user) return <div>Loading...</div>;

  return <div>{user.name}</div>;

}

React 18 Concurrent Features

New Root API

// Before: React 17

import ReactDOM from "react-dom";

ReactDOM.render(<App />, document.getElementById("root"));

// After: React 18

import { createRoot } from "react-dom/client";

const root = createRoot(document.getElementById("root"));

root.render(<App />);

Automatic Batching

// React 18: All updates are batched

function handleClick() {

  setCount((c) => c + 1);

  setFlag((f) => !f);

  // Only one re-render (batched)

}

// Even in async:

setTimeout(() => {

  setCount((c) => c + 1);

  setFlag((f) => !f);

  // Still batched in React 18!

}, 1000);

// Opt out if needed

import { flushSync } from "react-dom";

flushSync(() => {

  setCount((c) => c + 1);

});

// Re-render happens here

setFlag((f) => !f);

// Another re-render

Transitions

import { useState, useTransition } from "react";

function SearchResults() {

  const [query, setQuery] = useState("");

  const [results, setResults] = useState([]);

  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {

    // Urgent: Update input immediately

    setQuery(e.target.value);

    // Non-urgent: Update results (can be interrupted)

    startTransition(() => {

      setResults(searchResults(e.target.value));

    });

  };

  return (

    <>

      <input value={query} onChange={handleChange} />

      {isPending &#x26;&#x26; <Spinner />}

      <Results data={results} />

    </>

  );

}

Suspense for Data Fetching

import { Suspense } from "react";

// Resource-based data fetching (with React 18)

const resource = fetchProfileData();

function ProfilePage() {

  return (

    <Suspense fallback={<Loading />}>

      <ProfileDetails />

      <Suspense fallback={<Loading />}>

        <ProfileTimeline />

      </Suspense>

    </Suspense>

  );

}

function ProfileDetails() {

  // This will suspend if data not ready

  const user = resource.user.read();

  return <h1>{user.name}</h1>;

}

function ProfileTimeline() {

  const posts = resource.posts.read();

  return <Timeline posts={posts} />;

}

Codemods for Automation

Run React Codemods

# Rename unsafe lifecycle methods

npx jscodeshift -t https://raw.githubusercontent.com/reactjs/react-codemod/master/transforms/rename-unsafe-lifecycles.js src/

# Update React imports (React 17+)

npx jscodeshift -t https://raw.githubusercontent.com/reactjs/react-codemod/master/transforms/update-react-imports.js src/

# Add error boundaries

npx jscodeshift -t https://raw.githubusercontent.com/reactjs/react-codemod/master/transforms/error-boundaries.js src/

# For TypeScript files

npx jscodeshift -t https://raw.githubusercontent.com/reactjs/react-codemod/master/transforms/rename-unsafe-lifecycles.js --parser=tsx src/

# Dry run to preview changes

npx jscodeshift -t https://raw.githubusercontent.com/reactjs/react-codemod/master/transforms/rename-unsafe-lifecycles.js --dry --print src/

# Class to Hooks (third-party)

npx codemod react/hooks/convert-class-to-function src/

Custom Codemod Example

// custom-codemod.js

module.exports = function (file, api) {

  const j = api.jscodeshift;

  const root = j(file.source);

  // Find setState calls

  root

    .find(j.CallExpression, {

      callee: {

        type: "MemberExpression",

        property: { name: "setState" },

      },

    })

    .forEach((path) => {

      // Transform to useState

      // ... transformation logic

    });

  return root.toSource();

};

// Run: jscodeshift -t custom-codemod.js src/

Performance Optimization

useMemo and useCallback

function ExpensiveComponent({ items, filter }) {

  // Memoize expensive calculation

  const filteredItems = useMemo(() => {

    return items.filter((item) => item.category === filter);

  }, [items, filter]);

  // Memoize callback to prevent child re-renders

  const handleClick = useCallback((id) => {

    console.log("Clicked:", id);

  }, []); // No dependencies, never changes

  return <List items={filteredItems} onClick={handleClick} />;

}

// Child component with memo

const List = React.memo(({ items, onClick }) => {

  return items.map((item) => (

    <Item key={item.id} item={item} onClick={onClick} />

  ));

});

Code Splitting

import { lazy, Suspense } from "react";

// Lazy load components

const Dashboard = lazy(() => import("./Dashboard"));

const Settings = lazy(() => import("./Settings"));

function App() {

  return (

    <Suspense fallback={<Loading />}>

      <Routes>

        <Route path="/dashboard" element={<Dashboard />} />

        <Route path="/settings" element={<Settings />} />

      </Routes>

    </Suspense>

  );

}

TypeScript Migration

// Before: JavaScript

function Button({ onClick, children }) {

  return <button onClick={onClick}>{children}</button>;

}

// After: TypeScript

interface ButtonProps {

  onClick: () => void;

  children: React.ReactNode;

}

function Button({ onClick, children }: ButtonProps) {

  return <button onClick={onClick}>{children}</button>;

}

// Generic components

interface ListProps<T> {

  items: T[];

  renderItem: (item: T) => React.ReactNode;

}

function List<T>({ items, renderItem }: ListProps<T>) {

  return <>{items.map(renderItem)}</>;

}

Migration Checklist

### Pre-Migration

- [ ] Update dependencies incrementally (not all at once)

- [ ] Review breaking changes in release notes

- [ ] Set up testing suite

- [ ] Create feature branch

### Class → Hooks Migration

- [ ] Identify class components to migrate

- [ ] Start with leaf components (no children)

- [ ] Convert state to useState

- [ ] Convert lifecycle to useEffect

- [ ] Convert context to useContext

- [ ] Extract custom hooks

- [ ] Test thoroughly

### React 18 Upgrade

- [ ] Update to React 17 first (if needed)

- [ ] Update react and react-dom to 18

- [ ] Update @types/react if using TypeScript

- [ ] Change to createRoot API

- [ ] Test with StrictMode (double invocation)

- [ ] Address concurrent rendering issues

- [ ] Adopt Suspense/Transitions where beneficial

### Performance

- [ ] Identify performance bottlenecks

- [ ] Add React.memo where appropriate

- [ ] Use useMemo/useCallback for expensive operations

- [ ] Implement code splitting

- [ ] Optimize re-renders

### Testing

- [ ] Update test utilities (React Testing Library)

- [ ] Test with React 18 features

- [ ] Check for warnings in console

- [ ] Performance testing
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card