Analyzing the class version
Let’s take a look at the class version and understand the concepts used in it. I’ll start with the generic parts of the code that are not about the core app logic and will then explain the main components with inline code comments.
Telling React where to start
The top-level root component for this app is TodosApp. This is what we hand to ReactDOM to make it mount the UI it represents in the browser’s DOM. It’s the starting point of the app.
This root component receives an initialData prop:
// The beginning of the story. Show something in the browser.
ReactDOM.render(
<TodosApp initialData={initialData} />,
document.getElementById('mountNode')
);
The ReactDOM.render method takes 2 arguments:
-
The first argument is the "WHAT". It’s the top-level React element to be rendered to the browser. It’s an instance of
TodosAppfor this example. -
The second argument is the "WHERE". It needs to be a reference to a DOM element that’s already rendered in the browser. React will take over that target DOM element and render the source React element (the first argument) inside it.
Using realistic test data
I made the TodosApp component receive an initialData prop because the requirement specified that the TODO app has to support rendering an initial list of TODOs. To make things a bit more interesting and closer to reality, I made the initialData element an object of todos indexed by their IDs (instead of using arrays).
Here is a mock variable to represent that initial data object:
// Seed test data
const initialData = {
todos: {
A: {
body: 'Learn React Fundamentals',
done: true,
},
B: {
body: 'Build a TODOs App',
done: false,
},
C: {
body: 'Build a Game',
done: false,
},
},
};
Each TODO item needs a unique id property, a body text property, and a done Boolean property to indicate its complete status.
Don’t use short or random data to test your app. Try to use realistic data. That will help you design your UIs better.
| I like to use string values for IDs instead of numeric values. JavaScript is not the best language when it comes to working with numbers. |
Bringing external modules
Chances are your React app will need external functions that are not part of any component but rather imported for the app to use. Together they form what we call the app dependencies. This TODOs example app has one utility function that is external to it: uniqueId.
Since we will be adding new TODOs, we need a way to generate unique id properties for them. This is not something that the user will provide. We can use a simple counter but that’s boring. Here’s a function that uses the current time concatenated with a random value to generate a unique id:
const uniqueId = () => Date.now().toString(36) + Math.random().toString(36);
| You can also import and use an npm package for the "unique-id" dependency. For example, shortid. |
Whenever we need to add a new TODO in the UI we can use this uniqueId function to generate a unique id property for it. Note that this is usually something that a "todo-save" API endpoint would provide. However, for this example, new TODOs will only be saved locally in the application memory to keep things simple.
The core concepts in the class version
Let’s now take a detailed look at the code for the class version and understand the main concepts in it.
I’ll use inline comments to explain concepts after they’re used in the code. If you want to experiment with the code, the interactive version is included at the end of this lesson:
class TodosApp extends React.Component {
/* The component class definition is the blueprint used every time
an "element" from this component is to be rendered in the browser.
React instantiates an object from this class when we do:
<TodosApp ... />
and that object gets displayed in the browser.
React associates this instance with the DOM-rendered element.
This instance can be accessed using the "this" keyword
within the class definition here. */
state = {
todos: this.props.initialData.todos,
filterLabel: 'All',
};
/* Initialize the special state property on the instantiated object
This is a "class field" syntax that's equivalent to doing
"this.state = { .... }" in the class constructor method.
A class component state is a plain-old JS object. This one has a
"todos" property that gets its initial value from the props input
and a "filterLabel" string. You can have as many properties on
the state object as you need but you should keep what you put
on the state to the absolute minimum. */
/* As you'll see in the many methods below, class fields can also
be used with arrow functions and because of how closures work for
arrow functions, we can safely use the "this" keyword within them
and we'll be correctly using the instantiated instance of TodosApp. */
addNewTodo = newTodoBody =>
this.setState(prevState => ({
todos: {
...prevState.todos,
[uniqueId()]: {
body: newTodoBody,
done: false,
},
},
}));
markTodoDone = (todoId, newDoneValue) =>
this.setState(prevState => ({
todos: {
...prevState.todos,
[todoId]: {
...prevState.todos[todoId],
done: newDoneValue,
},
},
}));
deleteTodo = todoId =>
this.setState(prevState => {
const { [todoId]: _, ...todos } = prevState.todos;
return { todos };
});
deleteAllCompleteTodos = () =>
this.setState(prevState => ({
todos: Object.entries(prevState.todos).reduce((acc, [todoId, todo)] => {
if (!todo.done) {
acc[todoId] = todo;
}
return acc;
}, {}),
}));
setFilter = newFilterLabel => this.setState({ filterLabel: newFilterLabel });
/* The 5 methods above are mutations to be done on React's state for
this component (they have nothing to do with the DOM).
Also, except for the "setState" method, all the logic here is
not really React but rather pure JavaScript.
setState is React’s way to keep track of the state changes since
React needs to “react” to all of these changes.
The whole TodosApp component will get re-rendered along with all of
its children every time any of these methods is called. However, React
will still only take to the browser the parts that get different
content based on the new modifications to the state. React uses
a special "reconciliation algorithms" to do that. */
shouldShowTodo = todo => {
const { filterLabel } = this.state;
return (
filterLabel === 'All' ||
(filterLabel === 'Active' && !todo.done) ||
(filterLabel === 'Completed' && todo.done)
);
};
/* The shouldShowTodo method perform "computations".
Computing values is a common thing to do before the render method.
Computation methods can be called directly in the render method below.
They are NOT "handlers" like the mutation methods above. */
render() {
return (
<>
<header>TODO List</header>
<ul>
{Object.entries(this.state.todos).map(
([todoId, todo]) =>
this.shouldShowTodo(todo) && (
<TodoItem
key={todoId}
id={todoId}
todo={todo}
markTodoDone={this.markTodoDone}
deleteTodo={this.deleteTodo}
/>
)
)}
{/* We take the entries of the todos object and map every entry
to a <TodoItem /> element passing it the props it needs.
An entry is a key/value pair. The key is the id of a todo
and the value is the todo object itself (body and done).
Before rendering a TodoItem element, we invoke a function
to check if that element should be shown in the UI based
on the current filtering state. This use of the "&&"
operator is called "short-circuit evaluation". If the
shouldShowTodo function call is false, React will render
nothing. If it's true, React will render the second part
of the Boolean check, which is the TodoItem element itself.
The "key" attribute is a React internal thing. It helps React
identify dynamic children and what to do when they change.
Your code can't depend on it. An "id" prop was defined
to pass the same value used as "key". */}
</ul>
<AddTodoForm onSubmit={this.addNewTodo} />
<div className="actions">
Show:{' '}
<FilterButton
label="All"
onClick={this.setFilter}
active={this.state.filterLabel === 'All'}
/>
<FilterButton
label="Active"
onClick={this.setFilter}
active={this.state.filterLabel === 'Active'}
/>
<FilterButton
label="Completed"
onClick={this.setFilter}
active={this.state.filterLabel === 'Completed'}
/>
</div>
<div className="actions">
<button onClick={this.deleteAllCompleteTodos}>
Delete All Completed
</button>
</div>
<footer>
<TodosLeft todos={this.state.todos} />
</footer>
</>
);
}
/* The render method is what React uses to determine the shape
this element should take in the real browser's UI.
It returns JSX which gets compiled into React API calls.
The <></> syntax is sugar for "React.Fragment"
which is a way to group React elements without
introducing an unnecessary "wrapping" parent element. */
}
class TodoItem extends React.PureComponent {
/* We're not managing any state or side effects for a TodoItem.
Each TodoItem gets purely rendered based on its props input
(which contains both data and behavior).
This purity makes the component a candidate to extend
React.PureComponent instead of React.Component. This signals
to React that if the parent component re-renders and
nothing has changed in the props of a rendered TodoItem
then don't bother re-rendering that TodoItem (in memory).
It's pure! React can re-use the previously rendered version. */
handleCheckboxChange = event => {
const newDone = event.target.checked;
/* React gives access to the native DOM "event" associated
with any handler. You can use event.target to access the
DOM element on which the event occurred. This target is
used here to read the checkbox's "checked" state */
this.props.markTodoDone(this.props.id, newDone);
};
handleXClick = () => this.props.deleteTodo(this.props.id);
/* The 2 local handlers above just invoke the behaviors
that are pre-defined by the parent. The TodoItem
component has no control over these behaviors except
when to invoke them and what arguments they receive.*/
render() {
const { todo } = this.props;
const todoStyle = { textDecoration: todo.done ? 'line-through' : 'none' };
/* This is a computed style object. This is a common way
to represent "conditional" styles. It's used below
with the todo.body span */
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={this.handleCheckboxChange}
/>
<span style={todoStyle}>{todo.body}</span>
<span role="link" onClick={this.handleXClick}>
X
</span>
</li>
);
}
}
class AddTodoForm extends React.PureComponent {
/* The AddTodoForm component depends on a single prop,
which is the onSubmit behavior and it invokes it when
the Add TODO button is clicked. It passes what the user
types in an input box using the DOM directly to read that
typed value. */
handleSubmit = event => {
event.preventDefault();
this.props.onSubmit(event.target.todoBody.value);
event.target.todoBody.value = '';
};
/* This is another example where the handled event's target
attribute is used to read a user interaction. React is not
aware of the UI state changes when the user types or check
a checkbox. It just asks the DOM API for these value when
it needs them. This is the "uncontrolled input" pattern. */
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" name="todoBody" placeholder="What TODO?" />
<button type="submit">Add TODO</button>
</form>
);
}
}
class FilterButton extends React.PureComponent {
handleClick = () => this.props.onClick(this.props.label);
render() {
const buttonStyle = {
fontWeight: this.props.active ? 'bold' : 'normal',
};
/* Another example of conditional styling */
return (
<button onClick={this.handleClick} style={buttonStyle}>
{this.props.label}
</button>
);
}
}
class TodosLeft extends React.PureComponent {
activeTodosCount = () =>
Object.values(this.props.todos).filter(todo => !todo.done).length;
/* This is a practical reason to have this component as "pure".
When the "filterLabel" state changes in the parent TodosApp
component (but the todos array is exactly the same). We do NOT
need to recompute the active TODOs count. It did not change.
The purity of this component will make React
skip that computation for that case. */
componentDidMount() {
document.title = Active TODOs: ${this.activeTodosCount()};
}
componentDidUpdate() {
document.title = Active TODOs: ${this.activeTodosCount()};
}
/* The 2 lifecycle methods present an example of a "side effect".
Whenever this component is re-rendered in memory it'll update
the main document title to show the current active TODOs count. */
render() {
return <div>TODOs left: {this.activeTodosCount()}</div>;
}
}