To follow up on part 5 of my series on writing better react code, I think it’s worth discussing why small components are the key to keeping your react projects well organized, maintainable, and scalable.
Why should you use small components?
Here are a few reasons why you should avoid writing all your code in a large jsx
file, and get in the habit of writing small components
- Reusability: Small components can be reused in multiple places across your application. This means you can write code once and use it many times, making your application more efficient and easier to maintain.
- Readability: Small components are easier to read and understand than large components with many nested elements. This makes it easier for developers to understand how the component works and what it does.
- Modularity: Small components are modular, meaning they can be combined with other components to create more complex functionality. This allows you to build up your application using small, reusable building blocks.
- Maintainability: Small components are easier to maintain than large components because they have a single responsibility. If a small component needs to be updated or fixed, you only need to change that one component, rather than a large, complex component that does many things.
- Performance: Small components can be optimized for performance more easily than large components. This is because small components can be memoized or optimized in other ways to avoid unnecessary re-renders.
An Example
An example of a component that is too large in React could be a component that handles too many responsibilities or has too many nested elements. For example, consider a component that handles a user’s profile page. A large profile component might look something like this:
const Profile = () => { state = { name: '', email: '', avatar: '', bio: '', interests: [], friends: [], posts: [], } componentDidMount() { // Fetch user data from API and update state } handleNameChange = (event) => { // Update name in state } handleEmailChange = (event) => { // Update email in state } handleAvatarChange = (event) => { // Update avatar in state } handleBioChange = (event) => { // Update bio in state } handleInterestsChange = (event) => { // Update interests in state } handleFriendAdd = (friend) => { // Add friend to state } handlePostCreate = (post) => { // Add post to state } render() { const { name, email, avatar, bio, interests, friends, posts } = this.state; return ( <div> <h1>Profile</h1> <div className="profile-container"> <div className="profile-details"> <form> <label>Name:</label> <input type="text" value={name} onChange={this.handleNameChange} /> <label>Email:</label> <input type="text" value={email} onChange={this.handleEmailChange} /> <label>Avatar:</label> <input type="text" value={avatar} onChange={this.handleAvatarChange} /> <label>Bio:</label> <textarea value={bio} onChange={this.handleBioChange} /> </form> <Interests interests={interests} onInterestsChange={this.handleInterestsChange} /> </div> <div className="profile-friends"> <Friends friends={friends} onFriendAdd={this.handleFriendAdd} /> </div> </div> <Posts posts={posts} onPostCreate={this.handlePostCreate} /> </div> ); } }
In this example, the Profile
component is handling too many responsibilities. It is responsible for fetching user data from an API, updating state based on user input, rendering profile details, rendering a list of interests, rendering a list of friends, and rendering a list of posts. This makes the component difficult to read.
The Rewrite
To reduce the responsibilities of the Profile
component, we can extract some of its logic into smaller, more specialized components. One way to do this is to identify the different sections of the profile (i.e. details, interests, friends, and posts) and create a separate component for each section.
Here’s an example of how we can refactor the Profile
component:
const Profile = () => { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [avatar, setAvatar] = useState(''); const [bio, setBio] = useState(''); const [interests, setInterests] = useState([]); const [friends, setFriends] = useState([]); const [posts, setPosts] = useState([]); useEffect(() => { // Fetch user data from API and update state }, []); const handleNameChange = useCallback((event) => { setName(event.target.value); }, []); const handleEmailChange = useCallback((event) => { setEmail(event.target.value); }, []); const handleAvatarChange = useCallback((event) => { setAvatar(event.target.value); }, []); const handleBioChange = useCallback((event) => { setBio(event.target.value); }, []); const handleInterestsChange = useCallback((newInterests) => { setInterests(newInterests); }, []); const handleFriendAdd = useCallback((newFriend) => { setFriends((prevFriends) => [...prevFriends, newFriend]); }, []); const handlePostCreate = useCallback((newPost) => { setPosts((prevPosts) => [...prevPosts, newPost]); }, []); return ( <div> <h1>Profile</h1> <div className="profile-container"> <ProfileDetails name={name} email={email} avatar={avatar} onNameChange={handleNameChange} onEmailChange={handleEmailChange} onAvatarChange={handleAvatarChange} onBioChange={handleBioChange} /> <InterestsSection interests={interests} onInterestsChange={handleInterestsChange} /> <FriendsSection friends={friends} onFriendAdd={handleFriendAdd} /> <PostsSection posts={posts} onPostCreate={handlePostCreate} /> </div> </div> ); };
In this refactored version, we’ve separated the details section into a ProfileDetails
component that handles all of the input changes related to name, email, avatar, and bio. We’ve also created three more components to handle interests, friends, and posts.
Each of these components now has a more focused responsibility, which makes the overall Profile
components are easier to understand and maintain. Additionally, each of these smaller components can be reused in other parts of the application if needed.