Question Mark Anti-Patterns? 2 to Look Out for in React/JS

If you’re working with React, you may be familiar with using the single question mark in JavaScript. This acts as the ternary operator in a ternary statement, or for optional chaining with variables. These shortcuts are handy, but they can also be signs of trouble in certain situations.

Nested Ternary Statements

The ternary operator is a useful shortcut JavaScript gives us to quickly resolve values using true/false logic. (If you’re not familiar with the ternary operator, check out the documentation here.) The ternary operator can be extremely valuable in small situations. For example, if we compared this:


const randomBoolean = Math.random() >= 0.5;
let x = "";

if (randomBoolean) {
  x = "true";
} else {
  x = "false";
}

to this:


const randomBoolean = Math.random() >= 0.5;

const x = randomBoolean ? "true" : "false";

The ternary reads much easier and allows us to utilize a const variable right away. However, when we start using ternaries within one another, our code starts pushing the boundaries of what other developers (or our future selves) can decode on the fly. Take, for example, this ternary statement:


const ternary = randomBoolean ? anotherRandomBoolean ? yetAnotherRandomBoolean
   ? "yetAnotherTrue" : "yetAnotherFalse" : "anotherTrue" : "false";

Or even this monstrosity:


const ternary = randomBoolean ? anotherRandomBoolean ? yetAnotherRandomBoolean 
     ? "yetAnotherTrue" : "yetAnotherFalse" : "anotherTrue" 
     : seriouslyAnotherRandomBoolean ? "seriously...?"
     : justWow ? "just... wow" : "false";

Nesting ternary statements up to three or more times can turn a statement into what I like to call “ternary soup.” The logic becomes harder to understand with each nested ternary. And, it can look even uglier when used in practice (for example, when conditionally rendering many React components). The unreadability of this nested code largely outweighs the benefits the ternary statement provides. You could replace it with an if/else or a switch statement to make the code more readable. Be careful: don’t let your ternary statements get out of hand!

Optional Chaining with Displayed Data

Another question mark anti-pattern occurs when we use optional chaining on variables in a React component. (Read more about optional chaining here.) Optional chaining gives us a nifty shortcut to access a nested function or object property on a possibly null or undefined object. Using a question mark will resolve the whole statement as undefined if its accessor is null/undefined. It does this rather than throwing a nasty error, which could result in a broken page.

Optional chaining can be a quick way to avoid a major error. But, it could also be a sign that we’re not handling a state of displayed data appropriately. Let’s check out a simple example in React. Here, we want to load data to display from a server, using a custom hook that queries for data asynchronously and returns a state variable. (Let’s assume the component is re-rendered when the data changes.)


const DisplayName = () =>{
  const [data] = useQueryExampleData(); //custom hook loads data asynchronously

  const displayName = data?.displayName;

  return (
    <>
      <h1>Here is some example data:</h1>
      <p>{displayName}</p>
    </>
  );
};

Since we’re asynchronously querying for data, the variable could possibly be undefined while the query is loading. As a shortcut, it’s easy to use optional chaining here on the returned data variable to avoid an error. It’s still much better than displaying a broken page to the user, but we’re missing something.

When we use optional chaining here, we’re overlooking one state of the data: when the query is still loading and the data is undefined. While a good user experience is subjective, displaying “undefined” to the user should be a no-go, even for a second. Using the optional chaining question mark here signals that we’re not handling a possible state of displayed data.

Let’s fix this:


const DisplayName = () => {
  const [data] = useQueryExampleData(); //custom hook loads data asynchronously

  if (!data) {
    return (<p>Loading...</p>)
  }

  const displayName = data.displayName;

  return (
    <>
      <h1>Here is some example data:</h1>
      <p>{displayName}</p>
    </>
  );
};

Now that we’ve added an undefined check on the returned data variable, we’re handling all possible states of the data that could be displayed to the user. That means we can operate with the returned data knowing for certain it’s populated at that point.

Throwing a question mark on a potentially undefined variable in a component might be fine in a pinch, but be careful. It could be a red flag indicating an unhandled state and could lead to a bad user experience.

Question Mark Anti-Patterns

The single question mark in JavaScript can be a great shortcut, whether you’re using it in a ternary statement or for optional chaining. But, be aware of these anti-patterns, specifically when you use them in React components. They can lead to trouble for you, your team, or even the users of your application.

 
Conversation
  • Great advice! I think there’s a lot of this that’s generally good even outside React.

    I would change one thing—using the expression “!data” in your final code example will usually work, but has the potential to have unexpected effects in some cases due to Boolean coercion (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean#boolean_coercion).

    One key example is when “data” is an empty string. In this case, you’ll end up with a “loading” message stuck indefinitely.

    What I do instead is use an explicit “not undefined” check, and write it as a TypeScript type guard like this: https://gist.github.com/atomicmattie/df8a03670360721754eab5bcc5f43fcf

    • Teagen Kiel Teagen Kiel says:

      Ah – great catch!! It’s certainly important to avoid Boolean coercion here, and using narrowing via TypeScript is a great way to catch that. Thanks for the clarification Mattie!

  • Join the conversation

    Your email address will not be published. Required fields are marked *