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#
Start with a data mockup and UI mockup.
Based on the mockups, break the UI into components.
Build a static version in React.
Figure out what needs to go in state.
Decide where state should live.
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
, andEditableHoursCell
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
.
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>
);
}
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.
function EditableRateCell({ value, isEditing }) {
return isEditing ? (
<td>
$<input type="text" value={value} />
/hr
</td>
) : (
<td>{formatCurrency(value)}/hr</td>
);
}
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:
export default EditableRowModeButtons;
Alternatively, you can put export default
at the beginning of the component declaration:
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.
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:
export default InvoiceTable;
Then, in src/App.jsx
, import and render InvoiceTable
:
import './App.css';
import InvoiceTable from './components/InvoiceTable.jsx';
// (...excerpt)
function App() {
return <InvoiceTable />;
}
export default App;
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:
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.
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:
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
:
function App() {
return <InvoiceTable initialInvoiceList={TEST_DATA} />;
}
Then, update InvoiceTable
to read initialInvoiceList
from props and use it to render each row.
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
, andhours
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
, andEditableHoursCell
. Their first common parent isInvoiceTable
.The row mode (
isEditable
) is used byEditableRowModeButtons
,EditableDescriptionCell
,EditableRateCell
, andEditableHoursCell
. Their first common parent isInvoiceTableRow
.The values of inputs are used by
EditableDescriptionCell
,EditableRateCell
, andEditableHoursCell
. Their first common parent isInvoiceTableRow
.
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:
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
:
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:
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
:
<EditableRowModeButtons
isEditing={isEditing}
onEditClick={setEditMode}
onSaveClick={setNormalMode}
/>
Inside EditableRowModeButtons
, add onClick
event handlers and set the parent state from them:
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:
<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
:
// 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:
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
:
<InvoiceTableAddButton onClick={addInvoiceRow} />
Inside InvoiceTableAddButton
, add the onClick
handler and use it to set the parent state:
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
:
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
:
<EditableRowModeButtons
isEditing={isEditing}
onEditClick={setEditMode}
onSaveClick={setNormalMode}
onDeleteClick={onDeleteRow}
/>
Finally, in EditableRowModeButtons
, use the function to handle the onClick
event for the
Delete button:
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;