Lessons Learned From Rewriting A React App

Author: Ethan Glover
Last Updated: Nov 14, 2021, 10:59 AM UTC
React

Video meetings, chat, phone calls, voicemail, calendar, and filesharing. This is roughly the app I spent the last couple of months rewriting from the ground up. The original project started before me with Twilio's example React app and it seems every change from there was just a litany of simple logic piled on top to force things to work. The architecture didn't match what we were trying to do. The Material UI implementation was putting custom layouts into a stranglehold. And don't get me started on conflicting CORS rules, and routing between an express node server and react-router.

I had the chance to take what was there and pull it into a new project. I removed Material UI completely (vanilla CSS is more scalable) and brought in Next.js as a framework.

I thought I would do my best to share some lessons learned during this process, what I did to improve things and what I would do differently in the future.

React is Not a Framework

React is a UI library. Not a framework. It's very good at what it does. Hooks are brilliant. But I don't believe it's realistic to build the frontend for a large complex app with only React. Especially when you're trying to do a lot of live or dynamic data with many integrations. (We'll get back to handling this stuff later.)

That's where Next.js comes in.

Next.js is a React framework. Complete with its own server to build an API and handle server-side rendering. As well as a brilliant Image component (that seems to be using web assembly these days), directory-based routing, data fetching tools and more. I would even argue that Next is a full-stack framework, albeit a small one.

If you're working with a well-written API that follows proper REST conventions, or better yet a GraphQL API and don't have a lot of client-side rendering, you can survive with pure React. But honestly, why bother with react-router and the headache of image tags? If you're starting a new React project I recommend always going Next.js.

Use Eslint

I created my own lint config for Next and TypeScript. It basically just extends... well... a lot of stuff. Critics may note that there's a lot of stuff overwriting a lot of stuff. And that is a valid criticism. But to me, the more hardcore the eslint config, the better. And with some tweaking to order and a few custom rules, this is working out for me.

Eslint can catch a lot of issues such as missing useEffect dependencies (note the useEffect article I link below), accessibility issues, unused imports, inefficient algorithms, etc. Hell, you can even use it to sort imports and alphabetically order object properties.

At a minimum, I would highly recommend the next configs, prettier, jsx-a11y, react-hooks, and typescript-eslint. But don't be afraid to get deeper and continuously tweak. Even create your own config that you can share between projects.

Use TypeScript

I can't tell you how many times TypeScript has saved my ass or made life easier. I would hate to write JavaScript without it. If you haven't gotten into TS yet, or you're still thinking, “It's extra typing for no benefit.” Go learn TypeScript right now. I recommend Stephen Grider's course.

With TypeScript, I created interfaces for any data calls to a REST API I made, got more comfortable with generics to make code more reusable and easier to read, created a pretty cool form generator using OOP style classes, and best of all, my Intellisense is totally superpowered.

Don't Abstract Fetch

I'm working with a lot of APIs. Twilio, Rocket Chat, FileCloud, Outlook, and of course our own. Using REST, WebSockets, and WebRTC. I'm doing server-side calls, client-side calls, polling, cache invalidation, and listening to websockets. Fetching has become my biggest pain point on this project.

For get requests I use useSWR and for posts I wrote a custom usePost hook (as useSWR isn't really intended for pushing data).

It's tempting to create a file that exports a bunch of fetch requests to any endpoint so you can use them anywhere in the application. But this doesn't sit right with me. Do you export promises? Or do you create hooks with state and useEffect? How do you call those hooks conditionally? How do you handle promises in non-async functions? What happens when you got a lot of requests for a complex page with many integrations?

useSwr is good at handling conditional calls. If the URL is null, it won't do anything. But if that changes, it will. A little something like the below will make sure the call is not made until userId is available. (For example, if you're pulling it from state.) You can expand this logic however you like.

const {data} = useSwr(userId ? `domain.com/${userId}` : null)

My usePost hook works the same way, except instead of running as soon as url is available, it exports an execute function.

So what's the problem with this? Chaining requests is the problem. Let's say in order to create a group file share you have to login to the api, grab cookies off the response, use those to create a directory, add a “share” to it, take the shareId to update its name, and mark it as private, add each user to the directory one at a time, then set each user's permissions in that directory one at a time. That's a minimum of 6 REST API calls that have to be done in order. What do you do? Link a bunch of useEffects together?

You might say, that's a job for the backend. Great, with Next.js we have our own Node API. It's perfect for this kind of thing. (I'll get back to some other use cases for the Next API later). However, things didn't always work out like this for me. There were a lot of cases where I needed live data flow to change the client-side. It's just not always optimal to do everything server-side. (We'll get back to this too.) And I did, admittedly, do some pretty nasty useEffect chaining. (By this I mean one useEffect has the returned data of another useEffect as a dependency.)

Here's what I found that works better:

TypeScript gives us the opportunity to build some good ol' OOP classes. An object with methods, and an instantiation that can keep track of any properties. ...It just feels structured. Now I know, hooks are brilliant. I love stumbling across new hook repos just to see what people have done with them. But they have rules. Rules you should follow. And if those rules conflict with what you're trying to do. Find another way.

A TypeScript class is code that can be shared by both the Next API and your client. So wherever it is most appropriate to make these calls, you can do it with the same code. This is pretty freaking cool if you ask me.

const rocketChat = new RocketChatController();
await rocketChat.login();

Don't SSR Everything

Just because you're using Next, doesn't mean everything should be SSR.

Next does a lot of cool optimization without any input from you. CSS for example is rendered server-side. (I also highly recommend using CSS modules that React supports out of the box.) SSR isn't the only reason to use Next and it's not the only tool it has. Use the tools you need, when you need them.

Get familiar with all of the tools Next provides for data fetching. Familiarize yourself with useSWR (previously mentioned) and don't be afraid to do a good ol' client-side fetch. Especially when dealing with live or dynamic data. You should know these tools and when to use them. Putting too much work on the server is just as bad as putting too much work on the browser. Split it up, load only what you need, when you need it.

Learn From Smart People, Don't Stop Learning

I initially learned React from Wes Bos. and did a lot of Andrei Neagoie and Stephen Grider courses. Since then I've come across guys like Dan Abramov and Harry Wolf. I've even linked some of their articles in my documentation of this project. Specifically:

Don't ever think you know everything, you never will. I've come across know-it-alls, worked with know-it-alls, and seen projects decimated by know-it-alls. Stay open, keep finding new perspectives, keep getting deeper, and never let Einstellung set in.

Use The Next API

Even if you're not intending to use Next for your entire stack, the Next API is a quick and easy way to solve some problems. Fixing bad REST APIs is one of them. Got an endpoint that returns too much data and doesn't have an option to paginate? Send it through Next, paginate the results and give yourself a structure that works well with useSWR.

Need to make... ahem.... 6 REST calls to do one thing? Bundle it into a single call you can make on your front end.

Want to work with environment variables that you don't want the client to have access to? API routes can access environment variables that are not NEXT_PUBLIC.

Keep Scaling, Keep Refactoring

Don't get stuck with mistakes. I moved this project from taking two weeks to add a feature, to a few hours. (So long as an API doesn't give me too much trouble.) I've got a lot of places in my code that I need to clean up. Fetch requests being one of them. I only thought of using classes recently.

We're working on a GraphQL server that we're adding both our database and our third-party APIs to. We're discovering that we can create whatever crazy relationship we want to. A user's email on our database can fit nicely into a user search for RocketChat. Add it to the graph. Those chaining rest calls could be a series of relationships. Add it to the graph. Because our users have the same credentials for both the app and the integrations, we can start linking things together. Add it to the graph. For example:

Why not? Having a separate server that allows us to make these kinds of calls to basically abstract out interacting with data sources will be a huge boost.

I've thought about hooking something like Prisma up to connect Next directly to our database. But with a GraphQL server, I don't think that will be necessary or even preferable. But if I have a use for it, I'd like to keep that in my back pocket. I mean, maybe there's a use for an SQLite server working with Prisma? A cool little in-memory app DB. Who knows, I'm keeping an open mind.

If you don't keep improving your code, it's going to fall apart. If you've got a team that's afraid to step on each other's toes, or is paralyzed by a buggy codebase. Pause, reevaluate, and understand that time is an investment. If you want fast development and high productivity you need to invest in the foundation of your work. Don't think you can get there by brute force. And don't think you're better than “new-fangled” technologies. That kind of attitude will keep you and your team back.