Four Tips to Write Maintainable React Components

It allows us to declare how our components should look, and the library manages everything for us. This is great, because it makes maintenance and refactoring components easy. However, sometimes it’s easy to make mistakes which makes it harder to maintain and debug components.

I’ll discuss some of the simple mistakes that I’ve seen people do, and how we can avoid them.

1. Minimize state

I’m not encouraging to use the eslint plugin which forbids setState. State can actually be very useful. But the problem is unnecessary use of state.

Consider the following example, a simple input with autocomplete, which receives all the data as a prop and filters it to get suggestions as you type:

class AutocompleteInput extends Component {
state = {
text: '',
suggestions: [],
};
_handleChangeText = text => {
const suggestions = this.props.data.filter(item => item.startsWith(text));
this.setState({
text,
suggestions,
});
};
render() {
return (
<div style={styles.container}>
<input
type='text'
value={this.state.text}
onChangeText={this._handleChangeText}
/>
<div style={styles.suggestions}>
{this.state.suggestions.map(suggestion => (
<div style={styles.suggestion}>{suggestion}</div>
))}
</div>
</div>
);
}
}

Note: The onChangeText handler is purely hypothetical :)

This looks pretty innocent. But we already have a bug here. If this.props.data updates, the suggestions won’t update. We can do the following so that suggestions update when the data updates.

componentWillReceiveProps(nextProps) {
const suggestions = nextProps.data.filter(item => item.startsWith(text));
this.setState({
suggestions,
});
}

Agreed, this is a very trivial example, and the bug might not even matter for a simple autocomplete. This becomes visible when as components become more complex and have more logic. While it’s easy to fix, we could’ve avoided this issue altogether. The simple alternative is to calculate suggestions in render.

class AutocompleteInput extends Component {
state = {
text: '',
};
_handleChangeText = text => {
this.setState({
text,
});
};
render() {
const suggestions = this.props.data.filter(item => item.startsWith(this.state.text));
return (
<div style={styles.container}>
<input
type='text'
value={this.state.value}
onChangeText={this._handleChangeText}
/>
<div style={styles.suggestions}>
{suggestions.map(suggestion => (
<div style={styles.suggestion}>{suggestion}</div>
))}
</div>
</div>
);
}
}

Notice how our component becomes smaller and the bug is gone without having to do anything special. The trick is to minimize unnecessary state and move as much logic you can to the render function. In short, always derive from props and state as much as you can.

2. Use the component state

Redux is great, and makes managing complex apps much easier. Though it’s important to know when you need to use Redux and when you don’t.

Consider the following example, a simple form with some data. We store the form state in Redux. So we need a Form.js (presentational component), FormContainer.js (container component which connects the form to redux store), form.js (reducer for updating the form in redux store).

I’ll not write down code for all the files, but will focus on the presentational component.

class Form extends Component {
_updateField = fieldName => value => {
// Action which updates the store
this.props.updateField(fieldName, value);
};
render() {
return (
<form style={styles.form}>
<h1>Create TODO</h1>
<input
type='text'
value={this.props.title}
onChangeText={this._updateField('title')}
/>
<input
type='text'
value={this.props.description}
onChangeText={this._updateField('description')}
/>
</form>
);
}
}
view raw form hosted with ❤ by GitHub

Our component looks nice and simple, but we already had to create 3 files for this (we didn’t even make an action creators file). And there is a hidden bug.

Say the user navigates away from the form. And then comes back to the form later. The form already has the inputs filled with what he had entered previously, which might not be desirable.

To fix it, we’ve got to do the following:

componentWillUnmount() { this.props.clearForm();}

But wait, this still doesn’t fix the issue if we’re actually persisting the redux state to localStorage. We could add logic to ignore the form where we persist the data, but the simple alternative is to not use Redux at all.

class Form extends Component {
state = {
title: '',
description: '',
};
_updateField = fieldName => value => {
this.setState({
[fieldName]: value,
});
};
render() {
return (
<form style={styles.form}>
<h1>Create TODO</h1>
<input
type='text'
value={this.state.title}
onChangeText={this._updateField('title')}
/>
<input
type='text'
value={this.state.description}
onChangeText={this._updateField('description')}
/>
</form>
);
}
}
view raw gistfile1.txt hosted with ❤ by GitHub

Our component’s state is local to the component and we don’t need to do extra work to clear the form when component is unmounted. Sure, we’ve to write a little more inside the component, but it makes up for not having to create all those extra actions and reducer.

Agreed, sometimes it’s desirable to keep the state persisted, but not always. And when it’s desirable, it’s not a lot of work to move the state to Redux.

3. Avoid using refs

Refs are a way to access a property or a method on component instance. They can be useful in some circumstances like focusing an input. But using refs in your own components can make it harder to refactor things.

Refs break encapsulation by exposing component’s internal properties, and it’s difficult to refactor a component without having to search everywhere for what properties are used. In addition, due to the imperative API, most of the time you need to manage things manually, which is always a source of bugs.

Consider the following example, we’ve a component which displays a banner when there are no online users.

class ChatList extends Component {
componentDidMount() {
if (!this.props.users.length) {
this._banner.show();
}
}
render() {
return (
<div style={styles.list}>
<Banner ref={c => this._banner = c}>
No one is online right now :-(
</Banner>
<ChatMessages messages={this.props.messages} />
<OnlineUsers users={this.props.users} />
</div>
);
}

Now this looks okay, but we already have a bug. When number of online users change, we don’t hide the banner. Now that’s easy to fix, we can handle it in componentDidUpdate.

componentDidUpdate(prevProps: Props) {
if (this.props.users !== prevProps.users) {
if (this.props.users.length) {
this._banner.hide();
} else {
this._banner.show();
}
}
}

Ok, now we have extra code to manually show and hide the banner. And it’ll just get more complex as our conditions change, say we want to show the banner only when there are less than two online users, and none of them in our friend list. You can figure out the code, but it could have been much simpler.

Imagine that our <Banner /> had a prop called visible. Now we could just use it declaratively rather than having to manage it all by ourselves.

const ChatList = props => {
return (
<div style={styles.list}>
<Banner visible={!props.users.length}>
No one is online right now :-(
</Banner>
<ChatMessages messages={props.messages} />
<OnlineUsers users={props.users} />
</div>
);
}
view raw const ChatList hosted with ❤ by GitHub

Our code becomes much simpler and there’s no manual work. React will handle it for us.

4. Avoid imperative APIs

One of the things I love about React that it’s declarative. But it takes time get used to do things the react way. Recently, I was refactoring some code in one of our apps and unintentionally broke a totally different component.

One of the two components was rendering buttons in a toolbar.

const ToolbarButtons = props => {
if (!props.allowed) {
return null;
}
return (
<div style={styles.buttons}>
<ProfileButton />
<SettingsButton />
</div>
);
};

The other component was a password form.

class PasswordForm extends Component {
state = {
password: '',
};
_handleChangeText = async password => {
this.setState({
password,
});
const allowed = await this.props.isPasswordAllowed(password);
if (allowed) {
this.navigateTo('details');
this.updateToolbarProps({ allowed: true });
}
};
render() {
return (
<input
type='password'
value={this.state.password}
onChangeText={this._handleChangeText}
/>
);
}
}

There are two things here, first, we’re trying to make the details route protected, by showing the password screen and then manually navigating there when password is allowed.

But it’s not really protected, since any other component can just navigate to details without navigating to the password screen.

Let’s modify our details screen component to actually make it protected.

const DetailsScreen = props => {
if (props.allowed) {
return <Details />;
} else {
return <PasswordForm />;
}
};

Here, we can store the allowed flag in the Redux store or in a parent component state, whichever makes more sense for the scenario.

Second, we manually update props of the toolbar. So the toolbar component is indirectly dependent on the PasswordForm component. If we forget to do this, then our toolbar will never update. We can use the allowed flag from our Redux store instead.

Here we simplified our component by doing less number of manual things and fixed a bug too.

When I started with React, I made many of the above mistakes. It takes a bit of time to get used the React way of doing things. In the end, it’s worth it. I hope this article helped you with writing better code.

Insights

Learn more about Scalability

cover

Jul 16Podcast

Structure Monorepo for Speed, Scale & Sanity

Discover how Nx empowers teams to build and scale monorepos without losing their sanity, from selective testing to AI-powered workflows.

Event Cover

Sep 7Talk

The Invisible Architect

Learn how communication shapes your app development, from code to design. Discover React’s role and leverage Conway’s Law for success.

cover

Dec 27Article

Why Is It Important to Always Run The Latest Version of React Native?

Upgrade your app to the latest version to get more features and better support. Find out more in our article.

cover

Aug 13Article

12 Rules and Approaches of Code Writing for Beginners

If you are a beginning programmer, we prepared a list of 12 rules and approaches of code writing that will help you in your coding journey!

cover

May 28Article

Building Component Libraries for React Native Apps

Satyajit Sahoo shares insights on building component libraries for React Native apps.

cover

Nov 23Article

React Developer’s Everyday Struggle - Extending Big Applications

Do you struggle with extending bigger applications? Check the solutions we arrived at while working on the EEDI app. Contact us to discuss your project!

cover

Mar 1Article

Reusable React Native Components

It’s pretty hard to find simple methods to customize components that could be reused within the app. This article will help you create more efficient code.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

Writing a Swift TurboModule in React Native

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.