Blog #36: Touching Legacy Code – When a refactor effort becomes a disaster
The story of an 'itchy hand' wanting to clean up old code, ending in an all-nighter to rollback the system.
1. The Context
It was a peaceful Tuesday afternoon when I received a small ticket: "Change the color and display an additional data field on the order management page." This project was a massive 5-year-old monolith, running on React Class Components mixed with a mess of Redux Saga and libraries that had long since "retired."
When I opened AdminOrderList.tsx, I saw 1,500 lines of code. Endless event handlers, state variables named data1, data2. I muttered: "Good heavens, who wrote this mess?". My Clean Code sensibilities were triggered. I decided not just to fix the bug, but to refactor it into Functional Components and use Hooks to make it "modern."
I was confident. I thought I understood the data flow. But when I hit deploy to the Staging environment, the entire management page turned white. undefined errors were dancing everywhere. Pressure mounted as my boss asked: "Why did a simple color change crash the admin page?".
2. Foundational Knowledge
In React, converting from Class Components to Functional Components isn't just about changing this.state to useState. The biggest difference lies in Lifecycle and Closure.
In a Class, you have componentDidUpdate, where you can compare prevProps and this.props. In Hooks, you use useEffect. If you don't understand how useEffect captures variable values, you can easily create infinite loops or use stale data. Legacy code is often full of underlying "side effects" that only the old lifecycles could keep in check.
3. The Specific Problem
The system managed thousands of orders daily. A massive file with dozens of overlapping dependencies.
The issue appeared when I tried to replace the deprecated componentWillReceiveProps.
// Old code (Class Component)
componentWillReceiveProps(nextProps) {
if (nextProps.orderId !== this.props.orderId) {
this.fetchOrderDetail(nextProps.orderId);
}
}
// My refactored code (Functional Component)
useEffect(() => {
fetchOrderDetail(orderId);
}, [orderId]);
Looks correct, right? But the "trap" was that the old fetchOrderDetail function internally called setState for an array of other dependencies, which indirectly triggered an orderId change through some weird Redux Saga middleware. The result was an infinite loop that froze the user's browser.
4. How I Handled It
I started by panicking. I tried adding if checks to block logic, but the more I added, the messier the code became. I had assumed the old logic was wrong just because it was "ugly." That was my biggest mistake. Code might be ugly, but it's running, which means it has survived thousands of real-world test cases I didn't even know about.
I used Chrome Profiler to see why the component was re-rendering 100 times per second. I discovered a change in a filterStatus variable deep in the store that I had never noticed.
Finally, after 4 hours of debugging, I had to accept a painful reality: Step back. I aborted the entire Functional Component refactor. I went back to the old Class Component file, swallowed my pride, and added exactly 2 lines of code to fix the color and add the data field as originally requested.
5. Trade-offs
The "patching" solution on the old code stabilized the system immediately. But it left a major drawback: the Technical Debt was still there. That 1,500-line file remained as hideous as ever, and the next person would still have to endure it.
If I were starting over, I wouldn't choose to tear down and rebuild a large component. I would break it into smaller parts, wrap the old parts in Error Boundaries, and perform an Incremental Refactoring instead of trying to be a "cleanup hero" in a single afternoon.
6. Lesson Learned
I now understand more clearly: Never refactor dirty code unless you have Unit Test coverage for it.
- Advice: When touching Legacy Code, treat yourself as a surgeon. Cut exactly where you need to cut; don't try to replace the patient's entire brain unless you're sure you can reconnect the nerves.
- When to apply: Refactor when you have enough time to write tests and understand 100% of the side effects.
- When not to: When the deadline is near, when there are no tests, or when your only reason is "this code is just so ugly."
Sometimes, writing a "slightly ugly" but safe piece of code is the mark of a Senior who knows how to quantify risk, rather than a Junior who always wants everything to be textbook-perfect.
Notes on humility before the code that came before.
Series • Part 36 of 50