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.
\ Let’s check out some of these examples:
1.) Object ReferencingIt’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.
Shallow copyA 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.
Deep CopyA 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.
2.) Short circuit operator and Zero displayUsing 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 = () =>This is a component to be rendered
const MainComponent = () => { return (This is the main component
{testArr?.length &&\
3.) Environment Variablesit’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!
4.) Unnecessary "use client" StatementsNext.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\
7.) Keys in Array rendered with MapReact 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 = () => (\
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.
\
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:
\
\
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.
\
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.
10.) Effective State Management and Hook Usage in ReactIn 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:
Avoid Direct Mutation in setStateRather 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 (Counter: {counter}
In this example:
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 (Count: {count}
\ In this example:
\
Choosing useState vs. useContextuseState 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 (Count: {count}
\ 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.
Optimizing with useMemo and useCallbackIn 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.
When to use useLayoutEffect hookIn 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 (Element height: {height}px
\ In this example, useLayoutEffect captures the element’s height as soon as it’s added to the DOM and updates the height state.
CONCLUSIONIn 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.
All Rights Reserved. Copyright , Central Coast Communications, Inc.