Published on

Refactoring: How I Refactor a React Native Component

11 min read
Authors
  • avatar
    Name
    Asfiolitha Wilmarani
    Trakteer
refactoring react native components
Picture of a doggo as thumbnail because y not

It’s midterm week and it’s the first week I am officially homework-free, hurray! Well, except for assignments from side projects and other course projects. Hence why, I’m taking this time to write a bunch of dev logs for Ajaib on DEX while having progress on my Camp Nanowrimo writing goal~

It’s like feeding two birds with one scone!

This time, I’m going to talk about refactoring. It’s a term I’ve encountered a lot, both when working on a new project and a legacy project. I found that it is something we eventually ought to do at some point in the project, because the code we write for the first time is often not the greatest version of it.

Why Refactor

Basically, we want our code to be efficient, readable, and easy to maintain1. Refactoring is one way of achieving this. By making our code readable, it also helps us in the QA and debugging process later down the line.

It’s also important to avoid code rot (the stink beyond code smell 🙃). Code rot may result from multiple developers writing codes in their own styles, without any guidelines whatsoever. It can also happen as a result of duplicate codes, patches, and other rotting spaghetti meatballs.

When to Refactor

From the various articles I’ve read while researching for this topic, the best time to refactor is just before you add a new feature to the codebase. Cleaning up the current code before adding in new ones will not only improve the code quality, but also makes it easier for future devs to build on top of it.

An analogy would be like, when you’re trying to make space for a new bed in your bedroom, it’s a good idea to clean up your room first. Reorganize your endtables, shelves, and vanity, maybe clean up some dust and vacuum the whole room before placing the bed inside. It will be a lot better than shoving a brand new bed in a mess of a room. This way, the process of placing the new bed will go smoother, and it’s easier for you to move around with everything being spotless.

Another good time to consider refactoring is just after the product delivery. It’s a good idea to do a little housekeeping before working on the next backlog/project because the dev most likely have the availability to work on the refactoring.

tdd-cycle

Diagram from bitbar.com

Last but not least, of course we had to go back to our favorite little graph. See there where it says refactor? Indeed. It is also a great idea to refactor just after you create the initial implementation to pass a previously failing test. It is one third of the entire TDD cycle after all.

When to Stop Refactoring

Refactoring should only serve as a clean-up effort. It’s not meant to affect the performance or the design of the application. Here are times you should stop refactoring.

  • When the feature / app needs to be completely revamped – sometimes it takes less effort to redo something from scratch than to salvage an obsolete code that’s to hard to read.
  • When you’re in a very tight and hard deadline (such as a market release date) – refactoring is time consuming and additional coding or testing on an already tight timeline will only lead to frustration and additional cost.
  • When it’s readable enough for your team – it’s easy to forget that every code will someday be an obsolete legacy code anyway, so over-refactoring is a terrible idea–especially if the code is already readable to your entire team.

One example is when I realized that the tech I’m currently using is not really suitable for the feature I’m working on. On the second sprint, I worked on the watchlist feature, and I implemented it with AsyncStorage at first.

asyncfail

But, after some discussions with my team members, we agreed that it’d be better if I used zustand persist instead so that we wouldn’t have to take care of the state management separately. Therefore, I completely redid the watchlist logic and removed the unused code afterwards.

zustand

How I Refactor

There are quite a lot of methods to refactoring depending on the code smells found on your code base. Several ways I’ve found during my reading around are as follows.

  • Red-Green-Refactor – the TDD way of refactoring, doing it after passing some test.
  • Refactoring by abstraction – one example is the pull-up/push-down method. The pull-up method pulls code parts into a superclass to eliminate code duplication, while the push-down takes it from a superclass and moves it down into subclasses1.
  • Composing method – involves streamlining the code in order to reduce duplications. It can be done through extraction and inline refactoring.
  • Simplifying method – involves tweaking the interaction between classes by adding, removing, and introducing new parameters along with replacing parameters with explicit methods.
  • Preparatory refactoring – a part of software update instead of a separate refactoring process. Done when a developer notices the need of refactoring while adding a new feature.

Because I’m working on a react native app that doesn’t really deal with OOP, I used the composing method for my refactoring. Here’s my process of refactoring in Ajaib on DEX.

Identify

First thing I do is I identify the component that needs refactoring. Usually, I work on a component in a single .tsx file first until it’s somewhat finished and functional. This results in a big file, likely with multiple components defined in it.

I also look for repeating components that could possibly be replaced with a .map() statement. It’s a good practice because it makes the code way more readable and also makes it easier to edit the props/data that goes into the component.

For this article, I’ll use the Bottom Tab Bar component again as an example.

BottomTabNavigator.tsx
function BottomTabNavigator() {
  return (
    <Tab.Navigator
      tabBar={(props) => <BottomTabBar {...props} />}
      {...}
    >
      <Tab.Screen
        name="Home"
        component={HomeScreen}
        options={{
          tabBarTestID: "home-tab",
          tabBarIcon: ({ color }) => <Icon />,
        }}
      />
      <Tab.Screen {...} />
      <Tab.Screen {...} />
      <Tab.Screen {...} />
      <Tab.Screen {...} />
    </Tab.Navigator>
  );
}

Extract

From this file, I know that I want to extract the screen component. In the snippet above, we see that there are five screens defined explicitly with the same component copied and pasted five times just with different props. We need to change this, because it makes the file longer than it needs, and it makes it slightly harder to change the props.

Here’s how I extracted the info into an array of object, put it into another file, and rewrote how the screens are rendered.

BottomTabsNavigation.tsx
import { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs/lib/typescript/src/types";

export type BottomTab = {
  name: string;
  component: ComponentType<any>;
  options: BottomTabNavigationOptions;
};

export const tabs: BottomTab[] = [
  {
    name: "Beranda",
    component: HomeScreen,
    options: {
      tabBarTestID: "home-tab",
      tabBarIcon: ({ color }) => <Icon />,
    },
  },
  ...
];
BottomTabNavigator.tsx
function BottomTabNavigator() {
  return (
    <Tab.Navigator
      {...}
      tabBar={(props) => <BottomTabBar {...props} />}
    >
      {tabs.map((tab: BottomTab) => (
        <Tab.Screen
          key={tab.name}
          name={tab.name}
          component={tab.component}
          options={tab.options}
        />
      ))}
    </Tab.Navigator>
  );
}

I took advantage of Array.map() to render the screens according to the props I defined in another file. It’s much easier to edit the props now that it’s in a single file. The team members that are in charge of the respective screen can update this file

Run Tests

After all the new files are made, I reloaded the application to make sure everything still renders as intended. When I’m sure everything looks and works fine, I move along to rerun my previously made test.

app screenshot
It still renders perfectly fine~

Rerunning tests again and again is part of quality assurance. Doing this also makes sure the refactoring I’m doing is not breaking any functionalities that shouldn’t be broken. It may also tell me if a bug has appeared because of the refactoring that I’m doing.

test passed
These are the tests we’ve written from before 😉

When I confirmed that my tests are still passing, I can be sure that the refactoring is a success.

Bugfix

Sometimes, everything doesn’t go as smoothly. I often find bugs when trying to refactor. It usually involves minor UX details or unexpected behavior from before I refactor it. Although I haven’t encounteres this problem yet while using TDD, this happens to me a lot on other projects.

The tests can help me navigate around to find the bug. Using the debugger from vscode also helps a lot, because I can set breakpoints on my code to see which variable holds which value at a certain time. Also, error stacktrace helps a lot.

When I’m sure that the bug is fixed, I gather the courage to push my changes and submit a merge request for my team members to review. Then, I would hope and pray that nobody’s gonna find a new bug I have to fix (cuz bugfixing after you thought the backlog is over is so frustrating).

Other React Smells 🦨

In the current development process, I didn’t find any of these smells. But, they are often encountered in many React-based projects. Some of the code smells to watch out for are as follows2.

  • Too many props – a lot of props hint that a component may be doing more things than it should. To combat this, we can use composition or passing down a configuration props instead.
  • Incompatible props – props that doesn’t make sense together hint that the component needs some breaking down.
  • Copying props into state – doing this completely ignores all updated values of the props, hence it may make the component more bug-prone.
  • Multiple booleans for state – sometimes it’s wiser to use enums than excessive booleans to represent a component’s state.
  • Too many useStates in a component – components with many useState is likely doing too many things. To combat this, we can use the useReducer hook instead.
  • Large useEffect – these usually means that it’s doing multiple things. Reduce the complexity of components by breaking down useEffect hooks and lower the risk of creating bugs.

TL;DR

  • Refactoring is a very important step in development (and it’s one third of the TDD cycle).
  • Tho, there is a limit to it–meaning at some point you should not refactor anymore.
  • Refactoring can be done in various methods, including but not limited to: refactoring by abstraction, composition method, simplifying method, and preparatory refactoring.
  • In our project, Ajaib on DEX, I usually use the composition method as it is the commonly used one for react native.

That’s it for this dev log! See you on the next one~

Footnotes

  1. https://www.altexsoft.com/blog/engineering/code-refactoring-best-practices-when-and-when-not-to-do-it/ 2

  2. https://antongunnarsson.com/react-component-code-smells/