Understanding the Problem
In this phase, we'll refactor the Folder component from a class component to a function component. The Folder component represents a more complex challenge than the App component because it:
- Receives and uses props to render dynamic content
- Maintains its own state to track the currently selected tab
- Interacts with a child component (
Headers) - Contains more complex rendering logic
This phase will teach us how to handle props in function components and how component composition works with hooks.
George Polya's Problem-Solving Method
Step 1: Understand the Problem
Let's analyze the original Folder class component to understand its functionality:
// Folder.jsx
import React from 'react';
const Headers = ({ titles, currentTab, selectTab }) => {
const handleClick = (e) => {
const idx = parseInt(e.target.id, 10);
selectTab(idx);
}
const tabs = titles.map((title, idx) => {
const headerClass = (idx === currentTab) ? 'active' : '';
return (
<li
key={idx}
id={idx}
onClick={handleClick}
className={headerClass}
>
{title}
</li>
);
});
return (
<ul className='tab-header'>
{tabs}
</ul>
);
}
class Folder extends React.Component {
constructor(props) {
super(props);
this.state = {
currentTab: 0
};
}
selectTab = (num) => {
this.setState({ currentTab: num });
}
render() {
const folder = this.props.folders[this.state.currentTab];
const titles = this.props.folders.map((folder) => folder.title);
return (
<section className="tabs-section">
<h1>Tabs</h1>
<div className='tabs'>
<Headers
titles={titles}
currentTab={this.state.currentTab}
selectTab={this.selectTab}
/>
<div className='tab-content'>
{folder.content}
</div>
</div>
</section>
);
}
}
export default Folder;
Analyzing this code, we can see that:
- The
Foldercomponent takes an array of folder objects as props, each withtitleandcontentproperties - It maintains a
currentTabstate variable (initialized to 0) to track which tab is selected - It has a
selectTabmethod that updates thecurrentTabstate - It extracts the titles from the folders array and passes them to the
Headerscomponent - It displays the content of the currently selected folder
- The
Headerscomponent is already a function component, so we don't need to refactor it
Step 2: Devise a Plan
- Convert the
Folderclass declaration to a function declaration - Use object destructuring in the function parameters to directly access the
foldersprop - Replace
this.state.currentTabwith auseStatehook - Convert the
selectTabmethod to a regular function - Update references to
this.stateandthis.propsin the JSX - Remove the
rendermethod and return JSX directly
Step 3: Carry Out the Plan
1. Component Declaration and Props
We'll change the class declaration to a function declaration and use object destructuring for props:
// From
class Folder extends React.Component {
// Accessing props via this.props.folders
// To
function Folder({ folders }) {
// Directly accessing folders prop
This destructuring syntax is a clean way to extract specific props directly in the function signature, making it immediately clear what props the component expects.
2. State Management with useState
We'll replace the class state with useState:
// From
constructor(props) {
super(props);
this.state = {
currentTab: 0
};
}
// To
const [currentTab, setCurrentTab] = useState(0);
Don't forget to import useState at the top of your file:
import React, { useState } from 'react';
3. Converting Methods
We'll convert the selectTab method to a regular function:
// From
selectTab = (num) => {
this.setState({ currentTab: num });
}
// To
const selectTab = (num) => {
setCurrentTab(num);
};
4. Updating References
We'll update the variable access and JSX references:
// From
const folder = this.props.folders[this.state.currentTab];
const titles = this.props.folders.map((folder) => folder.title);
// To
const folder = folders[currentTab];
const titles = folders.map((folder) => folder.title);
// From
<Headers
titles={titles}
currentTab={this.state.currentTab}
selectTab={this.selectTab}
/>
// To
<Headers
titles={titles}
currentTab={currentTab}
selectTab={selectTab}
/>
5. Putting It All Together
Here's the complete refactored Folder component:
// Folder.jsx (Refactored)
import React, { useState } from 'react';
const Headers = ({ titles, currentTab, selectTab }) => {
const handleClick = (e) => {
const idx = parseInt(e.target.id, 10);
selectTab(idx);
}
const tabs = titles.map((title, idx) => {
const headerClass = (idx === currentTab) ? 'active' : '';
return (
<li
key={idx}
id={idx}
onClick={handleClick}
className={headerClass}
>
{title}
</li>
);
});
return (
<ul className='tab-header'>
{tabs}
</ul>
);
}
function Folder({ folders }) {
// Replace this.state with useState
const [currentTab, setCurrentTab] = useState(0);
// Convert selectTab method to a regular function
const selectTab = (num) => {
setCurrentTab(num);
};
// Get the current folder and titles
const folder = folders[currentTab];
const titles = folders.map((folder) => folder.title);
// Return what was previously in the render method
return (
<section className="tabs-section">
<h1>Tabs</h1>
<div className='tabs'>
<Headers
titles={titles}
currentTab={currentTab}
selectTab={selectTab}
/>
<div className='tab-content'>
{folder.content}
</div>
</div>
</section>
);
}
export default Folder;
Step 4: Look Back and Review
Let's verify that our refactored component maintains the same functionality as the original:
- The
currentTabstate is properly initialized to 0 - The
selectTabfunction correctly updates thecurrentTabstate - The
Headerscomponent still receives the necessary props - The content of the selected folder is displayed correctly
- The interaction between
FolderandHeadersis preserved
Run the tests to confirm everything is working as expected:
npm test
All tests for the Folder component should pass, confirming that our refactoring was successful.
Component Composition and Props in Function Components
Working with Props
One of the key differences between class and function components is how they handle props:
In Class Components:
Props are accessed through this.props, which is an object containing all the properties passed to the component:
class MyComponent extends React.Component {
render() {
const { prop1, prop2 } = this.props; // Destructuring within methods
return <div>{this.props.prop1}, {prop2}</div>;
}
}
In Function Components:
Props are received as the first parameter to the function, and can be destructured directly in the function signature:
function MyComponent({ prop1, prop2 }) {
return <div>{prop1}, {prop2}</div>;
}
This is more concise and eliminates the need for this, which is often a source of confusion in React.
Component Composition
Our Folder component demonstrates a common pattern in React: component composition. The Folder component uses a Headers component to render the tab headers, creating a clean separation of concerns:
Foldermanages the overall state and layoutHeadershandles the specific rendering and interaction of the tab headers
This pattern works the same way in both class and function components, but function components often make the data flow clearer because:
- Props are explicitly received at the top of the component
- State variables are declared at the top level alongside their update functions
- The relationship between state, props, and child components is more visible
Real-World Applications
The tabs pattern we've implemented is extremely common in web applications and can be found in many interfaces:
- Navigation tabs: Switching between different sections of a page or application
- Settings panels: Organizing different categories of settings
- Dashboard widgets: Displaying different data views
- Form sections: Breaking up long forms into manageable chunks
- Product details: Showing different aspects of a product (description, specifications, reviews)
The pattern can be extended to more complex interactions:
- Tabs with dynamic content loaded from an API
- Tabs that remember their state even when not visible
- Nested tabs (tabs within tabs)
- Tabs with animations or transitions
- Tabs that can be added, removed, or reordered by the user
By understanding how to implement this pattern with function components and hooks, you're building a foundation for creating many types of interactive interfaces.
Comparison: Class vs. Function Component Props
Let's compare how props are handled in class vs. function components side by side:
| Feature | Class Component | Function Component |
|---|---|---|
| Accessing props | this.props.propName |
propName (after destructuring) |
| Default props | Component.defaultProps = { prop: value } |
Default parameters: function Component({ prop = value }) |
| Props destructuring | Inside methods: const { prop1, prop2 } = this.props; |
In function parameters: function Component({ prop1, prop2 }) |
| Prop types | Component.propTypes = { prop: PropTypes.string } |
Same: Component.propTypes = { prop: PropTypes.string } |
| Renaming props | const { longPropName: shortName } = this.props; |
function Component({ longPropName: shortName }) |
| Rest props | More complex: requires separate destructuring and rest | Simple: function Component({ prop1, ...restProps }) |
As you can see, function components generally provide a more concise and straightforward way to work with props, leveraging standard JavaScript features like destructuring and default parameters.