React Native refactoring: from 800 lines to 100 lines
There comes a time when you open a file and physically feel the weight of technical debt. My main screen was 800 lines. A single file. Business logic, UI rendering, state management, API calls — all mixed into a monolithic component impossible to maintain. And this wasn't an isolated case: I had 16 screens in the same state.
What I'm going to tell you here is how I transformed a chaotic codebase into a clean architecture. Not through a spectacular revolution, but through methodical refactoring, screen by screen, error by error.
Key takeaways:
- A custom hook per screen separates business logic from UI rendering
- The "orchestrator + sub-components" pattern reduces files from 800 to 100 lines
- ESLint with strict rules (zeroany, awaited promises) eliminates silent bugs
- Technical debt is a real cost measurable in time, bugs, and motivation
- Refactoring screen by screen allows continuous delivery without breaking existing functionality
Why is an 800-line component a problem?
The obvious answer: it's unreadable. But the real problem is more insidious. A monolithic component:
- Hides bugs: when everything is in the same file, side effects are unpredictable. Modifying filter logic can break transition animation without you understanding why.
- Slows down development: each modification requires understanding the entire file. You spend 20 minutes "reading" before you can write 5 lines.
- Makes debugging impossible: when a bug occurs, you have 800 lines of suspects. No clear boundary between responsibilities.
- Deteriorates motivation: opening a monstrous file every morning takes its toll. It's the kind of invisible friction that slows down an indie project.
"You're alone, who's going to read this code?" Me. Me in three months when I've forgotten why this screen does what it does. Technical debt is not a metaphor. It's a real cost paid in time, bugs, and motivation.
What decomposition strategy should be adopted?
I refactored all 16 screens following a consistent structure. The pattern is simple but powerful:
1. A custom hook for business logic
Each screen has its hook: useTaskDetail, useFeedScreen, useGroupScreen... The hook encapsulates all the logic: data loading, state management, callbacks. The component only renders JSX.
Before:
// TaskDetailScreen.tsx - 800 lines
const TaskDetailScreen = () => {
const [task, setTask] = useState(null);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
// ... 50 other useState
// ... 200 lines of useEffect
// ... 300 lines of handlers
// ... 250 lines of JSX
}
After:
// TaskDetailScreen.tsx - 100 lines
const TaskDetailScreen = () => {
const { task, loading, editing, handlers } = useTaskDetail();
return (
<TaskDetailHeader task={task} onEdit={handlers.edit} />
<TaskDetailBody task={task} />
<TaskDetailActions handlers={handlers} />
);
}
The useTaskDetail hook is 200 lines. The sub-components are 50 to 100 lines each. In total, that's more lines than before — but each file is understandable in isolation.
2. Sub-components for rendering
Each visual section of the screen becomes a dedicated component: TaskDetailHeader, TaskDetailBody, TaskDetailActions. Each receives its props and only knows its responsibility.
This pattern is directly inspired by the official Thinking in React. It's not new, but it's incredible how much we forget it when we're in a hurry to deliver.
3. A main component for orchestration
The main component becomes a 100-line orchestrator: it calls the hook, distributes props to sub-components, and manages the global layout. Nothing else.
How to go from 71 to 0 ESLint errors?
In parallel with screen refactoring, I ran ESLint on the backend with strict rules. Result: 71 errors.
The temptation: disable the rules that generate the most errors. That's what 90% of developers do. I did the opposite: I fixed each error individually.
The most impactful corrections
Elimination of any: every any in TypeScript is a ticking time bomb. It compiles, it passes tests, but it crashes in production when an unexpected value arrives. I replaced every any with an explicit type. Some cases required creating dedicated interfaces, but the result is self-documenting code.
Properly awaited promises: asynchronous calls without await that failed silently. No error, no warning — just incorrect behavior in production. ESLint detected 12 cases of unawaited promises. 12 potential bugs eliminated in an hour.
Unused variables: forgotten imports, variables declared but never used. Not bugs, but noise that makes the code harder to read. Every unnecessary line is a distraction for the "future you" trying to understand the code.
Should you refactor everything at once or gradually?
Gradually. Always gradually. A "big bang refactoring" where you rewrite everything in a week is a recipe for disaster for a solo developer. You break everything, you can no longer deliver, and if a critical bug appears in production, you can't fix it quickly.
My method:
- Choose the most painful screen (the one you dread opening)
- Extract the custom hook first (the logic, not the rendering)
- Test that everything still works exactly as before
- Extract sub-components one by one
- Deliver the refactored screen, move to the next
Each refactored screen took between 2 hours and half a day. The most complex ones (the Feed, the group screen) took a full day. But at each step, the app remained functional and deliverable.
This workflow is similar to what Martin Fowler describes in his book on refactoring: small, incremental, and always reversible transformations.
What concrete gains after refactoring?
After two weeks of work:
- 16 screens refactored: each follows the same pattern (hook + sub-components + orchestrator)
- 0 ESLint errors on the backend (compared to 71 before)
- Understanding time divided by 3: opening a screen and understanding what it does takes 2 minutes instead of 10
- Reduced bugs: the following 3 weeks were the most stable of the project
- Renewed motivation: opening a clean file in the morning changes everything
The least measurable but most important gain: confidence. After refactoring, I was no longer afraid to modify a screen. I knew exactly where everything was, what the dependencies were, and what a change would impact.
How does technical debt affect a solo project?
Technical debt in a solo project has specific effects not found in a team:
No safety net. In a team, a colleague can review your code, detect a regression, or fix a bug when you're away. Alone, if your code is incomprehensible, it's you — and you alone — who pays the price.
The compound effect. Every shortcut you take today multiplies. An any here, a // TODO: fix later there, and in a few months, you have a codebase where every modification takes 3x longer than it should.
The demotivation spiral. Dirty code → bugs → frustration → quick fixes → even dirtier code. I've seen this spiral on other projects. Refactoring broke it clean.
This is the same logic I apply to database restructuring and security audit: invest time now to gain exponentially more later.
What ESLint rules would you recommend for a React Native project?
Here are the rules that had the most impact on TAMSIV's code quality:
@typescript-eslint/no-explicit-any: forbidsany. Non-negotiable.@typescript-eslint/no-floating-promises: detects unawaited promises. Critical for avoiding silent bugs.@typescript-eslint/no-unused-vars: eliminates dead code noise.react-hooks/exhaustive-deps: checks hook dependencies. Essential in React Native.no-console: forces the use of a logger instead ofconsole.login production.
Activate them all in "error" mode, not "warn". A warning can be ignored. An error must be fixed.
What I learned from this week of refactoring
This week was the best of the project. Not the most exciting — no visible features for the user. But the most impactful for the future. Every feature I built afterward — the gamification system, hierarchical groups, the collaborative agenda — benefited from this clean architecture.
If you're a solo developer and you're postponing refactoring because "it doesn't deliver anything visible," stop. It's the most profitable investment you can make. Not glamorous, not exciting, but fundamentally necessary.
The best week of the project. And I'd repeat it without hesitation.
FAQ
How long does it take to refactor 16 screens?
For me, about two full-time weeks. Each screen takes between 2 hours and a day depending on its complexity. The simplest screen (a basic form): 2 hours. The most complex (the Feed with gamification): a full day.
Does refactoring introduce new bugs?
That's the main risk. To minimize it, I refactored one screen at a time and manually tested each flow before moving on to the next. No major regressions appeared during the two weeks of refactoring.
Should you write tests before refactoring?
Ideally, yes. In reality, on a solo project with 16 screens to refactor, writing exhaustive tests before refactoring would double the time. I opted for rigorous manual tests and automated tests after refactoring, on the clean architecture.
Does the hook + sub-components pattern work for all screens?
Yes, with variations. Some simple screens don't need sub-components — the hook alone is enough. Other complex screens have multiple levels of sub-components. The principle remains the same: separate logic and rendering.
Does ESLint slow down daily development?
The first few hours, yes — the time it takes to fix existing errors. After that, it's the opposite: ESLint accelerates development by detecting errors even before launching the app. It's like a co-pilot correcting your trajectory in real-time.