A Larger React App

Let’s build a bigger app.

About This Exercise

This is another code-along, where things will be explained in class, so the notes on this page are not very detailed.

The little app I’m building here is the same as the one in videos 23-24 of The Net Ninja’s old React tutorial linked above. I’ve modernized the React to use Hooks rather than classes, changed some names, used Material UI instead of Materialize styles, and integrated Git into the tutorial, but all credit for the app design itself goes to The Net Ninja.

The Code-Along

Step 1: Get Ready

This code-along assumes you’ve gone through the introduction to command line React applications. So we’ll assume you have Node.js installed and know how to run npx create-react-app.

Create a React project with create-react-app on your laptop. Call it todolist.

Go into that new folder and test that the starter app works:

$ cd todolist
$ npm start

npm start opens a browser tab for you and shows the starter app. See the spinning React logo? Now look in the terminal window: you don’t have a prompt, because there is a watcher running.

Note the message in the running app that says “Edit src/App.js and save to reload.” Let’s do that! Bring up your favorite text editor, load in your todos folder, locate App.js, and change the message. Save the file, then note the change in the browser! The watcher did that. (If the watcher ever stops, start it up again with npm start)

There’s a ton of stuff we don’t need in the starter code, so let’s get it out of our way:

Save everything and look at the browser window. Hopefully you don’t see any errors, and you just see the text you replaced the spinning logo with. If your browser window is showing errors, refresh the page. If that doesn’t work, check your code for typos and other problems.

Step 2: Start using Git and GitHub

Commit those changes to your local repo. Yes, your local repo! create-react-app already initialized a git repo in your project folder, so:

$ git add .
$ git commit -m 'Remove extra create-react-app files'

At this point, it’s educational to enter:

$ git log
You should see two commits, right?

Connect the project to GitHub. At GitHub, create a new public repository called todos BUT DO NOT CHECK THE BOX THAT SAYS INITIALIZE THIS REPOSITORY WITH A README. DO NOT CHECK THAT BOX. Only enter the repository name and maybe a description. Create-react-app has already made your .gitignore, so don’t touch that either. Clicking on the Create repository button will create your repo and leave you at an instructions page. See the part where it says “…or push an existing repository from the command line”? Copy those three lines and paste them into your terminal (you should be in your project directory still):

$ git remote add origin git@github.com:YOURGITHUBNAMEHERE/todos.git
$ git branch -M main
$ git push -u origin main

Refresh your repository page at GitHub and note your two commits.

Step 3: Let’s get programming

Before writing code, let’s sketch out the architecture of our app. React is all about components. We will want three components:

todoappcomponents.png

The app component holds the actual list of todos in its state. This will be passed as a prop to the todo-list component which will render them, and respond to clicks on the items by deleting them. The main component has to pass the deleter function as a prop. The todo-entry-form component will hold as its state whatever a user is typing, and calls its passed-in adder function to add to the app’s state. To get started with all this, edit src/App.js to look like this:

import { useState } from 'react';

export default function App() {
  const [todos, setTodos] = useState([
    {id: 1, content: 'Meet Masao for Udon'},
    {id: 2, content: 'Fix up these horrible React notes'},
  ]);
  return (
    <div className="App">
      <h1>Things To Do</h1>
    </div>
  );
}

The React way is to keep the state high up in the tree and pass it down to subcomponents that will do the rendering. Create the file src/TodoList.js like so:

export default function TodoList({ todos }) {
  const todoList = todos.length ? (
    todos.map(todo => <div key={todo.id}>{todo.content}</div>)
  ) : (
    <p>No todos left</p>
  );
  return <div>{todoList}</div>;
}

We don’t see anything yet, because we haven’t actually added a TodoList component to the App component. We set up the TodoList to accept a list called todos in its props. So when we define the component in App.js we can pass it the todos. In App.js enter this line at the end of the import section:

import TodoList from './TodoList'
and this line after the h1 element:
<TodoList todos={todos} />

Look at the browser page now. You should see your todos!

Let’s add delete functionality. We want that when we click on a todo, it is deleted. In React, we just have to change the state and the page will re-render. But you can only change the state by calling the setter function! Do not (repeat DO NOT) try to change the value that the getter gave you. One way to do this deletion is for App to pass setTodos as a prop, but this gives the TodoList component too much power. Instead, we create a function in App called deleteTodo and pass just that down as a prop. Edit the components like so:

export default function TodoList({ todos, deleter }) {
  const todoList = todos.length ? (
    todos.map(todo => {
      return (
        <div key={todo.id}>
          <span onClick={() => deleter(todo.id)}>{todo.content}</span>
        </div>
      );
    })
  ) : (
    <p>No todos left</p>
  );
  return <div>{todoList}</div>;
}
import { useState } from 'react';
import TodoList from './TodoList'

export default function App() {
  const [todos, setTodos] = useState([
    {id: 1, content: 'Meet Masao for Udon'},
    {id: 2, content: 'Fix up these horrible React notes'},
  ]);
  function deleteTodo(id) {
    setTodos(todos => todos.filter(todo => todo.id !== id));
  }
  return (
    <div className="App">
      <h1>Things To Do</h1>
      <TodoList todos={todos} deleter={deleteTodo} />
    </div>
  );
}

It might be a good time to commit and push now. You know the drill, right:

$ git add .
$ git commit -m 'Add the delete functionality'
$ git push

Good thing we used the -u on the first push, so now we can just say git push. (Oh, are you doing your git stuff in a different terminal window? Cool if you are. If not, just remember to npm start again.)

Step 4: Review some technical terminology

Components, State, and Props

Let’s not be to hand-wavy about this stuff. There are some important things worth knowing here:
  • Components can have state and/or props or neither.
  • Props are read-only. They are passed to the component.
  • State is mutable, but there are rules you must follow to mutate properly:
    • NEVER modify state directly, use the setters!
    • If you need to set the state based off of the current state, pass a function to your setter that takes in the current state. (You can find the reasons why in the React documentation; for now, just do it.)
This all gets easier the more you practice. You will see these concepts take shape in your mind, and become second nature, over time.

Step 5: Forms!

Let’s make a component with a form to add a todo. This state of this component will be just the value being typed in the input box; it updates whenever the input box changes. When the form is submitted, we’ll update the todo list in App. How? App will pass a function to the form, as a prop, to update App’s state. Takes getting used to, perhaps, but it’s so good. Here is the new component src/TodoEntryForm.js:

import { useState } from 'react';

export default function TodoEntryForm( { adder }) {
  const [content, setContent] = useState('');
  function submit(e) {
    e.preventDefault();
    adder({ id: Math.random(), content });
    setContent('');
  }
  return (
    <form onSubmit={submit}>
      <input value={content} onChange={e => setContent(e.target.value)} />
    </form>
  )
}

And here are the additions to src/App.js (you should know where they go by now!):

import TodoEntryForm from './TodoEntryForm';
function addTodo(todo) {
  setTodos(todos => [...todos, todo]);
}
<TodoEntryForm adder={addTodo} />
Add, commit, push.

Do some user testing. You might find this bug: If you hit Enter with an empty todo in your form, you’ll actually have an empty string todo in your list, but it renders as nothing, and you can’t get your cursor over it to delete it and you’ll never see the “No todos left” ever again. We need to prevent empty todos:

function submit(e) {
  e.preventDefault();
  if (content.trim()) {
    adder({ id: Math.random(), content });
  }
  setContent('');
}
Add, commit, push.

Step 6: Get the keys right!

Wait, wait, Math.random() is a silly hack for ids! It isn’t good enough. What can we use instead? We can use uuids. They’re not built in to JavaScript, but we can install a package and use them:

$ npm install uuid
In the entry form component, import:
import uuidv4 from 'uuid/v4';
and use it:
adder({ id: uuidv4(), content });
Add, commit, push.

Step 7: Tests!

Oh did we forget about those tests? If you did not do so already, go into src/App.test.js and fix up that sole starter test to actually look for the title we put into our app, and not the old “Learn React” test:

test('renders the page heading', () => {
  const { getByText } = render();
  const linkElement = getByText(/Things to do/i);
  expect(linkElement).toBeInTheDocument();
});

In a separate terminal window, go to the project folder and enter:

$ npm test

Select a from the menu to run all tests. Also, look at the options: there’s a way to get a test watcher. There’s so much more to testing that it needs to be covered elsewhere. For now, we’re going to move on to styling.

Step 8: Style it up

There are different ways we can do styling in React:

Let’s go all in with Material UI. Start by bringing it in to the project:
$ npm install @material-ui/core
  • Material UI is a rather large library, so rather than explaining everything on these notes, we’ll just present the whole updated application. It is not perfect by any means, but enough to show off what Material UI apps look like. There are plenty of resources for learning Material UI on the web (like this one and this one), so happy searching.

    App.js
    import { useState } from 'react';
    import Paper from '@material-ui/core/Paper'
    import Container from '@material-ui/core/Container'
    import Typography from '@material-ui/core/Typography'
    import TodoList from './TodoList'
    import TodoEntryForm from './TodoEntryForm';
    
    export default function App() {
      const [todos, setTodos] = useState([
        {id: 1, content: 'Meet Masao for Udon'},
        {id: 2, content: 'Fix up these horrible React notes'},
      ]);
      function deleteTodo(id) {
        setTodos(todos => todos.filter(todo => todo.id !== id));
      }
      function addTodo(todo) {
        setTodos(todos => [...todos, todo]);
      }
      return (
        <Container maxWidth="sm" style={{marginTop: 20}}>
          <Paper style={{padding: 20, backgroundColor: '#fffff3'}}>
            <Typography variant="h3" align="center">Things To Do</Typography>
            <TodoList todos={todos} deleter={deleteTodo}/>
            <TodoEntryForm adder={addTodo} />
          </Paper>
        </Container>
      );
    }
    
    TodoList.js
    import { Fragment } from 'react';
    import List from '@material-ui/core/List';
    import ListItem from '@material-ui/core/ListItem';
    import Divider from '@material-ui/core/Divider';
    
    export default function TodoList({ todos, deleter }) {
      const todoList = todos.length ? (
        todos.map(todo => {
          return (
            <Fragment key={todo.id}>
              <ListItem>
                <span onClick={() => deleter(todo.id)}>{todo.content}</span>
              </ListItem>
              <Divider />
            </Fragment>
          );
        })
      ) : (
        <p>No todos left</p>
      );
      return <List style={{marginTop: 20}}>{todoList}</List>;
    }
    
    TodoEntryForm.js
    import { useState } from 'react';
    import TextField from '@material-ui/core/TextField';
    import uuidv4 from 'uuid/v4';
    
    export default function TodoEntryForm( { adder }) {
      const [content, setContent] = useState('');
      function submit(e) {
        e.preventDefault();
        if (content.trim()) {
          adder({ id: uuidv4(), content });
        }
        setContent('');
      }
      return (
        <form onSubmit={submit}>
          <TextField
            label="Add Another"
            value={content}
            onChange={e => setContent(e.target.value)}
            margin="normal"
          />
        </form>
      )
    }
    
    Here’s how this first pass looks:

    todoscreenshot.png

    Much more can be done. Don’t make the mistake of thinking this little app is perfect or even beautiful.