React Component Design

February 26, 2020

This article is a written version of Rithm School’s React Component Design lecture.
  • Considerations for deciding on components & state
  • Practice designing a React app
  • Compare different patterns for writing components

The goals of this lecture are to:

  1. Understand different considerations when deciding on components and state
  2. Practice designing a React application
  3. Compare different patterns for writing components

Designing Components and State

Designing React components is a skill that takes time and repitition to develop.

Components

Generally speaking, React components should be small and do one thing.

By limiting the scope of functionality, we increase the chance that they can be used elsewhere in our application.

Take for example a component that displays a specific todo task. This component could be used across many types of lists, not just in the context of a todo list.

"Dumb" Components

Often, small components with limited functionality are so simple that they don't require any state.

These components are also referred to as presentational components.

In the code snippet below, our TodoItem component does nothing but display a property that is sent in via props:

function TodoItem(props) {
  return <div className="TodoItem">{ props.task }</div>;
}

A level up, in the containing component where our TodoItem lives, we then pass to it the only information it needs (task) via a prop:

function TodoList() { 
  // ... lots missing
  return (
    <div className="TodoList">
      <TodoItem task={ todos[0] } />
      <TodoItem task={ todos[1] } />
      <TodoItem task={ todos[2] } />
    </div>
  );
}

💡Though we refer to them as "dumb" components, these presentational components are ideal. Remember: we like predictable, easy-to-debug code.

By limiting the state of components to only what is absolutely necessary, and only using presentational components otherwise, if problems arise, we can often root out the source of the problem much faster.
Don't Store Derived Information

When we use the term "derived information", we mean information that can be caluclated from the data we alreay have.

For example, take a look at the following component:

function TodoList() {
  const [todos, setTodos] = useState(["wash car", "wash cat"]);
  const [numTodos, setNumTodos] = useState(2);

  return (
    <div>
      You have {numTodos} tasks ...
    </div>
  );
}

Rather than adding to the complexity of our component with the additional state overhead of numTodos, it would be a better choice to simply access todos.length.

Resist the urge to over-engineer and keep your components as simple as possible.

Example: Dice Game

Let's work through designing an example application.

Say we are building a dice-rolling game for a casino.

Our game will feature 6 dice, each with a value between 1 and 20. When the user clicks the "Roll" button, each die will be assigned a new value.

In the root of our application, in App.js, we'll place our game component, <Dice />:

<Dice />

But, say it's the case that we'd like to have a concurrent game appear below, this time with only 4 dice and with each die having a maximum of 6 rather than 20.

How might we refactor our existing component to allow that?

A good place to start is by identifying the properties that we'd like to be customizable. In this case, numDice, title (of the game) and the maxVal.

Our resultant JSX would look like the following:

<div className="App">
  <Dice />
  <Dice numDice={4} title="Mini Dice" maxVal={6} />
</div>
Design Considerations

Now that we've gotten a high level view of what our application will do, there are a series of questions we need ask ourselves as we dive into the code:

  1. What components do we need?
  2. What props does each component need?
  3. What state does each component need?
Component

From our planning above, we identified that the Dice component needs three props: numDice, title, and maxVal.

Within our <Dice /> component, our state will hold the dice that will be displayed. We will create a nested component called <Die /> for each item in the dice array.

Last but not least, we know that when a user clicks on the "Roll" button, the state of the dice will change. This onClick event will require a corresponding click handler.

Component

As mentioned above, we've decided there will be one <Die /> component for each die within our <Dice /> component's state.

The only thing that the <Die /> cares about is the number it must display, which we'll pass in via a prop.

Other than that, there is no other functionality required for this component!

Patterns for Writing Components

Let's take a look at a few common patterns to keep in mind when writing components.

Destructuring

You've seen how helpful ES6 destructuring can be with regard to keeping our code clean and concise. Using destructuring within our components is no different.

Up until this point, we've been using props in the following way:

function Dice(props) {
  // we reference props via
  // props.title, props.numDice, props.maxVal
}

But, assuming there aren't an exceessive number of props being passed, we can use destructuring to avoid having to access each property off of the props object:

function Dice({ title, numDice, maxVal }) {
  // we reference props via
  // title, numDice, maxVal
}

What's more, by using destructuring with the props that are passed in, we can easily set default values to each.

Up until this point, we've assigned default properties using defaultProps:

function Dice(props) {
  // ... lots missing
}

Dice.defaultProps = {
  title: "Main Game",
  numDice: 6,
  maxVal: 20
};

But, we can just as easily accomplish the same thing using default values:

function Dice({ title = "Main Game", numDice = 6, maxVal = 20 }) {
  // ... lots missing
}
Arrow Functions

Components are just functions, so we can choose to write them as arrow functions if we choose.

If a component immediately renders, rather than using the function keyword, as we've been doing in the past:

function Die(props) {
  return <div className="Die">{props.value}</div>;
}

we can instead take advantage of the arrow function's implicit return, refactoring our function to look like the following:

const Die = ({ value }) => (
  <div className="Die">{value}</div>
);

Keep in mind that it's not the case that you should just blindly use arrow functions for everything.

You'll see arrow functions used throughout documentation and code examples, but whatever direction you decide to go, the most important part is to stay consistent with the pattern you choose.