6 React Mistakes I Wish I Hadn't Made

in react

About 4 months into developing my second large React app, I'm already starting to see some of the anti patterns I shouldn't have used in development (and will now have a hard time fixing).

With the hope that it'll help you avoid those same mistakes, here they are with the means I should have taken to avoid.

Code Duplication

Code duplication certainly isn't unique to React, and using React should have helped avoid some duplication (And it did. Occasionally).

But splitting a real world design into separate entities can be a difficult task. In my application I had 2 groups of pages, where the pages in each group had the same layout. Using 2 different layout components was the easy way. Unfortunately along the way the design had changed and some common features began to appear in all the pages. At that point, implementing the common features in both layouts was easier than uniting them.

Lesson Learned: As soon as you feel like you're writing the same code in two places, that's probably the best time you'll get to create an abstraction.

Wrong Abstractions

Unfortunately, one can also err on the side of too many abstractions, and that error is just as bad.

Assume all the pages in your application need a side menu, but each group of pages uses a slightly different design for the side menu. To make it simple, let's assume each has a different color. Easy right? Just create a single React component and pass the background color as property. Even better, pass class name as property and let CSS deal with the difference in style:

class Sidebar extends React.Component {
    render() {
        return (
        <div className={`sidebar ${this.props.className}`}>
            ...
        </div>
        );
    }
}

Awesome. But as time passes and design changes the difference between the two types of sidebars grows. One should display some items that the other doesn't, or one includes a small image gallery that is not available in the other one.

Additionally some items can depend on the specific layout, for example a login box should be displayed for guest users in one layout, but a mini signup form in the other layout.

Adding more properties will not solve your problems but will only make them worse. As a component feels like it's branching too much, the right thing is to split it to multiple components. Of course it's not always clear how to do that, and you risk duplicating some code. I think that's the problem with real world code.

Vanilla Redux

The Redux readme loves to show examples that use switch case blocks:

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    return state
  }
}

That's cool and simple and easy to understand. Unfortunately it doesn't scale. 4 months into development my reducers are too long and string ids are everywhere. Action creators are coupled with reducers and adding each small new action requires modifying at least 3 files (actions, reducer and the relevant component).

Either use a library such as redux-actions or build your own. But don't copy/paste your base from the Redux readme.

Fat Components

As more functionality is required in the application it should be written somewhere. Well, React components turned out to be a very bad place for logic and functionality.

The reason is the more functionality you write in a React component, the harder it gets to break that component into multiple smaller ones. And as we learned from the above mistakes: no matter how smart you think you are, changes in requirements will surprise you. Smart developers are prepared to change their code in response.

Fat Actions (Thunks)

Another bad place to write business logic is inside Redux actions, mostly thunks. Thunks are great for async ajax or other operations, but the more business logic you put in them the harder it is to change or test the code.

I now find it best to write most of my business logic in helpers that return promises, and use async/await syntax in thunks. Here's what an async file upload thunk could look like:

function uploadSelectedFile() {
    return async (dispatch, getstate) => {
        const file = getstate().uploadForm.get('file');
        const url = getstate().urls.get('upload');
        
        const uploader = new FileUploader();
        try {
            dispatch(uploadStart());
            await uploader.upload(file, url, showProgress);
            dispatch(uploadFinished());
        } catch (err) {
            reportError(err);            
        }
    };
}

And all the specific logic of how to upload the file, and when to call showProgress is written in FileUploader class. The important thing here is FileUploader class is not a React or Redux thing, but a normal JavaScript class. This makes it easier to change and unit test it.

Mixing Class and Component Hirearchy

A final React mistake I found myself making is mixing class and component hirearchies. In classic object oriented programming I was used to using inheritance to share functionality between 2 or more classes. In React, creating a new base component and inheriting from it is considered bad practice, and for good reason.

Consider the following example:

class MyBase extends React.Component {
    componentDidMount() {
        // do some important stuff after mount
    }
}

class MyThing extends MyBase {
    componentDidMount() {
        super.componentDidMount();
        // do some more stuff specific to MyThing
    }
}

Here's the problem: Delete componentDidMount from MyBase and MyThing also stops working. One could argue this problem exists in all object oriented designs, and they would have a point. But in React lifecycle methods are special in that they are called automatically by the framework at certain times. That means the problem is harder to find once it happens.

Final Thoughts

React has a way of making complex code feel simple, but that's not always a good thing. I think now after writing some real world code in React I'd certainly spend more thought on the architecture and how to break the app into components in my next applications.

Making right decisions there could save you hours of debugging and dealing with crappy code down the road.

Comments