Code Along: Thinking in React#

To build effective React apps, you’ll need to change how you think about your UI designs and your application architecture. In this code along, you’ll practice thinking in React: breaking UI into components, building visual states for each, and connecting them together so data flows through them.

Setup#

Run the command below to download the starter code.

$ dmget wb-thinking-react

Navigate to the project folder and use the following commands to install dependencies and open the project in VS Code.

$ npm install
$ code .

Project overview#

As a web developer at Mountain Consulting, Inc. and your current project is an internal tool to create invoices for your clients.

What you’re building#

You’re working alongside the back end team and design team in order to develop the tool as a React app.

The back end team will serve invoice data through a JSON API. They’ve given you a mockup of the data so you know what to expect from the server:

[
  { id: 0, description: 'Content plan', rate: 50, hours: 4 },
  { id: 1, description: 'Copy writing', rate: 50, hours: 2 },
  { id: 2, description: 'Website design', rate: 50, hours: 5 },
  { id: 3, description: 'Website development', rate: 100, hours: 5 },
];

The design team has provided a mockup of the UI design:

The rest is in your hands! You’ll follow a general framework for building React apps to complete the task.

Thinking in React#

React is hard—in order to build successful apps, you need to think in React. This can take some getting used to. The steps in this framework will help you think in React so you can build apps more quickly and with less bugs.

Steps for building React apps#

  1. Start with a data mockup and UI mockup.

  2. Based on the mockups, break the UI into components.

  3. Build a static version in React.

  4. Figure out what needs to go in state.

  5. Decide where state should live.

  6. Add inverse data flow so state changes based on user input.

You’ve already done the first step. Time to tackle the next!

Break UI into a component hierarchy#

Based on the UI mockup, draw boxes around the components you need to implement and give them names.

Identifying components#

One way to think about splitting up a design into components is to treat them like you would any other function or object. The single-responsibility principle— a software design principle—is a good place to start. It says that a function or component should only do one thing. Following this principle means decomposing large components into smaller subcomponents.

There are four types of components in this mockup:

  • InvoiceTable contains the entire app.

  • InvoiceTableHeader displays the table header.

    • It might be overkill to make this a subcomponent since it’s pretty simple, but following the single-responsibility principle will make it easier to add complexity should project requirements change. You can always decide to include this as part of the InvoiceTable component later, during the final stages of the project.

  • InvoiceTableRow displays a row for each invoice item.

  • InvoiceTableAddButton displays a button to add a new invoice item.

We can break the InvoiceTableRow component down even further:

  • EditableRowModeButtons allows users to toggle between normal mode and edit mode.

    • In normal mode, the Delete button will remove the row and the Edit button will put the row in edit mode.

    • In edit mode, users will be able to edit the information in each cell and press the Save button to save their changes.

  • EditableDescriptionCell, EditableRateCell, and EditableHoursCell display the data from each invoice. In edit mode, they’ll display a text input used to update the data.

Organize components into a hierarchy#

Now that you’ve decided on your components, arrange them in a hierarchy so you have an outline of parent components and their children. You may want to copy this hierarchy into your own notes or project repo so you can keep track of your progress and organize your work.

  • InvoiceTable

    • InvoiceTableHeader

    • InvoiceTableRow

      • EditableRowModeButtons

      • EditableDescriptionCell

      • EditableRateCell

      • EditableHoursCell

    • InvoiceTableAddButton

Build a static version of the app#

In React, a static app only renders UI without adding interactivity. Building a static app requires a lot of typing and no thinking, but adding interactivity requires a lot of thinking and not so much typing. That’s why it’s easier to start with a static app and then add interactivity later.

Recap: The component hierarchy#

These are the components needed to build the app that you organized into a hierarchy from the previous step:

  • InvoiceTable

    • InvoiceTableHeader

    • InvoiceTableRow

      • EditableRowModeButtons

      • EditableDescriptionCell

      • EditableRateCell

      • EditableHoursCell

    • InvoiceTableAddButton

You can either start building from the top and go down or from the bottom and build up. Most people find it easier to go top-down for larger apps and bottom-up for smaller ones. In practice, you’ll probably use a combination of both techniques.

Going bottom-up, let’s start with the Editable* components.

Build the Editable* components#

Don’t forget to start the dev server with npm run dev!

These components should all go in separate files. For example, the EditableRowModeButtons component should go in a new file called src/components/EditableRowModeButtons.jsx.

All of the Editable* components are conditionally rendered based on an isEditing prop. When a row is in normal mode, isEditing will be false. If it’s in edit mode, isEditing will be true.

src/components/EditableRowModeButtons.jsx#
function EditableRowModeButtons({ isEditing }) {
  return isEditing ? (
    <td>
      <button>Save</button>
    </td>
  ) : (
    <td>
      <button>Delete</button>
      <button>Edit</button>
    </td>
  );
}

Destructuring function parameters

The example above uses the destructuring pattern to unpack values from props. It’s equivalent to:

function EditableRowModeButtons(props) {
  return props.isEditing ? (
    <td>
      <button>Save</button>
    </td>
  ) : (
    <td>
      <button>Delete</button>
      <button>Edit</button>
    </td>
  );
}
src/components/EditableDescriptionCell.jsx#
function EditableDescriptionCell({ value, isEditing }) {
  return isEditing ? (
    <td>
      <input type="text" value={value} />
    </td>
  ) : (
    <td>{value}</td>
  );
}

For EditableRateCell, we’ll want to display its value as currency. We’ll use a function that’s already been provided in utils/formatCurrency.js to do that. Import formatCurrency at the top of the file:

import formatCurrency from '../utils/formatCurrency.js';

Now we can call it to format the rate as a currency.

src/components/EditableRateCell.jsx#
function EditableRateCell({ value, isEditing }) {
  return isEditing ? (
    <td>
      $<input type="text" value={value} />
      /hr
    </td>
  ) : (
    <td>{formatCurrency(value)}/hr</td>
  );
}
src/components/EditableHoursCell.jsx#
function EditableHoursCell({ value, isEditing }) {
  return isEditing ? (
    <td>
      <input type="text" value={value} />
    </td>
  ) : (
    <td>{value}</td>
  );
}

Once you’ve created the components, don’t forget to export default all of them so that you can import them in the parent InvoiceTable component. For example, in EditableRowModeButtons.jsx, you would put this at the bottom of the file:

src/components/EditableRowModeButtons.jsx#
export default EditableRowModeButtons;

Alternatively, you can put export default at the beginning of the component declaration:

src/components/EditableRowModeButtons.jsx#
export default function EditableRowModeButtons({ isEditing }) {
  ...
}

Now, in src/components/InvoiceTable.jsx, let’s just add a barebones InvoiceTable component and have it render the Editable* components with some test data. Don’t forget to import the Editable* components at the top.

src/components/InvoiceTable.jsx#
function InvoiceTable() {
  return (
    <table>
      <tbody>
        <tr>
          <EditableRowModeButtons isEditing={false} />
          <EditableDescriptionCell value="Web Development" isEditing={false} />
          <EditableRateCell value={25} isEditing={false} />
          <EditableHoursCell value={10} isEditing={false} />
        </tr>

        <tr>
          <EditableRowModeButtons isEditing={true} />
          <EditableDescriptionCell value="Copywriting" isEditing={true} />
          <EditableRateCell value={20} isEditing={true} />
          <EditableHoursCell value={8} isEditing={true} />
        </tr>
      </tbody>
    </table>
  );
}

Now export InvoiceTable as the default export at the bottom of the file:

src/components/InvoiceTable.jsx#
export default InvoiceTable;

Then, in src/App.jsx, import and render InvoiceTable:

src/App.jsx#
import './App.css';
import InvoiceTable from './components/InvoiceTable.jsx';

// (...excerpt)

function App() {
  return <InvoiceTable />;
}

export default App;

Build InvoiceTableHeader and InvoiceTableAddButton#

These two components are much more straightforward.

src/components/InvoiceTableHeader.jsx#
function InvoiceTableHeader() {
  return (
    <tr>
      <th></th>
      <th>Description</th>
      <th>Rate</th>
      <th>Hours</th>
      <th>Amount</th>
    </tr>
  );
}

export default InvoiceTableHeader;
src/components/InvoiceTableAddButton.jsx#
function InvoiceTableAddButton() {
  return (
    <tr>
      <td></td>
      <td colSpan="4">
        <button>Add</button>
      </td>
    </tr>
  );
}

export default InvoiceTableAddButton;

To test the components, import and render them in InvoiceTable:

src/components/InvoiceTable.jsx#
function InvoiceTable() {
  return (
    <table>
      <thead>
        <InvoiceTableHeader />
      </thead>
      <tbody>
        <tr>
          <EditableRowModeButtons isEditing={false} />
          <EditableDescriptionCell value="Web Development" isEditing={false} />
          <EditableRateCell value={25} isEditing={false} />
          <EditableHoursCell value={10} isEditing={false} />
        </tr>

        <tr>
          <EditableRowModeButtons isEditing={true} />
          <EditableDescriptionCell value="Copywriting" isEditing={true} />
          <EditableRateCell value={20} isEditing={true} />
          <EditableHoursCell value={8} isEditing={true} />
        </tr>
      </tbody>
      <tfoot>
        <InvoiceTableAddButton />
      </tfoot>
    </table>
  );
}

Build InvoiceTableRow#

You’ve already written the majority of the code needed for InvoiceTableRow when you tested the Editable* components. The code just needs to be tweaked to replace hardcoded values with props:

src/components/InvoiceTableRow.jsx#
function InvoiceTableRow({ initialInvoiceData, initialIsEditing }) {
  const { description, rate, hours } = initialInvoiceData;

  return (
    <tr>
      <EditableRowModeButtons isEditing={initialIsEditing} />
      <EditableDescriptionCell value={description} isEditing={initialIsEditing} />
      <EditableRateCell value={rate} isEditing={initialIsEditing} />
      <EditableHoursCell value={hours} isEditing={initialIsEditing} />
    </tr>
  );
}

Why initialInvoiceData and initialIsEdit?

Notice the props for InvoiceTableRow are named initialInvoiceData and initialIsEdit instead of just invoiceData and isEdit. This is because the invoice data and mode of a row needs to change over time—otherwise, you wouldn’t be able to implement rows that users can edit and delete! The props will be used to give rows their initial data.

The last thing it needs is a cell to display the total cost of the invoice item. The total cost is calculated using rate * hours. Then, import the formatCurrency function we used earlier (from src/utils/formatCurrency.js) and use the function to render the Amount cell.

src/components/InvoiceTableRow.jsx#
function InvoiceTableRow({ initialInvoiceData, initialIsEditing }) {
  const { description, rate, hours } = initialInvoiceData;

  return (
    <tr>
      <EditableRowModeButtons isEditing={initialIsEditing} />
      <EditableDescriptionCell value={description} isEditing={initialIsEditing} />
      <EditableRateCell value={rate} isEditing={initialIsEditing} />
      <EditableHoursCell value={hours} isEditing={initialIsEditing} />
      <td>{formatCurrency(rate * hours)}</td>
    </tr>
  );
}

Replace the <tr> elements in InvoiceTable with your new InvoiceTableRow components and test that they work correctly:

src/components/InvoiceTable.jsx#
function InvoiceTable() {
  return (
    <table>
      <thead>
        <InvoiceTableHeader />
      </thead>
      <tbody>
        <InvoiceTableRow
          initialInvoiceData={{ description: 'Web Development', rate: 25, hours: 10 }}
          initialIsEditing={false}
        />
        <InvoiceTableRow
          intialInvoiceData={{ description: 'Copywriting', rate: 20, hours: 8 }}
          initialIsEditing={true}
        />
      </tbody>
      <tfoot>
        <InvoiceTableAddButton />
      </tfoot>
    </table>
  );
}

At this point, your app should look something like this:

Build InvoiceTable#

You have almost all the parts you need to complete InvoiceTable. The only piece that’s missing is making it possible to render initial invoice data from a server instead of using hardcoded values.

To do this, the initial data needs to be passed in as a prop. The starter code in src/App.jsx includes data from the back end mockup in a variable called TEST_DATA. Update the App component so it passes the test data to InvoiceTable:

src/App.jsx#
function App() {
  return <InvoiceTable initialInvoiceList={TEST_DATA} />;
}

Then, update InvoiceTable to read initialInvoiceList from props and use it to render each row.

src/components/InvoiceTable.jsx#
function InvoiceTable() {
  const rows = initialInvoiceList.map((invoiceItem) => {
    const { id, description, rate, hours } = invoiceItem;

    return (
      <InvoiceTableRow
        key={id}
        initialInvoiceData={{ description, rate, hours }}
        initialIsEditing={false}
      />
    );
  });

  return (
    <table>
      <thead>
        <InvoiceTableHeader />
      </thead>
      <tbody>{rows}</tbody>
      <tfoot>
        <InvoiceTableAddButton />
      </tfoot>
    </table>
  );
}

The final result should look like this:

With the static version of the app complete, you can begin to add interactivity.

Find what should go in state#

Before you can implement the interactive features of the app, you need to come up with a plan for how to structure and utilize state.

The principles for structuring state—avoiding redundancies, duplication, etc.—can be boiled down to a single principle: DRY (Don’t Repeat Yourself). You need to figure out the absolute minimal representation of the state your application needs to work. Think about which values are absolutely needed and which can be calculated on-demand.

For example

How would you build a task list app in React? You can store the tasks as an array in state. What if you want to also display the number of items in the list? It might be tempting to store that number in state but you don’t actually need another state value—instead, you should get this number from the length of the task array.

Determine what’s in state#

Brainstorm a list of all the pieces of data in the application and determine which pieces should be in state.

Everybody always asks, “What is state?” but nobody ever asks, “How is state?”

How do you figure out of something should be in state or not? Here are some questions you can ask yourself:

  • Does it remain unchanged over time? If yes, it isn’t state.

  • Is it passed in from a parent via props? If yes, it isn’t state.

  • Can it be derived (calculated) from existing state or props? If yes, it definitely isn’t state.

After asking these questions and disqualifying non-state values, what’s left should probably go in state.

Here’s the data you’ll need in the application:

  • The initial list of invoice items (either an empty list or a list that was previously saved in the database.)

  • The amount of money each item costs.

  • The mode for each row, whether it’s in normal mode or edit mode.

  • Anything the user enters as input for a row’s description, rate, and hours in edit mode.

Going through the list one by one:

  • The list of invoice items seems to be state. Even though it’s initially passed in as props, the list can change over time (as items are added and deleted). Also, it can’t be computed from anything else.

  • The amount of money each item costs is not state because it can be calculated from rate and hours.

  • The mode for each row seems to be state since it changes over time and can’t be computed from anything else.

  • The values of inputs for the row’s description cell, rate cell, and hours cell seem to be state since it changes over time and can’t be computed from anything else.

Identify where your state should live#

Now that you have the app’s minimal state data, you need to identify which component should own the state.

You have five state values (invoice data, a row’s mode, description, rate, and hours). For each value:

  • Identify every component that renders something based on that state.

  • Find their closest common parent component—a component above them all in the hierarchy. In most cases, this is where your state should live.

What if it’s not possible to store state in the closest parent?

Remember that React uses one-way data flow, passing data down the component hierarchy from parent to child. Any kind of interactivity will be possible as long as state lives above where it’s needed.

So, if it’s impossible to store state in the closest common parent, you can put state into some component above the common parent. As a last resort, if you can’t find a component that makes sense, then you can create a new component that’s only in charge of holding state (as long as you add it above the common parent in the hierarchy).

Where state should live#

Let’s go through each state value and determine where it should be stored:

  • The list of invoice items is used by InvoiceTableRow, EditableDescriptionCell, EditableRateCell, and EditableHoursCell. Their first common parent is InvoiceTable.

  • The row mode (isEditable) is used by EditableRowModeButtons, EditableDescriptionCell, EditableRateCell, and EditableHoursCell. Their first common parent is InvoiceTableRow.

  • The values of inputs are used by EditableDescriptionCell, EditableRateCell, and EditableHoursCell. Their first common parent is InvoiceTableRow.

To recap, these components will be responsible for storing the following state values:

  • InvoiceTable

    • List of invoice data

  • InvoiceTableRow

    • Row mode

    • Value of the description field

    • Value of the rate field

    • Value of the hours field

Add state with useState#

Add the state values to their components with the useState hook.

Let’s start with InvoiceTableRow. Add a state value for isEditing and convert description, rate, and hours into state instead of reading them from props:

src/components/InvoiceTableRow.jsx#
function InvoiceTableRow({ initialInvoiceData, initialIsEditing }) {
  const [isEditing, setIsEditing] = useState(initialIsEditing);

  const [description, setDescription] = useState(initialInvoiceData.description);
  const [rate, setRate] = useState(initialInvoiceData.rate);
  const [hours, setHours] = useState(initialInvoiceData.hours);
/* (...excerpt) */ }

Do the same for InvoiceTable:

src/components/InvoiceTable.jsx#
function InvoiceTable({ initialInvoiceList }) {
  const [invoiceList, setInvoiceList] = useState(initialInvoiceList);
/* (...excerpt) */ }

Add inverse data flow#

Your app renders correctly with props and state flowing down the hierarchy. Now you’ll need to support data going the other direction. To change state based on user input, components lower in the hierarchy (like EditableDescriptionCell) need to update the state in InvoiceTableRow and InvoiceTable.

Implement edit mode for rows#

The buttons in EditableRowModeButtons should allow users to enable edit mode or normal mode for a row. This behavior is tied to isEditing: when isEditing is true, the row is in edit mode, and when it’s false, the row is in normal mode. The state is owned by InvoiceTableRow, so only it can call setIsEditing. To let EditableRowModeButtons update InvoiceTableRow’s state, you need to pass setIsEditing down to EditableRowModeButtons.

Let’s pass setIsEditing down through separate functions for enabling edit mode and enabling normal mode. You could also write these functions directly in the JSX but this way we have self-documenting code that doesn’t need to be explained with a comment:

src/components/InvoiceTableRow.jsx#
function InvoiceTableRow({ initialInvoiceData, initialIsEditing }) {
  const [isEditing, setIsEditing] = useState(initialIsEditing);

  const [description, setDescription] = useState(initialInvoiceData.description);
  const [rate, setRate] = useState(initialInvoiceData.rate);
  const [hours, setHours] = useState(initialInvoiceData.hours);

  const setEditMode = () => setIsEditing(true);
  const setNormalMode = () => setIsEditing(false);
/* (...excerpt) */ );}

Then, in the return statement, render these functions as props for EditableRowModeButtons:

src/components/InvoiceTableRow.jsx#
<EditableRowModeButtons
  isEditing={isEditing}
  onEditClick={setEditMode}
  onSaveClick={setNormalMode}
/>

Inside EditableRowModeButtons, add onClick event handlers and set the parent state from them:

src/components/EditableRowModeButtons.jsx#
function EditableRowModeButtons({ isEditing, onEditClick, onSaveClick }) {
  return isEditing ? (
    <td>
      <button onClick={onSaveClick}>Save</button>
    </td>
  ) : (
    <td>
      <button>Delete</button>
      <button onClick={onEditClick}>Edit</button>
    </td>
  );
}

The Edit and Save buttons should work now! Next, let’s allow users to type into the input fields during edit mode.

Enable editable cells#

To make it possible for Editable* components to update state, you’ll do something similar to what you just did for the row buttons. The setDescription, setRate, and setHours functions are pretty descriptive so there’s no need to create wrapper functions. Inside InvoiceTableRow, pass the set functions to their corresponding components:

src/components/InvoiceTableRow.jsx#
<EditableDescriptionCell
  value={description}
  isEditing={isEditing}
  onValueChange={setDescription}
/>
<EditableRateCell
  value={rate}
  isEditing={isEditing}
  onValueChange={setRate}
/>
<EditableHoursCell
  value={hours}
  isEditing={isEditing}
  onValueChange={setHours}
/>

Inside each Editable* component, add the onChange event handlers and set the parent state from them:

<input
  type="text"
  value={value}
  onChange={(e) => onValueChange(e.target.value)}
/>

Add/remove invoice rows#

To add or delete invoice rows, you’ll need to update state in InvoiceTable. First, add helper functions to the InvoiceTable component for adding and deleting invoice rows.

Adding a new invoice row will require creating an object with default data and a unique ID. You’ll find a utility to help with generating unique IDs in src/utils/idGenerator.js:

src/utils/idGenerator.js#
// Generate a unique ID.
// IDs are auto-incremented, starting at 0 (just like auto-incrementing database keys).
// Normally, this would be done by the database, but we don't have a database.
// Also, the first ID generated will be 4 because our test data already has IDs 0, 1, 2, and 3.

let id = 3;

function generateId() {
  id += 1;
  return id;
}

export default generateId;

To use this function, just import it at the top of src/components/InvoiceTable.jsx.

import generateId from '../utils/idGenerator.js';

Then you can use it in the helper function to add a new invoice row. In InvoiceTable define the following helper functions:

src/components/InvoiceTable.jsx#
function InvoiceTable({ initialInvoiceList }) {
  const [invoiceList, setInvoiceList] = useState(initialInvoiceList);

  const addInvoiceRow = () => {
    const newInvoiceList = [...invoiceList];
    newInvoiceList.push({
      id: generateId(),
      description: 'Description',
      rate: '',
      hours: '',
      isEditing: true,
    });
    setInvoiceList(newInvoiceList);
  };

  const deleteInvoiceRow = (id) => {
    const newInvoiceList = [...invoiceList];
    const index = newInvoiceList.findIndex((invoice) => invoice.id === id);
    newInvoiceList.splice(index, 1);
    setInvoiceList(newInvoiceList);
  };

/* (...excerpt) */ );}

To implement adding an invoice row, pass addInvoiceRow down to InvoiceTableAddButton:

src/components/InvoiceTable.jsx#
<InvoiceTableAddButton onClick={addInvoiceRow} />

Inside InvoiceTableAddButton, add the onClick handler and use it to set the parent state:

src/components/InvoiceTableAddButton.jsx#
function InvoiceTableAddButton({ onClick }) {
  return (
    <tr>
      <td></td>
      <td colSpan="4">
        <button onClick={onClick}>Add</button>
      </td>
    </tr>
  );
}

export default InvoiceTableAddButton;

To make the Delete button on each row work, you’ll need to pass the deleteInvoiceRow function from InvoiceTable to InvoiceTableRow to EditableRowModeButtons.

In InvoiceTable, pass deleteInvoiceRow to each InvoiceTableRow:

src/components/InvoiceTable.jsx#
const rows = invoiceList.map(({ id, description, rate, hours, isEditing }) => (
  <InvoiceTableRow
    key={id}
    initialInvoiceData={{ description, rate, hours }}
    initialIsEditing={isEditing}
    onDeleteRow={() => deleteInvoiceRow(id)}
  />
));

Then, from InvoiceTableRow, pass it to EditableRowModeButtons:

src/components/InvoiceTableRow.jsx#
<EditableRowModeButtons
  isEditing={isEditing}
  onEditClick={setEditMode}
  onSaveClick={setNormalMode}
  onDeleteClick={onDeleteRow}
/>

Finally, in EditableRowModeButtons, use the function to handle the onClick event for the Delete button:

src/components/EditableRowModeButtons.jsx#
function EditableRowModeButtons({ isEditing, onEditClick, onSaveClick, onDeleteClick }) {
  return isEditing ? (
    <td>
      <button onClick={onSaveClick}>Save</button>
    </td>
  ) : (
    <td>
      <button onClick={onDeleteClick}>Delete</button>
      <button onClick={onEditClick}>Edit</button>
    </td>
  );
}

export default EditableRowModeButtons;