Vue 2 to React Migration: Lessons Learned
Vue 2 to React Migration: Lessons LearnedFebruary 10, 2024

Vue 2 to React Migration: Lessons Learned

Real-world experience migrating a large-scale application from Vue 2 to React. Learn about component mapping, state management migration, routing differences, and strategies to minimize downtime during the transition.

Table of Contents

  1. Migration Overview
  2. Planning and Strategy
  3. Component Architecture Migration
  4. State Management Transition
  5. Routing Migration
  6. Styling and CSS Approach
  7. Testing Strategy
  8. Performance Considerations
  9. Team Training and Adoption
  10. Lessons Learned

Migration Overview {#overview}

Our journey from Vue 2 to React was driven by several factors: better ecosystem support, improved TypeScript integration, and alignment with company-wide technology standards. This comprehensive guide shares our experience migrating a complex e-commerce platform serving 500K+ monthly active users.

Project Background

  • Application: Medical e-commerce platform
  • Scale: 150+ components, 50+ pages
  • Team: 8 frontend developers
  • Timeline: 8 months (gradual migration)
  • Users: 500K+ monthly active users
  • Key Features: User management, product catalog, checkout, analytics dashboard

Migration Drivers

  1. Ecosystem Maturity: React's larger ecosystem and community support
  2. TypeScript Integration: Better type safety and developer experience
  3. Team Expertise: Existing React experience in the team
  4. Performance: Opportunity to implement modern performance optimizations
  5. Maintainability: Simplified architecture and improved code organization

Planning and Strategy {#planning}

Migration Approaches Considered

Big Bang Approach

  • Pros: Complete migration, clean slate
  • Cons: High risk, long development time, feature freeze
  • Decision: Rejected due to business requirements

Gradual Migration (Chosen)

  • Pros: Lower risk, continuous delivery, incremental learning
  • Cons: Temporary complexity, dual maintenance
  • Decision: Selected for minimal disruption

Micro-Frontend Approach

  • Pros: Independent development, technology isolation
  • Cons: Additional complexity, coordination overhead
  • Decision: Considered for future, not current migration

Migration Timeline

Phase 1 (Months 1-2): Foundation
├── Setup React build system alongside Vue
├── Create shared component library
├── Migrate core utilities and services
└── Train team on React best practices

Phase 2 (Months 3-4): Core Components
├── Migrate design system components
├── Implement new routing structure
├── Setup state management with Redux Toolkit
└── Migrate authentication system

Phase 3 (Months 5-6): Feature Migration
├── Migrate user management features
├── Migrate product catalog
├── Migrate checkout flow
└── Update testing infrastructure

Phase 4 (Months 7-8): Finalization
├── Migrate remaining components
├── Performance optimization
├── Remove Vue dependencies
└── Documentation and training

Component Architecture Migration {#component-migration}

Vue to React Component Mapping

Vue 2 Component Structure

<!-- UserProfile.vue -->
<template>
  <div class="user-profile">
    <div class="user-profile__header">
      <img :src="user.avatar" :alt="user.name" />
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    </div>
    
    <div class="user-profile__content">
      <user-stats :stats="userStats" />
      <user-preferences 
        :preferences="user.preferences" 
        @update="updatePreferences" 
      />
    </div>
  </div>
</template>

<script>
import UserStats from './UserStats.vue'
import UserPreferences from './UserPreferences.vue'

export default {
  name: 'UserProfile',
  components: {
    UserStats,
    UserPreferences
  },
  props: {
    userId: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      user: null,
      userStats: null,
      loading: false
    }
  },
  async created() {
    await this.fetchUser()
  },
  methods: {
    async fetchUser() {
      this.loading = true
      try {
        this.user = await this.$api.users.get(this.userId)
        this.userStats = await this.$api.users.getStats(this.userId)
      } catch (error) {
        this.$toast.error('Failed to fetch user data')
      } finally {
        this.loading = false
      }
    },
    async updatePreferences(preferences) {
      try {
        await this.$api.users.updatePreferences(this.userId, preferences)
        this.user.preferences = { ...this.user.preferences, ...preferences }
        this.$toast.success('Preferences updated')
      } catch (error) {
        this.$toast.error('Failed to update preferences')
      }
    }
  }
}
</script>

<style scoped>
.user-profile {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

.user-profile__header {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 2rem;
}
</style>

Equivalent React Component

// UserProfile.jsx
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useNotification } from '../hooks/useNotification';
import { userAPI } from '../services/api';
import UserStats from './UserStats';
import UserPreferences from './UserPreferences';
import './UserProfile.css';

const UserProfile = () => {
  const { userId } = useParams();
  const { showSuccess, showError } = useNotification();
  
  const [user, setUser] = useState(null);
  const [userStats, setUserStats] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (userId) {
      fetchUser();
    }
  }, [userId]);

  const fetchUser = async () => {
    setLoading(true);
    try {
      const [userData, statsData] = await Promise.all([
        userAPI.get(userId),
        userAPI.getStats(userId)
      ]);
      setUser(userData);
      setUserStats(statsData);
    } catch (error) {
      showError('Failed to fetch user data');
    } finally {
      setLoading(false);
    }
  };

  const updatePreferences = async (preferences) => {
    try {
      await userAPI.updatePreferences(userId, preferences);
      setUser(prevUser => ({
        ...prevUser,
        preferences: { ...prevUser.preferences, ...preferences }
      }));
      showSuccess('Preferences updated');
    } catch (error) {
      showError('Failed to update preferences');
    }
  };

  if (loading) {
    return <div className="loading-spinner">Loading...</div>;
  }

  if (!user) {
    return <div className="error-message">User not found</div>;
  }

  return (
    <div className="user-profile">
      <div className="user-profile__header">
        <img src={user.avatar} alt={user.name} />
        <h2>{user.name}</h2>
        <p>{user.email}</p>
      </div>
      
      <div className="user-profile__content">
        <UserStats stats={userStats} />
        <UserPreferences 
          preferences={user.preferences}
          onUpdate={updatePreferences}
        />
      </div>
    </div>
  );
};

export default UserProfile;

Component Migration Patterns

1. Template to JSX Conversion

// Conversion utilities
const templateToJSX = {
  // Vue directive to React prop
  'v-if': (condition) => condition && <Component />,
  'v-for': (items) => items.map((item, index) => <Item key={item.id || index} />),
  'v-model': (value, setValue) => ({ value, onChange: (e) => setValue(e.target.value) }),
  'v-show': (condition) => ({ style: { display: condition ? 'block' : 'none' } }),
  
  // Event handlers
  '@click': (handler) => ({ onClick: handler }),
  '@submit': (handler) => ({ onSubmit: handler }),
  '@input': (handler) => ({ onChange: handler }),
  
  // Class binding
  ':class': (classes) => ({ className: computeClasses(classes) }),
  
  // Style binding
  ':style': (styles) => ({ style: styles })
};

// Helper function for class computation
const computeClasses = (classObj) => {
  if (typeof classObj === 'string') return classObj;
  if (Array.isArray(classObj)) return classObj.filter(Boolean).join(' ');
  if (typeof classObj === 'object') {
    return Object.keys(classObj)
      .filter(key => classObj[key])
      .join(' ');
  }
  return '';
};

2. Lifecycle Methods Migration

// Vue 2 lifecycle to React hooks mapping
const lifecycleMigration = {
  // beforeCreate, created -> useState initialization
  created() {
    // Vue 2
    this.fetchData();
  },
  
  // React equivalent
  useEffect(() => {
    fetchData();
  }, []);

  // mounted -> useEffect with empty dependency
  mounted() {
    // Vue 2
    this.setupEventListeners();
  },
  
  // React equivalent
  useEffect(() => {
    setupEventListeners();
    return () => cleanupEventListeners();
  }, []);

  // beforeDestroy, destroyed -> useEffect cleanup
  beforeDestroy() {
    // Vue 2
    this.cleanup();
  },
  
  // React equivalent (cleanup function)
  useEffect(() => {
    return () => cleanup();
  }, []);

  // watch -> useEffect with dependencies
  watch: {
    userId: {
      handler(newVal, oldVal) {
        this.fetchUserData(newVal);
      },
      immediate: true
    }
  },
  
  // React equivalent
  useEffect(() => {
    if (userId) {
      fetchUserData(userId);
    }
  }, [userId]);
};

3. Props and Events Migration

// Vue 2 prop validation to React PropTypes/TypeScript
// Vue 2
export default {
  props: {
    user: {
      type: Object,
      required: true,
      validator: (user) => user && user.id
    },
    size: {
      type: String,
      default: 'medium',
      validator: (size) => ['small', 'medium', 'large'].includes(size)
    }
  }
}

// React with TypeScript
interface UserCardProps {
  user: {
    id: string;
    name: string;
    email: string;
  };
  size?: 'small' | 'medium' | 'large';
  onEdit?: (user: User) => void;
}

const UserCard: React.FC<UserCardProps> = ({ 
  user, 
  size = 'medium', 
  onEdit 
}) => {
  // Component implementation
};

// Event emission to callback props
// Vue 2
this.$emit('update-user', userData);

// React
onUpdateUser?.(userData);

State Management Transition {#state-management}

From Vuex to Redux Toolkit

Vuex Store Structure

// store/modules/user.js (Vuex)
const state = {
  currentUser: null,
  users: [],
  loading: false,
  error: null
};

const getters = {
  isLoggedIn: (state) => !!state.currentUser,
  userById: (state) => (id) => state.users.find(user => user.id === id),
  userCount: (state) => state.users.length
};

const mutations = {
  SET_CURRENT_USER(state, user) {
    state.currentUser = user;
  },
  SET_USERS(state, users) {
    state.users = users;
  },
  SET_LOADING(state, loading) {
    state.loading = loading;
  },
  SET_ERROR(state, error) {
    state.error = error;
  },
  ADD_USER(state, user) {
    state.users.push(user);
  },
  UPDATE_USER(state, updatedUser) {
    const index = state.users.findIndex(user => user.id === updatedUser.id);
    if (index !== -1) {
      state.users.splice(index, 1, updatedUser);
    }
  }
};

const actions = {
  async fetchUsers({ commit }) {
    commit('SET_LOADING', true);
    try {
      const users = await userAPI.getAll();
      commit('SET_USERS', users);
    } catch (error) {
      commit('SET_ERROR', error.message);
    } finally {
      commit('SET_LOADING', false);
    }
  },
  
  async createUser({ commit }, userData) {
    try {
      const user = await userAPI.create(userData);
      commit('ADD_USER', user);
      return user;
    } catch (error) {
      commit('SET_ERROR', error.message);
      throw error;
    }
  }
};

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
};

Redux Toolkit Equivalent

// store/slices/userSlice.js (Redux Toolkit)
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { userAPI } from '../../services/api';

// Async thunks
export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async (_, { rejectWithValue }) => {
    try {
      const users = await userAPI.getAll();
      return users;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

export const createUser = createAsyncThunk(
  'users/createUser',
  async (userData, { rejectWithValue }) => {
    try {
      const user = await userAPI.create(userData);
      return user;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

export const updateUser = createAsyncThunk(
  'users/updateUser',
  async ({ id, updates }, { rejectWithValue }) => {
    try {
      const user = await userAPI.update(id, updates);
      return user;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

// Slice
const userSlice = createSlice({
  name: 'users',
  initialState: {
    currentUser: null,
    users: [],
    loading: false,
    error: null,
  },
  reducers: {
    setCurrentUser: (state, action) => {
      state.currentUser = action.payload;
    },
    clearError: (state) => {
      state.error = null;
    },
    logout: (state) => {
      state.currentUser = null;
    },
  },
  extraReducers: (builder) => {
    builder
      // Fetch users
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.users = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      })
      // Create user
      .addCase(createUser.fulfilled, (state, action) => {
        state.users.push(action.payload);
      })
      .addCase(createUser.rejected, (state, action) => {
        state.error = action.payload;
      })
      // Update user
      .addCase(updateUser.fulfilled, (state, action) => {
        const index = state.users.findIndex(user => user.id === action.payload.id);
        if (index !== -1) {
          state.users[index] = action.payload;
        }
      });
  },
});

// Selectors
export const selectUsers = (state) => state.users.users;
export const selectCurrentUser = (state) => state.users.currentUser;
export const selectUsersLoading = (state) => state.users.loading;
export const selectUsersError = (state) => state.users.error;
export const selectIsLoggedIn = (state) => !!state.users.currentUser;
export const selectUserById = (id) => (state) => 
  state.users.users.find(user => user.id === id);

export const { setCurrentUser, clearError, logout } = userSlice.actions;
export default userSlice.reducer;

Store Configuration Migration

// Vuex store configuration
import Vue from 'vue';
import Vuex from 'vuex';
import user from './modules/user';
import products from './modules/products';
import cart from './modules/cart';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    user,
    products,
    cart
  },
  strict: process.env.NODE_ENV !== 'production'
});

// Redux Toolkit store configuration
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './slices/userSlice';
import productsReducer from './slices/productsSlice';
import cartReducer from './slices/cartSlice';

export const store = configureStore({
  reducer: {
    users: userReducer,
    products: productsReducer,
    cart: cartReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ['persist/PERSIST'],
      },
    }),
  devTools: process.env.NODE_ENV !== 'production',
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Custom Hooks for State Access

// Custom hooks to replace Vuex mappers
import { useSelector, useDispatch } from 'react-redux';
import { useCallback } from 'react';
import {
  selectUsers,
  selectCurrentUser,
  selectUsersLoading,
  fetchUsers,
  createUser,
  setCurrentUser,
} from '../store/slices/userSlice';

export const useUsers = () => {
  const dispatch = useDispatch();
  const users = useSelector(selectUsers);
  const loading = useSelector(selectUsersLoading);
  const currentUser = useSelector(selectCurrentUser);

  const loadUsers = useCallback(() => {
    dispatch(fetchUsers());
  }, [dispatch]);

  const addUser = useCallback((userData) => {
    return dispatch(createUser(userData));
  }, [dispatch]);

  const setUser = useCallback((user) => {
    dispatch(setCurrentUser(user));
  }, [dispatch]);

  return {
    users,
    currentUser,
    loading,
    loadUsers,
    addUser,
    setUser,
  };
};

// Usage in components
const UserList = () => {
  const { users, loading, loadUsers } = useUsers();

  useEffect(() => {
    loadUsers();
  }, [loadUsers]);

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

  return (
    <div>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
};

Routing Migration {#routing}

Vue Router to React Router

Vue Router Configuration

// router/index.js (Vue Router)
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
import store from '../store';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/users',
    name: 'Users',
    component: () => import('../views/Users.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/users/:id',
    name: 'UserDetail',
    component: () => import('../views/UserDetail.vue'),
    props: true,
    meta: { requiresAuth: true }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/Login.vue'),
    meta: { guest: true }
  }
];

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
});

// Navigation guards
router.beforeEach((to, from, next) => {
  const isLoggedIn = store.getters['user/isLoggedIn'];
  
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!isLoggedIn) {
      next({ name: 'Login' });
    } else {
      next();
    }
  } else if (to.matched.some(record => record.meta.guest)) {
    if (isLoggedIn) {
      next({ name: 'Home' });
    } else {
      next();
    }
  } else {
    next();
  }
});

export default router;

React Router Equivalent

// App.jsx (React Router)
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { useSelector } from 'react-redux';
import ProtectedRoute from './components/ProtectedRoute';
import GuestRoute from './components/GuestRoute';
import LoadingSpinner from './components/LoadingSpinner';
import { selectIsLoggedIn } from './store/slices/userSlice';

// Lazy loaded components
const Home = React.lazy(() => import('./pages/Home'));
const Users = React.lazy(() => import('./pages/Users'));
const UserDetail = React.lazy(() => import('./pages/UserDetail'));
const Login = React.lazy(() => import('./pages/Login'));

const App = () => {
  return (
    <Router>
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/" element={<Home />} />
          
          <Route path="/users" element={
            <ProtectedRoute>
              <Users />
            </ProtectedRoute>
          } />
          
          <Route path="/users/:id" element={
            <ProtectedRoute>
              <UserDetail />
            </ProtectedRoute>
          } />
          
          <Route path="/login" element={
            <GuestRoute>
              <Login />
            </GuestRoute>
          } />
        </Routes>
      </Suspense>
    </Router>
  );
};

// ProtectedRoute component
const ProtectedRoute = ({ children }) => {
  const isLoggedIn = useSelector(selectIsLoggedIn);
  
  if (!isLoggedIn) {
    return <Navigate to="/login" replace />;
  }
  
  return children;
};

// GuestRoute component
const GuestRoute = ({ children }) => {
  const isLoggedIn = useSelector(selectIsLoggedIn);
  
  if (isLoggedIn) {
    return <Navigate to="/" replace />;
  }
  
  return children;
};

export default App;

Route Parameters and Query Handling

// Vue Router parameter access
// In Vue component
export default {
  computed: {
    userId() {
      return this.$route.params.id;
    },
    searchQuery() {
      return this.$route.query.search;
    }
  },
  watch: {
    '$route.params.id': {
      handler(newId) {
        this.fetchUser(newId);
      },
      immediate: true
    }
  }
};

// React Router equivalent
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';

const UserDetail = () => {
  const { id: userId } = useParams();
  const [searchParams, setSearchParams] = useSearchParams();
  const navigate = useNavigate();
  
  const searchQuery = searchParams.get('search') || '';
  
  useEffect(() => {
    if (userId) {
      fetchUser(userId);
    }
  }, [userId]);
  
  const updateSearch = (query) => {
    setSearchParams({ search: query });
  };
  
  const goToUser = (id) => {
    navigate(`/users/${id}`);
  };
  
  return (
    <div>
      {/* Component content */}
    </div>
  );
};

Styling and CSS Approach {#styling}

Scoped Styles Migration

Vue Scoped Styles

<template>
  <div class="user-card">
    <div class="user-card__header">
      <img class="avatar" :src="user.avatar" />
      <h3 class="name">{{ user.name }}</h3>
    </div>
    <div class="user-card__content">
      <p class="email">{{ user.email }}</p>
    </div>
  </div>
</template>

<style scoped>
.user-card {
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 1rem;
}

.user-card__header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

.avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  object-fit: cover;
}

.name {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 600;
  color: #1a202c;
}

.email {
  margin: 0;
  color: #718096;
  font-size: 0.875rem;
}
</style>

CSS Modules Approach (React)

// UserCard.jsx
import React from 'react';
import styles from './UserCard.module.css';

const UserCard = ({ user }) => {
  return (
    <div className={styles.userCard}>
      <div className={styles.header}>
        <img 
          className={styles.avatar} 
          src={user.avatar} 
          alt={user.name}
        />
        <h3 className={styles.name}>{user.name}</h3>
      </div>
      <div className={styles.content}>
        <p className={styles.email}>{user.email}</p>
      </div>
    </div>
  );
};

export default UserCard;
/* UserCard.module.css */
.userCard {
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 1rem;
}

.header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

.avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  object-fit: cover;
}

.name {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 600;
  color: #1a202c;
}

.email {
  margin: 0;
  color: #718096;
  font-size: 0.875rem;
}

Styled Components Alternative

// UserCard.jsx with styled-components
import React from 'react';
import styled from 'styled-components';

const CardContainer = styled.div`
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 1rem;
`;

const Header = styled.div`
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
`;

const Avatar = styled.img`
  width: 48px;
  height: 48px;
  border-radius: 50%;
  object-fit: cover;
`;

const Name = styled.h3`
  margin: 0;
  font-size: 1.125rem;
  font-weight: 600;
  color: #1a202c;
`;

const Email = styled.p`
  margin: 0;
  color: #718096;
  font-size: 0.875rem;
`;

const UserCard = ({ user }) => {
  return (
    <CardContainer>
      <Header>
        <Avatar src={user.avatar} alt={user.name} />
        <Name>{user.name}</Name>
      </Header>
      <div>
        <Email>{user.email}</Email>
      </div>
    </CardContainer>
  );
};

export default UserCard;

Testing Strategy {#testing}

Vue Test Utils to React Testing Library

Vue Component Test

// UserCard.spec.js (Vue Test Utils)
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import UserCard from '@/components/UserCard.vue';

const localVue = createLocalVue();
localVue.use(Vuex);

describe('UserCard.vue', () => {
  let store;
  let wrapper;

  const mockUser = {
    id: '1',
    name: 'John Doe',
    email: 'john@example.com',
    avatar: 'avatar.jpg'
  };

  beforeEach(() => {
    store = new Vuex.Store({
      modules: {
        user: {
          namespaced: true,
          actions: {
            updateUser: jest.fn()
          }
        }
      }
    });

    wrapper = shallowMount(UserCard, {
      localVue,
      store,
      propsData: {
        user: mockUser
      }
    });
  });

  it('renders user information correctly', () => {
    expect(wrapper.find('.name').text()).toBe('John Doe');
    expect(wrapper.find('.email').text()).toBe('john@example.com');
    expect(wrapper.find('.avatar').attributes('src')).toBe('avatar.jpg');
  });

  it('calls updateUser action when edit button is clicked', async () => {
    const editButton = wrapper.find('[data-testid="edit-button"]');
    await editButton.trigger('click');
    
    expect(store._actions['user/updateUser']).toHaveBeenCalled();
  });
});

React Testing Library Equivalent

// UserCard.test.jsx (React Testing Library)
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import userEvent from '@testing-library/user-event';
import UserCard from '../components/UserCard';
import userReducer from '../store/slices/userSlice';

const mockUser = {
  id: '1',
  name: 'John Doe',
  email: 'john@example.com',
  avatar: 'avatar.jpg'
};

const createTestStore = (preloadedState = {}) => {
  return configureStore({
    reducer: {
      users: userReducer,
    },
    preloadedState,
  });
};

const renderWithStore = (component, store = createTestStore()) => {
  return render(
    <Provider store={store}>
      {component}
    </Provider>
  );
};

describe('UserCard', () => {
  it('renders user information correctly', () => {
    renderWithStore(<UserCard user={mockUser} />);
    
    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('john@example.com')).toBeInTheDocument();
    expect(screen.getByAltText('John Doe')).toHaveAttribute('src', 'avatar.jpg');
  });

  it('calls onEdit callback when edit button is clicked', async () => {
    const user = userEvent.setup();
    const onEditMock = jest.fn();
    
    renderWithStore(
      <UserCard user={mockUser} onEdit={onEditMock} />
    );
    
    const editButton = screen.getByTestId('edit-button');
    await user.click(editButton);
    
    expect(onEditMock).toHaveBeenCalledWith(mockUser);
  });
  
  it('displays loading state correctly', () => {
    renderWithStore(<UserCard user={mockUser} loading={true} />);
    
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });
});

Performance Considerations {#performance}

Bundle Size Optimization

Webpack Bundle Analysis

// webpack.config.js optimization
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  // ... other config
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          enforce: true,
        },
      },
    },
  },
  plugins: [
    // Add bundle analyzer for development
    process.env.ANALYZE && new BundleAnalyzerPlugin(),
  ].filter(Boolean),
};

Code Splitting Migration Results

Before Migration (Vue 2):
├── main.js: 1.2MB (gzipped: 350KB)
├── vendor.js: 800KB (gzipped: 250KB)
└── Total: 2MB (gzipped: 600KB)

After Migration (React):
├── main.js: 180KB (gzipped: 55KB)
├── vendor.js: 400KB (gzipped: 120KB)
├── pages/users.js: 120KB (gzipped: 35KB)
├── pages/dashboard.js: 150KB (gzipped: 45KB)
└── Total: 850KB (gzipped: 255KB)

Improvement: 57% reduction in bundle size

Performance Monitoring Results

Core Web Vitals Comparison

// Performance metrics before and after migration
const performanceMetrics = {
  before: {
    LCP: 3.1, // Largest Contentful Paint (seconds)
    FID: 180, // First Input Delay (milliseconds)
    CLS: 0.15, // Cumulative Layout Shift
    TTFB: 1.2, // Time to First Byte (seconds)
    FCP: 2.1   // First Contentful Paint (seconds)
  },
  after: {
    LCP: 1.2,  // 61% improvement
    FID: 85,   // 53% improvement
    CLS: 0.05, // 67% improvement
    TTFB: 0.8, // 33% improvement
    FCP: 1.1   // 48% improvement
  }
};

// SEO improvements
const seoMetrics = {
  before: {
    lighthouseScore: 67,
    googlePageSpeedMobile: 45,
    googlePageSpeedDesktop: 78
  },
  after: {
    lighthouseScore: 95,  // 42% improvement
    googlePageSpeedMobile: 88, // 96% improvement
    googlePageSpeedDesktop: 95 // 22% improvement
  }
};

Team Training and Adoption {#team-training}

Training Program Structure

Phase 1: React Fundamentals (Week 1-2)

Topics Covered:
├── JSX syntax and differences from Vue templates
├── Component lifecycle and hooks
├── Event handling and state management
├── Props vs Vue props and events
└── React developer tools

Learning Resources:
├── Internal React workshops
├── Code review sessions
├── Pair programming exercises
└── Migration examples repository

Phase 2: Advanced Patterns (Week 3-4)

Topics Covered:
├── Custom hooks development
├── Context API and Redux integration
├── Performance optimization techniques
├── Testing with React Testing Library
└── TypeScript integration

Practice Projects:
├── Convert existing Vue components
├── Build new features in React
├── Performance optimization exercises
└── Test coverage improvements

Knowledge Transfer Sessions

Weekly Migration Reviews

// Migration review checklist
const migrationChecklist = {
  codeQuality: [
    'Component follows React best practices',
    'Proper use of hooks and lifecycle methods',
    'TypeScript types are comprehensive',
    'Props interface is well-defined'
  ],
  performance: [
    'Bundle size impact assessed',
    'Rendering performance tested',
    'Memory leaks checked',
    'Core Web Vitals measured'
  ],
  testing: [
    'Unit tests migrated and passing',
    'Integration tests updated',
    'E2E tests still functional',
    'Test coverage maintained'
  ],
  documentation: [
    'Component API documented',
    'Migration notes recorded',
    'Breaking changes listed',
    'Usage examples provided'
  ]
};

Lessons Learned {#lessons-learned}

What Went Well

1. Gradual Migration Strategy

  • Benefit: Minimal disruption to ongoing development
  • Key Factor: Ability to ship features during migration
  • Recommendation: Always choose gradual approach for large applications

2. Strong TypeScript Adoption

  • Benefit: Caught many bugs during migration
  • Key Factor: Better IDE support and developer experience
  • Recommendation: Migrate to TypeScript early in the process

3. Component Library First Approach

  • Benefit: Consistent UI patterns across both frameworks
  • Key Factor: Shared design system reduced duplicated effort
  • Recommendation: Build shared components before migrating features

Challenges and Solutions

1. State Management Complexity

Challenge: Complex Vuex modules with heavy interdependencies

// Problem: Tightly coupled Vuex modules
const userModule = {
  actions: {
    async fetchUser({ dispatch, commit }, userId) {
      // This action depends on other modules
      await dispatch('products/fetchUserProducts', userId, { root: true });
      await dispatch('orders/fetchUserOrders', userId, { root: true });
      // ... more dependencies
    }
  }
};

// Solution: Simplified Redux approach with React Query
const useUserData = (userId) => {
  const user = useQuery(['user', userId], () => userAPI.get(userId));
  const products = useQuery(['userProducts', userId], () => productAPI.getUserProducts(userId));
  const orders = useQuery(['userOrders', userId], () => orderAPI.getUserOrders(userId));
  
  return { user, products, orders };
};

2. CSS Scoping Issues

Challenge: Global CSS conflicts during transition period

/* Problem: Vue scoped styles vs React CSS modules conflicts */
.user-card { /* Vue scoped */ }
.UserCard_user-card_abc123 { /* CSS modules */ }

/* Solution: Namespace approach during transition */
.vue-user-card { /* Temporary Vue namespace */ }
.react-user-card { /* React namespace */ }

3. Event System Differences

Challenge: Vue's global event bus vs React's prop drilling

// Vue event bus usage
this.$bus.$emit('user-updated', userData);

// React solution: Context + custom hooks
const useUserEvents = () => {
  const { updateUser } = useContext(UserContext);
  return { updateUser };
};

Performance Lessons

Bundle Size Management

// Lesson: Tree shaking is more effective with ESM
// Before: CommonJS imports
const { Button, Modal } = require('@/components');

// After: Named ESM imports
import { Button } from '@/components/Button';
import { Modal } from '@/components/Modal';

// Result: 40% reduction in bundle size

Memory Leak Prevention

// Lesson: React requires explicit cleanup
// Vue: Automatic cleanup of watchers and listeners
watch: {
  userId: 'fetchUser'
}

// React: Manual cleanup required
useEffect(() => {
  const controller = new AbortController();
  
  fetchUser(userId, { signal: controller.signal });
  
  return () => controller.abort();
}, [userId]);

Team Productivity Insights

Development Velocity Timeline

Week 1-4: 40% slower (learning curve)
Week 5-8: 80% normal speed (gaining confidence)
Week 9-12: 100% normal speed (full adoption)
Week 13-16: 120% faster (React ecosystem benefits)

Common Pitfalls to Avoid

  1. Don't migrate everything at once

    • Start with leaf components
    • Move up the component tree gradually
  2. Don't ignore TypeScript from the start

    • Type safety prevents many migration bugs
    • Better refactoring confidence
  3. Don't forget about accessibility

    • Vue's automatic form binding vs React's manual approach
    • Ensure ARIA attributes are properly migrated
  4. Don't overlook testing migration

    • Different testing philosophies require new approaches
    • Integration tests are more valuable than unit tests

Conclusion

The Vue 2 to React migration was a significant undertaking that ultimately resulted in improved performance, developer experience, and maintainability. Key success factors included:

  1. Gradual migration approach minimized business disruption
  2. Strong TypeScript adoption caught issues early
  3. Component library first strategy ensured UI consistency
  4. Comprehensive team training maintained development velocity
  5. Performance monitoring validated migration benefits

The 8-month migration resulted in:

  • 57% reduction in bundle size
  • 61% improvement in LCP
  • 42% improvement in Lighthouse scores
  • 20% increase in long-term development velocity

While challenging, the migration positioned our team for future growth and provided valuable lessons for similar large-scale frontend modernization projects.

Next Steps

  • Complete documentation of migrated patterns
  • Knowledge sharing with other teams
  • Continuous performance monitoring and optimization
  • Plan for future migrations (React 18+, concurrent features)

The journey from Vue 2 to React taught us that with proper planning, gradual execution, and team commitment, even large-scale frontend migrations can be successful while maintaining business continuity.