As a JavaScript developer, I've come across several recurring errors—both from my own experience, from reviewing code and mentoring others. Some of these mistakes are common to all levels, from beginners to seasoned developers, and understanding them can greatly improve the reliability and efficiency of our code. Through this curated list, I hope to share practical insights that help developers avoid common pitfalls in React, Next.js, and modern JavaScript, ultimately saving time and making code cleaner and more maintainable.
Generally, we can classify these errors into three broad categories - syntax, logical, and runtime errors.
null
or undefined
value. If unhandled, they can cause the application to crash, making error handling and thorough testing very important.
Let’s check out some of these examples:
It’s essential to understand how references work when working with Javascript objects as it impacts how data is modified accross different part of your code**.** Let’s look at an example:
const original = { name: 'James', bio: { age: 25 } };
const duplicate = original;
duplicate.name = 'Timilehin';
console.log(original.name); // Output: 'Timilehin'
From this, duplicate
is supposed to be a new object that can be modified independently of original
. However, understanding that duplicate
is assigned by reference rather than by value, any change to duplicate
directly affects original
as well because both original
and duplicate
point to the same memory location for the object, so changing duplicate.name
actually changes the name
property in the original object.
To avoid unintentional modifications like this, you need to create a copy of the object. This is where the concepts of shallow and deep copy come in.
A shallow copy duplicates the object at the top level only, leaving references to nested objects intact. Here’s how it works:
const shallowCopy = { ...original };
shallowCopy.name = 'Timilehin';
shallowCopy.details.age = 30;
console.log(original.name); // Output: 'Timilehin' - Top-level change doesn't affect original
console.log(original.details.age); // Output: 30 - Nested object is still affected
Using a shallow copy, you can independently modify top-level properties (like name
) without impacting the original object. However, if modifying a nested property (like details.age
), the original is still affected because nested objects are only copied by reference, not duplicated.
A deep copy duplicates everything in the object, including nested structures, so the original remains entirely unaffected by changes to the duplicate.
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.details.age = 30;
console.log(original.details.age); // Output: 25 - No change to the original object
deepCopy
is an entirely independent object in this case, so modifying it has no impact on original
. Understanding these differences helps avoid unintended side effects, especially when working with complex state or deeply nested data structures.
Using arr.length && something is a popular shorthand in JavaScript for checking if an array has elements before doing something with it. However, this shorthand behaves unexpectedly with empty arrays.
Here’s why: the &&
operator in JavaScript is a "short-circuit" operator, meaning it evaluates the left side (in this case, arr.length
) and only moves to the right side if the left side is truthy. When arr.length is 0 (as it is with an empty array), the entire expression immediately stops evaluating and returns 0 because 0 is falsy.
const testArr = []
const TestComponent = () => <p>This is a component to be rendered</p>
const MainComponent = () => {
return (
<div>
<p>This is the main component</p>
{testArr?.length && <TestComponent />} {/* 0 is displayed on the browser */}
</div>
);
};
//Alternatively, use the ternary operator instead
{testArr?.length ? <TestComponent /> : null}
it’s important to use the correct prefix for environment variables so they’re accessible in the browser. React requires environment variables to be prefixed with REACT_APP_
, while Next.js uses NEXT_PUBLIC_
. Missing or misnaming these prefixes is a common mistake that causes variables to be undefined in the browser.
I've often seen this oversight happen in code reviews, where forgetting the prefix results in values that don’t get passed correctly to the frontend. To avoid this, always double-check variable names and use the correct prefix for each framework. It’s a small step but can prevent some big headaches later on!
Next.js is default is to render components on the server. the "use client"
directive allows a component to run on the client side. If overused, or added unneccesarily, it can cause performance issues and affect efficiency of server-side rendering.
Refer to linting and code warnings as “early warning systems”, helping to catch potential issues before they lead to real problems. They identify mistakes, and inconsistencies, ensuring best practices and cleaner code overall. Ignoring or silencing these warnings might seem like a quick fix, but it’s a risky habit that can lead to hidden bugs, performance issues, and harder-to-maintain code.
Instead of silencing;
In React and Next Js, you need to use className
instead of class
when applying CSS classes to elements because class
is a reserved word in JavaScript. React uses className
as the JSX attribute to assign CSS classes, so if you accidentally use class
, it won’t work as expected and may cause errors or warnings in your code
// Correct usage
<div className="container">
Hello World
</div>
// Incorrect usage
<div class="container">
Hello World
</div>
React uses keys to keep track of elements in lists, making the reconciliation process smoother. If keys are missing or not unique, React might re-render list items incorrectly, leading to unexpected behavior. For example, without unique keys, if an item in the middle of a list is updated, React may accidentally update the wrong item or cause the entire list to re-render.
const names = ['James', 'Kola', 'Barry'];
const ListComponent = () => (
<ul>
{names.map((name, index) => (
<li key={index}>{name}</li> // Use a unique identifier here
))}
</ul>
);
Avoid using indices as keys if the list can change order, as this can lead to unexpected behavior.
Use a unique identifier, like an id
, as the key when possible.
Always provide keys when rendering lists, even if it’s a static list.
I've often seen code filled with long if-else
chains, overly complex ternary expressions, or switch
statements that could be simplified. These choices can easily make code messy and hard to read if we don’t know when each option is best. Choosing between if-else
, switch
, and ternary operators can simplify the logic and improve readability when used appropriately.
If-Else Statements: Best for straightforward, step-by-step checks. If your logic has only a few conditions or requires a nested approach, if-else
is likely the best option. If your code starts getting filled with multiple else if
blocks, consider using switch
to refactor. They’re also the easiest to read in cases where you have one main condition and a few alternatives.
const calculateDiscount = (userType) => {
if (userType === 'student') {
return 0.2; // 20% discount
} else if (userType === 'teacher') {
return 0.1; // 10% discount
} else {
return 0; // No discount
}
}
In this case, if-else
is simple and readable. If your conditions become more complex or there are only a few unique conditions, this structure can still work effectively.
Switch Statements: switch
statements provide clear way to handle multiple possible values for a single variable, especially if those values are fixed (like enums or constants). Switches are clearer and more readable than lengthy if-else chains. Here’s an example:
// Define an enum for user types
enum UserType {
Student = 'student',
Teacher = 'teacher',
Senior = 'senior',
}
// Use the enum in the function
const calculateDiscount = (userType) => {
switch (userType) {
case UserType.Student:
return 0.2; // 20% discount
case UserType.Teacher:
return 0.1; // 10% discount
case UserType.Senior:
return 0.15; // 15% discount
default:
return 0; // No discount
}
}
// Example usage:
const discount = getDiscount(UserType.Student); // 0.2
console.log(discount);
Ternary Operators: Ternary operators are best fit for simple, inline conditions and help keep code compact. They work best for straightforward expressions. When ternaries become nested or chained, they quickly become difficult to read and at that point, using a simple if-else
statement is usually clearer and more maintainable.
const discount = userType === 'student' ? 0.2 : 0;
From my experience, a lot of developers often overlook accessibility, unintentionally leaving out users who rely on inclusive design. Adhering to accessibility guidelines is essential, as it ensures that our applications are usable by everyone. Developing with inclusivity in mind isn’t just a best practice, it’s a commitment to providing good experience for all users. Here is a good read on improving accessibility.
In React, managing state efficiently and understanding hooks can be key to creating a stable and performant app. Here’s a breakdown of important concepts and best practices that prevent common pitfall:
setState
Rather than mutating the state which can lead to unexpected bugs and make the app challenging to debug, use functions that update the state based on the previous value using React’s useState
hook.
import React, { useState } from 'react';
const Counter = () => {
const [counter, setCounter] = useState(0);
// Incorrect: directly modifying state
// counter = counter + 1;
// Correct: using a function to update state
const incrementCounter = () => {
setCounter(prevCounter => prevCounter + 1);
};
return (
<div>
<p>Counter: {counter}</p>
<button onClick={incrementCounter}>Increment</button>
</div>
);
};
export default Counter;
In this example:
setCounter
to update the state instead of modifying counter
directly.setCounter
function receives prevCounter
as an argument, ensuring the update is based on the latest state value. This pattern helps prevent bugs, especially in cases where state updates might happen asynchronously.When using useEffect
, always include all required dependencies in its dependency array. This ensures that useEffect
only re-runs when necessary, such as when a dependency changes, preventing bugs or infinite loops. Omitting dependencies can cause effects to run unexpectedly or miss updates.
import React, { useState, useEffect } from 'react';
const ExampleComponent = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`Count is: ${count}`);
// Assuming this effect relies on "count" to log correctly.
}, [count]); // "count" is included here as a dependency.
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
In this example:
count
in the dependency array, React will re-run the effect whenever count
changes, ensuring the effect is always in sync with the latest value.count
would prevent useEffect
from re-running when count
changes, causing the logged value to be incorrect.
useState
vs. useContext
useState
is ideal for managing local state data or a state that only a specific component or a small group of closely related components needs. For example, if a component displays a counter and only that component and its immediate children need access to the counter value, useState
is a great choice.
It keeps the state private to the component, which improves performance and keeps your code clean.
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
In my early days, I didn’t fully understand how powerful useContext
could be for managing shared data across multiple components, especially in deeply nested trees. I would often pass props manually down through each component, a process known as "prop drilling."
Using useContext
removes that complexity by creating a context that acts as a global provider, making the data accessible anywhere within the provider’s component tree. This way, any component in the tree can access or update the shared data without needing to pass it down through props at every level.
In React, useMemo
and useCallback
can help optimize performance by caching values and functions that don’t need to be recalculated on every render. Here’s how they work when to use them:
useMemo
is helpful when you have a complex computation, like filtering data. It only recomputes the value if its dependencies change, which can save rerenders processing time.
const expensiveCalculation = (input) => {
console.log('Calculating...');
return input * 2; // Imagine this is heavy and time consuming calculation
};
// useMemo caches the result of `expensiveCalculation`
const result = useMemo(() => expensiveCalculation(input), [input]);
In this example, expensiveCalculation
only runs when input
changes. On rerenders with the same input
, React skips recalculating and just uses the cached value. This is especially useful in components where you might be passing down data to multiple children or rerendering often.
useCallback
is similar but works for functions. If you pass a function as a prop to child components, it’s recreated on every render by default. useCallback
tells React to reuse the same function instance as long as its dependencies don’t change, improving performance by reducing unnecessary re-renders.
const handleClick = (input) => {
console.log('Handling click...');
};
// `useCallback` memoizes `handleClick`
const memoizedHandleClick = useCallback(() => handleClick(input), [input]);
In this case, memoizedHandleClick
will retain its reference unless input
changes. This way, child components that receive memoizedHandleClick
as a prop don’t re-render unnecessarily.
useLayoutEffect hook
In React, useLayoutEffect
works similarly to useEffect
. The key difference between both is that it runs synchronously after the DOM is updated but before the browser has painted (just before the user sees the changes). This makes useLayoutEffect
useful for situations where the code directly interacts with the DOM like measuring elements’ sizes, positions, or making visual adjustments based on these measurements.
A good example is measuring an element’s height immediately after it renders and adjust it if necessary.
import { useLayoutEffect, useRef, useState } from 'react';
const LayoutExample = () => {
const elementRef = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
if (elementRef.current) {
setHeight(elementRef.current.clientHeight);
}
}, [elementRef]);
return (
<div ref={elementRef}>
<p>Element height: {height}px</p>
</div>
);
};
In this example, useLayoutEffect
captures the element’s height as soon as it’s added to the DOM and updates the height
state.
In conclusion, understanding these common mistakes and best practices in React and JavaScript can improve your code quality, performance, and maintainability. Each of the points discussed, from state management and the efficient use of hooks to accessibility contributes to writing cleaner, more efficient applications. Developing with these principles in mind ultimately saves time, reduces bugs, and makes our codebase more enjoyable for both ourselves and our team members.