Computed Properties in React

By Dave Ceddia Comment

Computed Properties in React

Frameworks like Vue have the notion of “computed properties” – a way to derive some new, possibly complex data based on something passed in.

This post covers how to achieve the same result in React.

Compute the Property at Render Time

The “React way” to handle computing derived data is to compute it in the render method (or in the function body, if it’s a stateless component). Yes, right at render time. Yes, every single render. (We’ll talk about performance in a minute)

The simplest way is to do it inline, like this. Remember that with React, you can run arbitrary JS code inside single braces, and React will render out the result of that expression.

function UrlPath({ fullUrl }) {
  return (
    <div>{new URL(fullUrl).pathname}</div>
  );
}

// Used like:
<UrlPath fullUrl="https://daveceddia.com/pure-react/" />

// Would render:
<div>/pure-react/</div>

If your computation is short and sweet, just put it in render.

Simple operations are unlikely to cause performance problems, but if you notice a slowdown, take a look at your browser’s performance tools. Chrome, Firefox, Safari, and Edge all have built-in tools, usually under the “Performance” tab of devtools, that’ll let you record your running app and see where the slowdown is happening.

Extract Computation to a Function

If your computation is complex, you might want to pull it out of your component and put it in a function. This would make it reusable in other components, too. Here’s an example of filtering and sorting a list of products to only show the “new” ones, and sort them by price:

function newItemsCheapestFirst(items) {
  return items
    .filter(item => item.isNew)
    .sort((a, b) => {
      if(a.price < b.price) {
        return -1;
      } else if(a.price > b.price) {
        return 1;
      } else {
        return 0;
      }
    });
}

function NewItemsList({ items }) {
  return (
    <ul>
      {newItemsCheapestFirst(items).map(item =>
        <li key={item.id}>{item.name}, ${item.price}</li>
      )}
    </ul>
  );
}

Here’s a working example on CodeSandbox.

The newItemsCheapestFirst function is doing most of the work here. We could inline it into the component, but it’s much more readable (and reusable) with it written as a standalone function.

Notice that the computation function doesn’t deal with creating the <li> element for each item. That’s intentional, to keep the “item handling stuff” separate from the “item rendering stuff”. Let the React component NewItemsList deal with rendering, while the newItemsCheapestFirst function deals with the data.

Computations in a Class Component

You can adapt the example above to a class component by moving the newItemsCheapestFirst function into the class, like this:

(Know that you don’t have to do this, though – if you want the function to be reused by other components, it makes more sense to leave it as a regular standalone function)

class NewItemsList extends React.Component {
  newItemsCheapestFirst() {
    // I'm getting `items` from `props` instead of
    // passing it in, but either way works.
    return this.props.items.filter(item => item.isNew).sort((a, b) => {
      if (a.price < b.price) {
        return -1;
      } else if (a.price > b.price) {
        return 1;
      } else {
        return 0;
      }
    });
  }

  render() {
    return (
      <ul>
        {this.newItemsCheapestFirst().map(item => (
          <li key={item.id}>
            {item.name}, ${item.price}
          </li>
        ))}
      </ul>
    );
  }
}

You could even take it one step further and turn the computation into a getter, so that accessing it works like accessing a property:

class NewItemsList extends React.Component {
  // Added the "get" keyword in front of the function name...
  get newItemsCheapestFirst() {
    return this.props.items.filter(item => item.isNew).sort((a, b) => {
      if (a.price < b.price) {
        return -1;
      } else if (a.price > b.price) {
        return 1;
      } else {
        return 0;
      }
    });
  }

  render() {
    // Now we can use it like a property instead of a function call
    return (
      <ul>
        {this.newItemsCheapestFirst.map(item => (
          <li key={item.id}>
            {item.name}, ${item.price}
          </li>
        ))}
      </ul>
    );
  }
}

Personally, I’d probably stick with the “function” approach, rather than using a getter. I think mixing and matching the two would lead to confusion – should it be this.newItemsCheapestFirst or this.newItemsCheapestFirst()? Be consistent in whatever you choose to do.

Memoize Expensive Computations

If your computation is expensive – maybe you’re filtering a list of hundreds of items or something – then you can get a nice performance boost by memoizing (but not memoRizing) your computation function.

What is Memoization?

Memoization is a fancy word for caching. It says “Remember the result of calling this function, and next time it’s called with the same argument(s), just return the old result instead of recomputing it.” The function is basically memorizing the answers. It’s still called “memoization,” though. Without the “r”.

How Does Memoization Work?

You can use an existing memoize function, like the one in Lodash, or you can write your own with not much code. Here is an example, patterned after the one in Lodash:

function memoize(func) {
  // Create a new cache, just for this function
  let cache = new Map();

  const memoized = function (...args) {
    // Use the first argument as the cache key.
    // If your function takes multiple args, you may
    // want to come up with a more complex scheme
    let key = args[0];

    // Return the cached value if one exists
    if (cache.has(key)) {
      return cache.get(key);
    }

    // Otherwise, compute the result and save it
    // before returning it.
    let result = func.apply(this, args);
    cache.set(key, result);
    return result;
  };

  return memoized;
}

function doSort(items) {
  console.log('doing the sort');
  return items.sort();
}

let memoizedSort = memoize(doSort);

// Look at the console and notice how it only
// prints 'doing the sort' once!

let numbers = [1,7,4,2,4,9,28,3];
memoizedSort(numbers);
memoizedSort(numbers);
memoizedSort(numbers);

Here’s the working example on CodeSandbox.

How to Use Memoization With React

Now that you have a handle on how memoization works, and have a memoization function in hand (your own or someone else’s), you can wrap your expensive function calls with it.

That could look something like this:

// Let's assume we're passing in a ton of items and this is slow
function newItemsCheapestFirst(items) {
  return items
    .filter(item => item.isNew)
    .sort((a, b) => {
      if(a.price < b.price) {
        return -1;
      } else if(a.price > b.price) {
        return 1;
      } else {
        return 0;
      }
    });
}

// Memoize the function...
memoizedCheapestItems = memoize(newItemsCheapestFirst);

function NewItemsList({ items }) {
  // Use the memoized wrapper instead of the original
  return (
    <ul>
      {memoizedCheapestItems(items).map(item =>
        <li key={item.id}>{item.name}, ${item.price}</li>
      )}
    </ul>
  );
}

ONE HUGE CAVEAT: Make sure you’re not re-creating the memoized function every time, or you won’t see any benefits from it – it will be called every time. In other words, if your call to memoize happens every render, that’s not good. Do not do this:

function NewItemsList({ items }) {
  // NO!
  // Don't memoize it every render! It'll never cache anything this way.
  const memoizedExpensiveFunction = memoize(expensiveFunction);

  return (
    <ul>
      {memoizedExpensiveFunction(items).map(item =>
        <li key={item.id}>{item.name}, ${item.price}</li>
      )}
    </ul>
  );
}

Recap

Here’s what we covered:

  • React doesn’t have “computed properties” but you can achieve the same effect with functions
  • If the computation is short and quick, do it in render (or directly in a function component)
  • If the computation is complex to read or needs to be reused, extract it to a function.
  • If the computation is expensive to run, consider memoizing it.

If you want to remember this stuff, pick a practice exercise or two and build it out on CodeSandbox. Typing it out with your own two hands is the best way to learn :)

Learning React can be a struggle -- so many libraries and tools!
My advice? Ignore all of them :)
For a step-by-step approach, read my book Pure React.

Loved it! Very well written and put together. Love that you focused only on React. Wish I had stumbled onto your book first before I messed around with all those scary "boilerplate" projects.
— John Lyon-Smith
comments powered by Disqus