But it works! How and when to refactor your code

An introductory look at refactoring in a React app

Jess Sommerville
5 min readMay 19, 2023

When you finally get a feature to work, it feels like time to raise your fists into the air then pour a celebratory cup of coffee. Once you or your peers start to build on top of the feature, however, that coffee has long grown cold and explaining your code can take a sour turn.

Maybe your code works, but is it legible to your team? Is it reusable? Is it now making your life harder? Getting the “ick” when reviewing your codebase is common enough that it’s had a nickname since the ‘90s — code smells.

Good code doesn’t require an art history paper

Code smells

Code smells are any characteristics of your code that “stink” of a larger problem.

Think of confusing naming conventions, exceedingly long lines of code, copied and pasted logic, passing or returning unused data. Check out more smells by application, class, or method-level here.

As a new programmer, it may be tempting to ignore that intuition so that you can focus on the next set of features. And yet this is the time to refactor.

Refactoring is when you rewrite your code in a way that improves its logic without changing its functionality.

Any writer will tell you that the only writing is rewriting (thanks, Hemingway), and it’s no different when writing lines of code. Taking the opportunity to ask yourself how should this work, not just how can it work, can lead to the stickiest learnings early in your programming journey.

Not to mention this can help minimize technical debt, or code that works now but that we’ll need to fix later when it’s more difficult to do so.

Managing technical debt be like

What to refactor

When you’re ready to refactor, look for opportunities to:

  • Remove duplicate code
  • Move functionality to the most appropriate place
  • Re-organize your data structure and modifications
  • Abstract your overarching logic and delegate further responsibilities
  • Simplify your conditional expressions

These are high-level categories for when you’re just getting started learning how to refactor; they are far from exhaustive. For a deeper dive, and for potential optimizations for each of these scenarios, check out the resources linked at the bottom of this article.

React app example

When you learn a new language or library, you might still think in an old one when writing your code. Remember to sanity-check if you are working with or against the benefits of your new library.

That was just the case when building a React app with my peers this week (funnily enough, about sharing recipes … talk about smells 😉). We set up our own JSON-server to PATCH and POST recipes via a controlled form.

In our initial builds, each of these requests required a separate form — one to edit and one to add. The returns were nearly identical in terms of their inputs. The main differentiators were:

  • How you got to the form and where you ended up (/cookbook/:id/edit vs /recipes/new )
  • What you saw on the form (preloaded data for that cookbook recipe vs. empty fields to create a new one)
  • What happened on submit (PATCH vs. POST)

While we need to account for those differences, one of the main benefits of React is component reusability. If you give each component a narrowly scoped responsibility, then you can render it wherever that functionality is needed.

Let’s take a look at how that can work inside of a singular Form component. We’ll assume that we are using React Router for this example.

// inside our Form component 

// call the useParams() hook from React Router
// destructure the response to save the id to a variable
const { id } = useParams()

// set a state to keep track of our form data and control its input values
const [formData, setFormData] = useState(initialState)

// destructuring of our state for later use
const { recipeName } = formData

useEffect(() => {

// check if we have an id from the url,
// the data for which we should display as our initial form values

if (id) {
fetch(`endpoint/${id}`)
.then(res => res.json())
.then(setFormData)
}, [id])

We know that the user will land on this form at either /cookbook/:id/edit or /recipes/new . If we have an id parameter, we know we have something to edit. /cookbook/1/edit would suggest we should be editing the object with an id of 1, and const { id } = useParams() would have id equal to 1. If we do not have any id, we know we are looking to create something entirely new.

That can inform our logic for what we display in the form’s inputs. By default, we’ll set our formData state to an initialState , an assumed object with keys matching our database structure and empty values.

If we do have an id to look for, however, we’ll go fetch that object’s data from an interpolated url and setFormData equal to the return. Once that state is reset, the form input values will update accordingly. We’ll refetch whenever the id changes.

We still want two different actions on submit. Let’s create a simple ternary expression to account for each scenario.

const handleSubmit = (e) => {
e.preventDefault()

// if we have an id, modify with a PATCH request in our helper function
// if not, create a new data entry

id ? patchData() : postData()

// reset the form values
setFormData(initialState)
}

If we have an id , if we are editing, invoke our helper function with our PATCH request logic ready to go. If not, POST a new recipe instead. This keeps our submit function lightweight even though it’s accounting for “two” forms — it prevents the page from refreshing, invokes a function to process the new user inputs, then clears the form.

// our JSX return in our Form component

return (
// trigger one submit event regardless if we are editing or adding
<form onSubmit={handleSubmit}>

// control our input values with our formData state
// which is either the initial empty value or what we fetched for this id

// assume a handleChange function exists to update state as user types

<input name='recipeName' onChange={handleChange} value={recipeName} />
<input type='submit' />
</form>
)

We’ll keep it simple here with one input for the recipeName that we destructured above. We could have as many as we have keys on our recipe object and in our formData state. Now that we are working with React instead of against it, we can more easily adapt as our data or UI changes.

Some other reasons to refactor in your React app may include:

  • Sending eerily similar data to different components (Are you sure you can’t render the same component with different props, even if the data is from different sources?)
  • Prop drilling, or passing props to components for the sole purpose of getting them further down the prop chain
  • Setting states for information you have access to elsewhere

Additional resources

  • Refactoring Guru: Your one-stop shop for dirty vs. clean code, how to tell the difference, and what to do about it
  • Refactoring: This is The Book™️

I hope this introduction helps to make refactoring a little less intimidating. Now get cookin’!

--

--