What Became Hard to Change as Our React App Grew
A small React app feels friendly. A few screens, a few components, and you know where everything lives. Then more features arrive. You change one field and discover it has a social life across the entire codebase. A sh

A small React app feels friendly. A few screens, a few components, and you know where everything lives.
Then more features arrive.
You change one field and discover it has a social life across the entire codebase. A shared form needs another special case. A button disappears because a condition changed somewhere else.
With vibe coding, it's easy to keep adding features once the screen works. Without checking how the new code fits into the app, repeated rules and extra copies of data can go unnoticed. As the app grows, those small problems become harder to find and fix.
The app still works. Making even a small change can turn into a treasure hunt.
Here are a few places where things get harder to change, and what can help keep them manageable.
π§© The shared component that tried to do everything
A shared form starts with a reasonable goal: reuse the same fields across two screens.
Then one screen needs different validation. Another hides a section. Another submits to a different endpoint.
Soon, the component has so many true-or-false props that using it feels like filling out a questionnaire.
Sharing UI pieces that work the same way helps. Sharing a whole form gets harder when each screen needs different behavior.
Share common inputs and layouts. Let each feature decide its own rules. Build each screen from smaller components, so one shared form doesn't need to handle every case.
A little duplication can be easier to maintain than a growing collection of special cases.
π¦ The same data, now available in several flavours
For example, a customer list loads from the API. The selected customer's details are copied into a global store and reused on the details or edit page.
The idea is to save another fetch because the data is already available.
After an update, the query cache has the latest details, but the separate copy still has the old ones. Same customer, two different stories.
Store the selected customer's ID and read their details through the query layer. It can reuse cached data and refresh it when needed, based on its settings. Using an ID doesn't automatically mean another request.
Keep temporary UI state near the component. A form can also keep its own draft while the user edits. Just decide what happens when they save, cancel, or receive newer data.
π The field rename that became a group project
Reading API fields directly inside components feels convenient.
Until a backend field changes and several screens need edits.
If several screens turn an API response into the same display data, move that work into one function near the API call. Components can use the result without repeating those steps.
Do this when it reduces repeated work. Adding a layer that only renames fields can give you more code to maintain.
TypeScript won't check whether incoming JSON matches your types. You need to validate the response while the app is running if you want that check.
π The same rule, three different versions
An invoice can be edited only while its status is DRAFT. That check appears in the table's Edit button, the details page, and the edit form.
Later, the rule changes: locked invoices cannot be edited, even when they're drafts.
The table and form get updated. The details page gets missed. It still offers an Edit button for locked invoices. Apparently, it didn't get the memo.
Keep the shared check in one function inside the invoice feature:
function canEditInvoice(invoice: Invoice) {
return invoice.status === "DRAFT" && !invoice.isLocked;
}
Each screen uses canEditInvoice(invoice), so the rule has one place to change and test.
The backend must check the rule too. Hiding the Edit button doesn't stop someone sending an API request.
πΈοΈ The import that brought its whole family
A feature imports a hook from another feature. That hook imports a store from somewhere else.
Now a small change comes with a guided tour of the application.
Keep related code together. Decide which functions and components other features can import. Move code used by several features into shared modules when it makes sense.
Folders help people find code. Code reviews and rules about allowed imports help keep features from depending on each other's internal code.
π§ͺ The tests that objected to moving furniture
You refactor a component. The screen behaves exactly as before.
The tests disagree.
Tests that check internal state or replace most hooks with fake versions can break when you change how a component works.
Test what the user can do: entering data, submitting a form, and seeing the expected result. Test business rules directly, and use end-to-end tests to check important tasks across the app.
A refactor should feel safer because the tests exist.
π§ One smaller fix before the grand rewrite
When changes become difficult, a rewrite can sound tempting. So can a new state library.
Start with one awkward change instead. Look at the code it depends on. Find the repeated rule or the value that several places try to manage.
Fix that part, then see whether the next similar change takes less effort.
What became unexpectedly difficult to change in your React app?
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.

