How to Refactor Messy JavaScript Projects
There'll be a time where you're developing a project and all you need is one JavaScript file. This works great, and in fact, it's the way I recommend starting off simple project, however, as time goes on, this singular file is likely to get bloated with classes, functions, variables, and more. As a result it's important to be on the lookout for when things feel unruly, then refactor and organize accordingly.
Below are the methods I use to make a messy project more bearable.
Move classes into separate files
Placing your classes in a singular file will boost your productivity at first, but put too many in there and it becomes hard to differentiate between what's part of your implementation and what's part of your class design:
A simple fix for this is to take each class and place them within their own file:
You can clear hundreds of lines of code and enable file-searching ability for classes within your text editor using this method. Due to it's ease of implementation, it comes in as the number one way you can refactor a messy project, just make sure that you import your new files or else they won't be read by your index.js
file.
Move utility functions to separate file
Utility functions make our lives easier by abstracting algorithms into bite sized functions, but they'll easily muddy up our code if they're all placed in a singular file like index.js
:
Instead, it makes sense to place these in a separate utils.js
to signify, "This is where all of our helper functions go":
Since it's likely your project makes use of a handful of utility functions, this method also ranks near the top for best ways to refactor a messy project—it's super easy to implement as well, so we can immediately make impact to our project's organization.
Move event listeners into separate file
Similar to the issue had with utility functions, event listeners can clutter up an index.js
file as well:
Since events are their own category, seperate from a project's main implementation, it makes sense to place these within their own file:
Another easy change that immediately frees up space and makes things easier to find.
Move game setup functionality into separate file
Next, I'd recommend moving any game setup your have into a separate file. Game setup means anything like map generation, level variables, or global effects. Say all of my my map generation code is in index.js
:
As you can see, this is a beefy chunk of code, and what you don't see is that there are about 100 more lines of map generation code beneath it. Since this relates to initial setup rather than actual game functionality, it makes sense to place this in a separate file:
We can run this code by pulling it in via a script tag into an index.html
file, and as long as this file preceeds index.js
in load order, everything should work as it did before. However, if you don't want to introduce variables like map
into the global namespace, you can transform the generation code into a function:
import Boundary from './classes/Boundary.js'
import { createImage } from './utils.js'
function generateBoundaries() {
const map = [
['1', '-', '-', '-', '-', '-', '-', '-', '-', '-', '2'],
['|', '.', '.', '.', '.', '.', '.', '.', '.', '.', '|'],
['|', '.', 'b', '.', '[', '7', ']', '.', 'b', '.', '|'],
['|', '.', '.', '.', '.', '_', '.', '.', '.', '.', '|'],
['|', '.', '[', ']', '.', '.', '.', '[', ']', '.', '|'],
['|', '.', '.', '.', '.', '^', '.', '.', '.', '.', '|'],
['|', '.', 'b', '.', '[', '+', ']', '.', 'b', '.', '|'],
['|', '.', '.', '.', '.', '_', '.', '.', '.', '.', '|'],
['|', '.', '[', ']', '.', '.', '.', '[', ']', '.', '|'],
['|', '.', '.', '.', '.', '^', '.', '.', '.', '.', '|'],
['|', '.', 'b', '.', '[', '5', ']', '.', 'b', '.', '|'],
['|', '.', '.', '.', '.', '.', '.', '.', '.', 'p', '|'],
['4', '-', '-', '-', '-', '-', '-', '-', '-', '-', '3']
]
const boundaries = []
map.forEach((row, i) => {
row.forEach((symbol, j) => {
switch (symbol) {
case '-':
boundaries.push(
new Boundary({
position: {
x: Boundary.width * j,
y: Boundary.height * i
},
image: createImage('./img/pipeHorizontal.png')
})
)
break
}
})
})
return boundaries
}
This example requires a web server that can handle importing modules, yet it provides a namespace safe way to generate a number of Boundary
objects that you can utilize on a level-by-level basis. For more information on this sort of usage, check out my Pacman game dev course.
Abstract beefy groupings of code
Like how we just handled map generation in the last section, another great way to refactor messy codebases is to abstract beefy groupings of code. If you see around 5-30 lines of code that you just don't understand at first glance, it might be time to refactor that grouping into something more readable. The key here is to start small, abstracting large statements almost always causes more trouble that abstracting small pieces, so find a grouping of ~5 lines, see if it makes sense to refactor it, if not, increase your grouping amount.
Take this player movement code for example:
if (keys.w.pressed && lastKey === 'w') {
for (let i = 0; i < boundaries.length; i++) {
const boundary = boundaries[i]
if (
circleCollidesWithRectangle({
circle: {
...player,
velocity: {
x: 0,
y: -5
}
},
rectangle: boundary
})
) {
player.velocity.y = 0
break
} else {
player.velocity.y = -5
}
}
}
This states that whenever a player presses w
, move the player upwards. However, that's not very apparent based on the code we're looking at—it takes at least 10 seconds to a minute to understand what's really happening. As a result, the time it takes to comprehend makes this grouping a good candidate for abstracting.
We know the code makes our player go up, so when we press w
, what should we really say for more readable code? Here's an idea:
if (keys.w.pressed && lastKey === 'w') {
player.moveUp(boundaries)
}
These three lines of code are way more understandable than what we had earlier, but if we were to call things this way, we'd need to ensure we create a player method resembling the function we just called:
import { circleCollidesWithRectangle } from '../utils.js'
class Player {
moveUp(boundaries) {
for (let i = 0; i < boundaries.length; i++) {
const boundary = boundaries[i]
if (
circleCollidesWithRectangle({
circle: {
...this,
velocity: {
x: 0,
y: -5
}
},
rectangle: boundary
})
) {
this.velocity.y = 0
break
} else {
this.velocity.y = -5
}
}
}
}
Note, I changed any instance of player
to this
since now we're referencing the player's properties directly within the class definition. I also had to import circleCollidesWithRectangle
so we can use it within Player.js
, then passed boundaries
through as an argument so we can use our level's generated boundaries without having to reference them within a global namespace.
In the end, we may have written more code, but it is much more understandable and organized since we can easily locate everything related to moving our player up, compared to having to use some brain power to find it prior.
Conclusion
These are just some of the methods I use to refactor messy codebases, thankfully they're easy to implement and will provide you with valuable code-maintenance skills that'll serve you for years to come in your coding career. To learn more about this topic in depth, as I mentioned, I cover this exact topic in my Pacman course which you can take here.
Otherwise, you can follow me on Twitter for course updates and more. Thanks for reading and I'll see you in the next one ✌️