Codegen SDK provides powerful APIs for modernizing React codebases. This guide will walk you through common React modernization patterns.
Common use cases include:
- Upgrading to modern APIs, including React 18+
- Automatically memoizing components
- Converting to modern hooks
- Standardizing prop types
- Organizing components into individual files
and much more.
Converting Class Components to Functions
Here’s how to convert React class components to functional components:
# Find all React class components
for class_def in codebase.classes:
# Skip if not a React component
if not class_def.is_jsx or "Component" not in [base.name for base in class_def.bases]:
continue
print(f"Converting {class_def.name} to functional component")
# Extract state from constructor
constructor = class_def.get_method("constructor")
state_properties = []
if constructor:
for statement in constructor.code_block.statements:
if "this.state" in statement.source:
# Extract state properties
state_properties = [prop.strip() for prop in
statement.source.split("{")[1].split("}")[0].split(",")]
# Create useState hooks for each state property
state_hooks = []
for prop in state_properties:
hook_name = f"[{prop}, set{prop[0].upper()}{prop[1:]}]"
state_hooks.append(f"const {hook_name} = useState(null);")
# Convert lifecycle methods to effects
effects = []
if class_def.get_method("componentDidMount"):
effects.append("""
useEffect(() => {
// TODO: Move componentDidMount logic here
}, []);
""")
if class_def.get_method("componentDidUpdate"):
effects.append("""
useEffect(() => {
// TODO: Move componentDidUpdate logic here
});
""")
# Get the render method
render_method = class_def.get_method("render")
# Create the functional component
func_component = f"""
const {class_def.name} = ({class_def.get_method("render").parameters[0].name}) => {{
{chr(10).join(state_hooks)}
{chr(10).join(effects)}
{render_method.code_block.source}
}}
"""
# Replace the class with the functional component
class_def.edit(func_component)
# Add required imports
file = class_def.file
if not any("useState" in imp.source for imp in file.imports):
file.add_import("import { useState, useEffect } from 'react';")
Migrating to Modern Hooks
Convert legacy patterns to modern React hooks:
# Find components using legacy patterns
for function in codebase.functions:
if not function.is_jsx:
continue
# Look for common legacy patterns
for call in function.function_calls:
# Convert withRouter to useNavigate
if call.name == "withRouter":
# Add useNavigate import
function.file.add_import(
"import { useNavigate } from 'react-router-dom';"
)
# Add navigate hook
function.insert_before_first_return("const navigate = useNavigate();")
# Replace history.push calls
for history_call in function.function_calls:
if "history.push" in history_call.source:
history_call.edit(
history_call.source.replace("history.push", "navigate")
)
# Convert lifecycle methods in hooks
elif call.name == "componentDidMount":
call.parent.edit("""
useEffect(() => {
// Your componentDidMount logic here
}, []);
""")
Standardizing Props
Inferring Props from Usage
Add proper prop types and TypeScript interfaces based on how props are used:
# Add TypeScript interfaces for props
for function in codebase.functions:
if not function.is_jsx:
continue
# Get props parameter
props_param = function.parameters[0] if function.parameters else None
if not props_param:
continue
# Collect used props
used_props = set()
for prop_access in function.function_calls:
if f"{props_param.name}." in prop_access.source:
prop_name = prop_access.source.split(".")[1]
used_props.add(prop_name)
# Create interface
if used_props:
interface_def = f"""
interface {function.name}Props {{
{chr(10).join(f' {prop}: any;' for prop in used_props)}
}}
"""
function.insert_before(interface_def)
# Update function signature
function.edit(function.source.replace(
f"({props_param.name})",
f"({props_param.name}: {function.name}Props)"
))
Convert inline prop type definitions to separate type declarations:
# Iterate over all files in the codebase
for file in codebase.files:
# Iterate over all functions in the file
for function in file.functions:
# Check if the function is a React functional component
if function.is_jsx: # Assuming is_jsx indicates a function component
# Check if the function has inline props definition
if len(function.parameters) == 1 and isinstance(function.parameters[0].type, Dict):
# Extract the inline prop type
inline_props: TSObjectType = function.parameters[0].type.source
# Create a new type definition for the props
props_type_name = f"{function.name}Props"
props_type_definition = f"type {props_type_name} = {inline_props};"
# Set the new type for the parameter
function.parameters[0].set_type_annotation(props_type_name)
# Add the new type definition to the file
function.insert_before('\n' + props_type_definition + '\n')
This will convert components from:
function UserCard({ name, age }: { name: string; age: number }) {
return (
<div>
{name} ({age})
</div>
);
}
To:
type UserCardProps = { name: string; age: number };
function UserCard({ name, age }: UserCardProps) {
return (
<div>
{name} ({age})
</div>
);
}
Extracting prop types makes them reusable and easier to maintain. It also
improves code readability by separating type definitions from component logic.
Updating Fragment Syntax
Modernize React Fragment syntax:
for function in codebase.functions:
if not function.is_jsx:
continue
# Replace React.Fragment with <>
for element in function.jsx_elements:
if element.name == "React.Fragment":
element.edit(element.source.replace(
"<React.Fragment>",
"<>"
).replace(
"</React.Fragment>",
"</>"
))
Organizing Components into Individual Files
A common modernization task is splitting files with multiple components into a more maintainable structure where each component has its own file. This is especially useful when modernizing legacy React codebases that might have grown organically.
# Initialize a dictionary to store files and their corresponding JSX components
files_with_jsx_components = {}
# Iterate through all files in the codebase
for file in codebase.files:
# Check if the file is in the components directory
if 'components' not in file.filepath:
continue
# Count the number of JSX components in the file
jsx_count = sum(1 for function in file.functions if function.is_jsx)
# Only proceed if there are multiple JSX components
if jsx_count > 1:
# Identify non-default exported components
non_default_components = [
func for func in file.functions
if func.is_jsx and not func.is_exported
]
default_components = [
func for func in file.functions
if func.is_jsx and func.is_exported and func.export.is_default_export()
]
# Log the file path and its components
print(f"📁 {file.filepath}:")
for component in default_components:
print(f" 🟢 {component.name} (default)")
for component in non_default_components:
print(f" 🔵 {component.name}")
# Create a new directory path based on the original file's directory
new_dir_path = "/".join(file.filepath.split("/")[:-1]) + "/" + file.name.split(".")[0]
codebase.create_directory(new_dir_path, exist_ok=True)
# Create a new file path for the component
new_file_path = f"{new_dir_path}/{component.name}.tsx"
new_file = codebase.create_file(new_file_path)
# Log the movement of the component
print(f" 🫸 Moved to: {new_file_path}")
# Move the component to the new file
component.move_to_file(new_file, strategy="add_back_edge")
This script will:
- Find files containing multiple React components
- Create a new directory structure based on the original file
- Move each non-default exported component to its own file
- Preserve imports and dependencies automatically
- Keep default exports in their original location
For example, given this structure:
components/
Forms.tsx # Contains Button, Input, Form (default)
It will create:
components/
Forms.tsx # Contains Form (default)
forms/
Button.tsx
Input.tsx
The strategy="add_back_edge"
parameter ensures that any components that were
previously co-located can still import each other without circular
dependencies. Learn more about moving
code here.