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
TodosApp
for 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>; } }