---
name: react-version-migration
description: React version migration guide for Starwards - compatibility matrices, step-by-step upgrade/downgrade between React 17/18/19, Arwes compatibility layer patterns, XState version mapping, and common migration pitfalls
version: 2025-12-03
related_skills:
- arwes-react (Arwes framework details)
- starwards-verification (verify after migration)
- starwards-monorepo (build order during migration)
---
# React Version Migration Expert
Expert guide for migrating Starwards between React 17, 18, and 19.
## Overview
### Version Compatibility Matrix
| Component | React 17 | React 18 | React 19 |
|-----------|----------|----------|----------|
| ReactDOM.render() | ✅ Supported | ⚠️ Deprecated | ❌ Removed |
| createRoot() | ❌ N/A | ✅ Recommended | ✅ Required |
| Old Arwes (alpha.19) | ✅ Compatible | ⚠️ Peer dep warning | ❌ Incompatible |
| @arwes/react (1.0.0-next) | ❌ Incompatible | ✅ Compatible | ❌ Incompatible |
| @arwes-amir/react | ❌ N/A | ❌ N/A | ⚠️ Incomplete |
| @xstate/react v4 | ✅ Compatible | ✅ Compatible | ❌ Unknown |
| @xstate/react v6 | ❌ Unknown | ✅ Compatible | ✅ Compatible |
### Current State
**As of this writing**: Project is on React 18.2.0 with `@arwes/react@1.0.0-next.25020502`
## Migration Guides
### React 17 → React 18
#### 1. Update Packages
**Root `package.json`:**
```json
{
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0"
}
}
```
**`modules/browser/package.json`:**
```json
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
```
#### 2. Update Rendering API (Recommended)
**File**: `modules/browser/src/screens/index.tsx`
Before:
```typescript
import ReactDOM from 'react-dom';
const driver = new Driver(window.location).connect();
ReactDOM.render(, document.querySelector('#wrapper'));
```
After:
```typescript
import { createRoot } from 'react-dom/client';
const driver = new Driver(window.location).connect();
const root = createRoot(document.querySelector('#wrapper')!);
root.render();
```
**Note**: ReactDOM.render() still works in React 18 (compat mode) but triggers console warning.
#### 3. Arwes Compatibility
**Option A**: Keep old Arwes (not recommended)
- Add `--legacy-peer-deps` to `npm install`
- Expect peer dependency warnings
**Option B**: Upgrade to new Arwes (recommended)
- Install `@arwes/react@1.0.0-next.25020502`
- Create arwes-compat.tsx (see "Arwes Compat Layer" section)
#### 4. Run & Verify
```bash
npm install
npm run build
npm test
# Check localhost:3000 - verify no console errors
```
---
### React 18 → React 19
#### 1. Update Packages
**Root `package.json`:**
```json
{
"devDependencies": {
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2"
}
}
```
**`modules/browser/package.json`:**
```json
{
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
}
```
#### 2. MUST Update Rendering API
React 19 **requires** createRoot() - ReactDOM.render() is completely removed.
**File**: `modules/browser/src/screens/index.tsx`
```typescript
import { createRoot } from 'react-dom/client';
const driver = new Driver(window.location).connect();
const root = createRoot(document.querySelector('#wrapper')!);
root.render();
```
#### 3. Arwes Compatibility
**Known Issue**: As of Jan 2025, no fully compatible Arwes version exists for React 19.
Attempted solutions:
- `@arwes/react@1.0.0-next.*` - Requires React 18
- `@arwes-amir/react@^1.0.2` - Incomplete, many missing components
**Workaround**: Create comprehensive arwes-compat.tsx shim (see section below)
#### 4. Update XState React (Optional)
```json
{
"@xstate/react": "^6.0.0"
}
```
Note: v6 works with React 19, v4 compatibility unknown.
#### 5. Verification
```bash
npm install
npm run build
npm test
```
**Expected**: Build succeeds, but UI may have Arwes component gaps.
---
### React 19 → React 18 (Downgrade)
#### 1. Downgrade Packages
**Root `package.json`:**
```json
{
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0"
}
}
```
**`modules/browser/package.json`:**
```json
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@arwes/react": "1.0.0-next.25020502"
}
}
```
Remove `@arwes-amir/react` if present.
#### 2. Keep createRoot() API
React 18 supports createRoot(), so no code changes needed if already using it.
**File**: `modules/browser/src/screens/index.tsx` (no change needed)
```typescript
import { createRoot } from 'react-dom/client';
const driver = new Driver(window.location).connect();
const root = createRoot(document.querySelector('#wrapper')!);
root.render();
```
#### 3. Update arwes-compat.tsx
Ensure compatibility layer matches new Arwes API (see section below).
#### 4. Install & Verify
```bash
npm install
npm run build
npm test
```
---
### React 19/18 → React 17 (Full Revert)
#### 1. Downgrade All Packages
**Root `package.json`:**
```json
{
"devDependencies": {
"@types/react": "^17.0.89",
"@types/react-dom": "^17.0.26"
}
}
```
**`modules/browser/package.json`:**
```json
{
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2",
"@arwes/animation": "1.0.0-alpha.19",
"@arwes/core": "1.0.0-alpha.19",
"@arwes/design": "1.0.0-alpha.19",
"@arwes/sounds": "1.0.0-alpha.19",
"@xstate/react": "^4.1.3"
}
}
```
#### 2. Revert Rendering API
**File**: `modules/browser/src/screens/index.tsx`
```typescript
import ReactDOM from 'react-dom';
const driver = new Driver(window.location).connect();
ReactDOM.render(, document.querySelector('#wrapper'));
```
#### 3. Restore Old Arwes Imports
**Revert all files**:
- `modules/browser/src/components/lobby.tsx`
- `modules/browser/src/components/save-load-game.tsx`
- `modules/browser/src/widgets/monitor.tsx`
- `modules/browser/src/widgets/damage-report.tsx`
Change imports back:
```typescript
import { ArwesThemeProvider, Button, Card, Text } from '@arwes/core';
import { AnimatorGeneralProvider } from '@arwes/animation';
import { BleepsProvider } from '@arwes/sounds';
```
#### 4. Delete arwes-compat.tsx
```bash
rm modules/browser/src/components/arwes-compat.tsx
```
#### 5. Restore Type Definitions
**File**: `custom-typings/arwes__core/index.d.ts`
Restore from git or recreate type definitions for old Arwes.
#### 6. Verify
```bash
npm install
npm run build
npm test
```
---
## Arwes Compatibility Layer
### When to Use arwes-compat.tsx
Use when:
- React 18+ but old Arwes components needed
- New Arwes lacks required components
- Temporary bridge during migration
### Implementation Pattern
**File**: `modules/browser/src/components/arwes-compat.tsx`
#### Core Structure
```typescript
import React, { createContext, useContext } from 'react';
import { AnimatorGeneralProvider as NewAnimatorGeneralProvider } from '@arwes/react';
import type { AnimatorGeneralProviderSettings } from '@arwes/react';
// Re-export what works from new Arwes
export * from '@arwes/react';
// Shim what doesn't exist or has breaking changes
```
#### Theme Provider
Old Arwes had `ArwesThemeProvider`, new doesn't. Create simple wrapper:
```typescript
interface ArwesThemeProviderProps {
children: React.ReactNode;
}
export const ArwesThemeProvider: React.FC = ({ children }) => {
return (
{children}
);
};
export const StylesBaseline: React.FC = () => null;
```
#### Animator Provider
Old API used milliseconds, new uses seconds:
```typescript
interface AnimatorSettings {
duration?: { enter?: number; exit?: number } | number;
}
export const AnimatorGeneralProvider: React.FC<{
animator?: AnimatorSettings;
children: React.ReactNode;
}> = ({ animator, children }) => {
const settings: AnimatorGeneralProviderSettings = {};
if (animator?.duration) {
if (typeof animator.duration === 'number') {
settings.duration = animator.duration / 1000; // ms to seconds
} else {
settings.duration = {
enter: (animator.duration.enter || 200) / 1000,
exit: (animator.duration.exit || 200) / 1000,
};
}
}
return {children};
};
```
#### Bleeps Provider (Audio Stub)
```typescript
const BleepsContext = createContext(null);
export const BleepsProvider: React.FC<{
audioSettings?: any;
playersSettings?: any;
bleepsSettings?: any;
children: React.ReactNode;
}> = ({ children }) => {
// Stub - audio not functional but prevents crashes
const bleepsValue = {
play: (name: string) => {},
stop: (name: string) => {},
};
return {children};
};
```
#### Button Component
```typescript
interface ButtonProps {
palette?: 'primary' | 'success' | 'error' | 'secondary';
onClick?: () => void;
children: React.ReactNode;
}
export const Button: React.FC = ({ palette = 'primary', onClick, children }) => {
const colors = {
primary: '#26daaa',
success: '#0f0',
error: '#f00',
secondary: '#7ef8f0',
};
return (
);
};
```
#### Card Component (CRITICAL)
**Common Error**: `TypeError: options?.map is not a function`
**Root Cause**: Old Arwes accepts ReactNode, not array.
**Correct Implementation**:
```typescript
interface CardProps {
title?: string;
image?: { src: string };
options?: React.ReactNode; // NOT an array!
onClick?: () => void;
children?: React.ReactNode;
style?: React.CSSProperties;
hover?: boolean;
}
export const Card: React.FC = ({ title, image, options, children, style }) => {
return (
{image &&

}
{title &&
{title}
}
{children}
{options &&
{options}
}
);
};
```
**Usage** (lobby.tsx):
```typescript
window.location.assign('gm.html')}>
Game Master
}
>
Manage the game
```
#### FrameCorners Component (CRITICAL)
**Common Error**: Content invisible with cyan background covering viewport.
**Root Cause**: New Arwes FrameCorners uses SVG with full-viewport background rect.
**Solution**: Simple border wrapper:
```typescript
interface FrameCornersProps {
animator?: any;
palette?: string;
hover?: boolean;
children: React.ReactNode;
}
export const FrameCorners: React.FC = ({ children, palette }) => {
const color = palette === 'success' ? '#0f0' : '#26daaa';
return (
{children}
);
};
```
#### Text & Blockquote
```typescript
export const Text: React.FC<{ children: React.ReactNode; style?: React.CSSProperties }> = ({
children,
style,
}) => {
return {children}
;
};
export const Blockquote: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
{children}
);
};
```
---
## Common Issues & Fixes
### Issue 1: TypeError: options?.map is not a function
**Symptoms**: Card component crashes, React error boundary triggered
**Location**: `arwes-compat.tsx` Card component
**Cause**: Card `options` prop defined as array but receives ReactNode
**Fix**: Change interface to accept ReactNode instead of array:
```typescript
// ❌ WRONG
interface CardProps {
options?: CardOption[];
}
// Component uses: options?.map(...)
// ✅ CORRECT
interface CardProps {
options?: React.ReactNode;
}
// Component uses: {options && {options}
}
```
**Files to check**:
- `modules/browser/src/components/arwes-compat.tsx`
- `modules/browser/src/components/lobby.tsx` (usage)
---
### Issue 2: Content Invisible - Cyan Background
**Symptoms**: Page loads with solid cyan background, no text visible
**Location**: Components using FrameCorners
**Cause**: New Arwes FrameCorners renders SVG with full-viewport background rect
**Visual Check**: Inspect element shows:
```html