Practical Strategies for Decomposing Large Components in React and Vue
Lukas Schneider
DevOps Engineer · Leapcell

Introduction
In the rapidly evolving landscape of frontend development, React and Vue have emerged as dominant forces, empowering developers to build complex and interactive user interfaces. However, with the increasing complexity of applications, a common challenge arises: the growth of large, monolithic components. These "god components" often become unwieldy, difficult to maintain, test, and reuse, leading to slowed development cycles and increased bug potential. This article aims to address this critical issue by exploring practical and effective strategies for deconstructing large React and Vue components. We will delve into the powerful techniques of extracting Custom Hooks/Composables and sub-components, demonstrating how these architectural patterns can transform a tangled codebase into a modular, understandable, and scalable one, ultimately enhancing developer productivity and application robustness.
Core Concepts and Strategies
Before diving into the practical strategies, let's clarify some core terms that form the foundation of component decomposition in React and Vue.
Component: The fundamental building block in both React and Vue. It encapsulates a piece of UI and its associated logic. State: Data that a component manages internally and which can change over time, triggering re-renders. Props: Immutable data passed down from a parent component to a child component, enabling communication and configuration. Side Effects: Operations that interact with the outside world (e.g., data fetching, DOM manipulation, subscriptions) and are typically handled after rendering.
Custom Hooks (React) / Composables (Vue)
Custom Hooks in React and Composables in Vue are powerful mechanisms for extracting stateful logic from components into reusable functions. They allow you to encapsulate related logic, state, and side effects, making components cleaner and more focused on rendering UI.
Principle: Identify recurring logic, state management, or side effects within a component. This logic often doesn't directly concern the component's rendering responsibility.
Implementation (React - Custom Hook):
Consider a large ProductDetails component that handles fetching product data, managing loading states, and handling favorite status.
// Before: Large ProductDetails component function ProductDetails({ productId }) { const [product, setProduct] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isFavorite, setIsFavorite] = useState(false); const [error, setError] = useState(null); useEffect(() => { const fetchProduct = async () => { setIsLoading(true); try { const response = await fetch(`/api/products/${productId}`); if (!response.ok) throw new Error('Failed to fetch product'); const data = await response.json(); setProduct(data); // Assume logic to check if product is favorite setIsFavorite(data.isFavoriteByUser); } catch (err) { setError(err.message); } finally { setIsLoading(false); } }; fetchProduct(); }, [productId]); const toggleFavorite = () => { // API call to update favorite status setIsFavorite(prev => !prev); }; if (isLoading) return <div>Loading product...</div>; if (error) return <div>Error: {error}</div>; if (!product) return <div>Product not found.</div>; return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <button onClick={toggleFavorite}> {isFavorite ? 'Remove from Favorites' : 'Add to Favorites'} </button> {/* ... more UI elements */} </div> ); }
Now, let's extract the data fetching and favorite toggling logic into custom hooks:
// Custom Hook: useProductData.js import { useState, useEffect } from 'react'; function useProductData(productId) { const [product, setProduct] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchProduct = async () => { setIsLoading(true); setError(null); try { const response = await fetch(`/api/products/${productId}`); if (!response.ok) throw new Error('Failed to fetch product'); const data = await response.json(); setProduct(data); } catch (err) { setError(err.message); } finally { setIsLoading(false); } }; fetchProduct(); }, [productId]); return { product, isLoading, error, setProduct }; } // Custom Hook: useFavoriteToggle.js import { useState } from 'react'; function useFavoriteToggle(initialIsFavorite) { const [isFavorite, setIsFavorite] = useState(initialIsFavorite); const toggleFavorite = async (productId) => { // productId might be needed for API call // Simulate API call console.log(`Toggling favorite for product ID: ${productId}`); await new Promise(resolve => setTimeout(resolve, 500)); setIsFavorite(prev => !prev); // In a real app, you'd make an actual API call here }; return { isFavorite, toggleFavorite, setIsFavorite }; } // After: Refactored ProductDetails component function ProductDetails({ productId }) { const { product, isLoading, error, setProduct } = useProductData(productId); const { isFavorite, toggleFavorite } = useFavoriteToggle(product?.isFavoriteByUser || false); // Initialize with product's favorite status // Update product's favorite status when isFavorite changes useEffect(() => { if (product) { setProduct(prev => ({ ...prev, isFavoriteByUser: isFavorite })); } }, [isFavorite, product, setProduct]); if (isLoading) return <div>Loading product...</div>; if (error) return <div>Error: {error}</div>; if (!product) return <div>Product not found.</div>; return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <button onClick={() => toggleFavorite(productId)}> {isFavorite ? 'Remove from Favorites' : 'Add to Favorites'} </button> {/* ... more UI elements */} </div> ); }
Implementation (Vue - Composable):
Let's adapt the same product details scenario for Vue 3 with the Composition API.
<!-- Before: Large ProductDetails component --> <template> <div> <div v-if="isLoading">Loading product...</div> <div v-else-if="error">Error: {{ error }}</div> <div v-else-if="!product">Product not found.</div> <div v-else> <h1>{{ product.name }}</h1> <p>{{ product.description }}</p> <button @click="toggleFavorite"> {{ isFavorite ? 'Remove from Favorites' : 'Add to Favorites' }} </button> <!-- ... more UI elements --> </div> </div> </template> <script setup> import { ref, onMounted, watch } from 'vue'; const props = defineProps({ productId: String, }); const product = ref(null); const isLoading = ref(true); const isFavorite = ref(false); const error = ref(null); const fetchProduct = async () => { isLoading.value = true; error.value = null; try { const response = await fetch(`/api/products/${props.productId}`); if (!response.ok) throw new Error('Failed to fetch product'); const data = await response.json(); product.value = data; isFavorite.value = data.isFavoriteByUser; } catch (err) { error.value = err.message; } finally { isLoading.value = false; } }; const toggleFavorite = async () => { // Simulate API call console.log(`Toggling favorite for product ID: ${props.productId}`); await new Promise(resolve => setTimeout(resolve, 500)); isFavorite.value = !isFavorite.value; // In a real app, you'd make an actual API call here }; onMounted(fetchProduct); watch(() => props.productId, fetchProduct); </script>
Now, extracting the logic into Composables:
<!-- useProductData.js --> import { ref, onMounted, watch } from 'vue'; export function useProductData(productIdRef) { // productIdRef is a ref const product = ref(null); const isLoading = ref(true); const error = ref(null); const fetchProduct = async () => { isLoading.value = true; error.value = null; try { const response = await fetch(`/api/products/${productIdRef.value}`); if (!response.ok) throw new Error('Failed to fetch product'); const data = await response.json(); product.value = data; } catch (err) { error.value = err.message; } finally { isLoading.value = false; } }; onMounted(fetchProduct); watch(productIdRef, fetchProduct); return { product, isLoading, error, fetchProduct }; // Expose fetchProduct if needed } <!-- useFavoriteToggle.js --> import { ref } from 'vue'; export function useFavoriteToggle(initialIsFavorite) { const isFavorite = ref(initialIsFavorite); const toggleFavorite = async (productId) => { // productId might be needed for API call console.log(`Toggling favorite for product ID: ${productId}`); await new Promise(resolve => setTimeout(resolve, 500)); isFavorite.value = !isFavorite.value; // Real API call here }; return { isFavorite, toggleFavorite }; } <!-- After: Refactored ProductDetails component --> <template> <div> <div v-if="isLoading">Loading product...</div> <div v-else-if="error">Error: {{ error }}</div> <div v-else-if="!product">Product not found.</div> <div v-else> <h1>{{ product.name }}</h1> <p>{{ product.description }}</p> <button @click="toggleFavorite(productId)"> {{ isFavorite ? 'Remove from Favorites' : 'Add to Favorites' }} </button> <!-- ... more UI elements --> </div> </div> </template> <script setup> import { ref, watchEffect } from 'vue'; import { useProductData } from './useProductData.js'; import { useFavoriteToggle } from './useFavoriteToggle.js'; const props = defineProps({ productId: String, }); const productIdRef = ref(props.productId); // Create a ref from prop for composable const { product, isLoading, error } = useProductData(productIdRef); const { isFavorite, toggleFavorite } = useFavoriteToggle(product.value?.isFavoriteByUser || false); // Watch for product changes and update initialIsFavorite for the composable // This is a common pattern when initial composable state depends on async data watchEffect(() => { if (product.value) { isFavorite.value = product.value.isFavoriteByUser; } }); // If product updates, make sure the favorite status inside product also updates watch(isFavorite, (newVal) => { if (product.value) { product.value.isFavoriteByUser = newVal; } }); </script>
Composables/Custom Hooks are ideal for:
- Encapsulating reusable logic (e.g., form validation, data fetching, authentication).
- Managing complex state derived from multiple sources.
- Handling side effects cleanly without cluttering the component's render logic.
Child Components
Child components are another fundamental way to decompose large components. The principle here is to break down the UI and its immediate logic based on visual and logical boundaries.
Principle: Look for distinct sections within a component's render output. If a section has its own state (even if minimal), props, or complex rendering logic, it's a candidate for a child component.
Implementation (React):
Continuing with the ProductDetails example, let's say the product details view also includes a ProductReview section and an AddToCartButton.
// Before: All in ProductDetails function ProductDetails({ productId }) { // ... data fetching and favorite toggling logic as before ... if (isLoading) return <div>Loading product...</div>; if (error) return <div>Error: {error}</div>; if (!product) return <div>Product not found.</div>; return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <button onClick={() => toggleFavorite(productId)}> {isFavorite ? 'Remove from Favorites' : 'Add to Favorites'} </button> {/* Product reviews section directly here */} <h2>Reviews</h2> {product.reviews.length > 0 ? ( <ul> {product.reviews.map(review => ( <li key={review.id}> <strong>{review.author}</strong> - {review.rating}/5 <p>{review.comment}</p> </li> ))} </ul> ) : ( <p>No reviews yet.</p> )} {/* Add to cart section directly here */} <button onClick={() => alert(`Added ${product.name} to cart!`)}> Add to Cart </button> </div> ); }
Now, extract ProductReviews and AddToCartButton as child components:
// ProductReviews.jsx function ProductReviews({ reviews }) { if (reviews.length === 0) { return <p>No reviews yet.</p>; } return ( <div> <h2>Reviews</h2> <ul> {reviews.map(review => ( <li key={review.id}> <strong>{review.author}</strong> - {review.rating}/5 <p>{review.comment}</p> </li> ))} </ul> </div> ); } // AddToCartButton.jsx function AddToCartButton({ productName, onAddToCart }) { return ( <button onClick={onAddToCart}> Add {productName} to Cart </button> ); } // After: Refactored ProductDetails component function ProductDetails({ productId }) { const { product, isLoading, error } = useProductData(productId); const { isFavorite, toggleFavorite } = useFavoriteToggle(product?.isFavoriteByUser || false); // ... useEffect for syncing favorite state as before ... if (isLoading) return <div>Loading product...</div>; if (error) return <div>Error: {error}</div>; if (!product) return <div>Product not found.</div>; const handleAddToCart = () => { alert(`Added ${product.name} to cart!`); // In a real app, dispatch an action to a cart store }; return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <button onClick={() => toggleFavorite(productId)}> {isFavorite ? 'Remove from Favorites' : 'Add to Favorites'} </button> <ProductReviews reviews={product.reviews || []} /> <AddToCartButton productName={product.name} onAddToCart={handleAddToCart} /> </div> ); }
Implementation (Vue):
<!-- Before: All in ProductDetails (similar to React example, but using Vue template syntax) --> <!-- Same as the previous Vue example, but imagine reviews and add to cart logic directly in its template --> <!-- ProductReviews.vue --> <template> <div> <h2>Reviews</h2> <ul v-if="reviews.length > 0"> <li v-for="review in reviews" :key="review.id"> <strong>{{ review.author }}</strong> - {{ review.rating }}/5 <p>{{ review.comment }}</p> </li> </ul> <p v-else>No reviews yet.</p> </div> </template> <script setup> defineProps({ reviews: { type: Array, default: () => [], }, }); </script> <!-- AddToCartButton.vue --> <template> <button @click="handleClick"> Add {{ productName }} to Cart </button> </template> <script setup> const props = defineProps({ productName: String, }); const emit = defineEmits(['add-to-cart']); const handleClick = () => { emit('add-to-cart', props.productName); }; </script> <!-- After: Refactored ProductDetails component --> <template> <div> <div v-if="isLoading">Loading product...</div> <div v-else-if="error">Error: {{ error }}</div> <div v-else-if="!product">Product not found.</div> <div v-else> <h1>{{ product.name }}</h1> <p>{{ product.description }}</p> <button @click="toggleFavorite(productId)"> {{ isFavorite ? 'Remove from Favorites' : 'Add to Favorites' }} </button> <ProductReviews :reviews="product.reviews || []" /> <AddToCartButton :product-name="product.name" @add-to-cart="handleAddToCart" /> </div> </div> </template> <script setup> import { ref, watchEffect } from 'vue'; import { useProductData } from './useProductData.js'; import { useFavoriteToggle } from './useFavoriteToggle.js'; import ProductReviews from './ProductReviews.vue'; import AddToCartButton from './AddToCartButton.vue'; const props = defineProps({ productId: String, }); const productIdRef = ref(props.productId); const { product, isLoading, error } = useProductData(productIdRef); const { isFavorite, toggleFavorite } = useFavoriteToggle(product.value?.isFavoriteByUser || false); watchEffect(() => { if (product.value) { isFavorite.value = product.value.isFavoriteByUser; } }); watch(isFavorite, (newVal) => { if (product.value) { product.value.isFavoriteByUser = newVal; } }); const handleAddToCart = (productName) => { alert(`Added ${productName} to cart!`); // Dispatch action to cart store }; </script>
Child components are effective for:
- Improving readability by abstracting away complex UI blocks.
- Enabling reusability of UI elements across different parts of the application.
- Optimizing rendering performance (React's
memoor Vue's inherent reactivity can prevent unnecessary re-renders of child components if their props haven't changed). - Enforcing separation of concerns, where each component has a clear responsibility.
When to use which?
- Custom Hooks/Composables are for logic reuse and separation of concerns related to data and behavior. They don't render anything directly but provide state and functions.
- Child Components are for UI reuse and separation of concerns related to presentation. They encapsulate a part of the visual interface.
Often, you'll use both together. A child component might utilize a custom hook/composable for its internal logic, further enhancing modularity.
Conclusion
Decomposing large React and Vue components into smaller, focused units using Custom Hooks/Composables and child components is not merely a stylistic choice; it's a fundamental practice for building maintainable, scalable, and high-quality frontend applications. By consistently applying these strategies, developers can achieve cleaner code, easier debugging, improved reusability, and enhanced collaboration within teams, ultimately leading to a more robust and delightful user experience. Master these decomposition techniques to transform monolithic components into a harmonious orchestra of modular parts.