Table of Contents
This post continues our look at James Vickery’s simple-mern repository. Today we’ll look at the React frontend, under the client/
folder.
package.json
We’ll ignore package-lock.json
, since it was covered previously.
package.json
looks very similar to what create-react-app
spits out. There are some new options, though, so let’s look at them.
{
"proxy": "http://localhost:5000/",
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
These are all development-only options, in a sense:
proxy
sets the root path of your application in development mode. So with this setting, in development mode,/
resolves tolocalhost:5000/
, but in production it’ll resolve to the actual URL your app is being served from, e.g.,https://yoursite.github.io/
.eslintConfig
is for theeslint
utility, which is a highly configurable linter, similar topylint
. The optionextends
tellseslint
to use theeslint-config-react-app
preset by default. These are just sane default coding standards for a React app.browserslist
is used by a few different packages, most prominentlybabel
, to keep track of necessary polyfills. The options in that array denote the criteria for a particular browser to be supported. They are fairly self-explanatory;ie
is Internet Explorer andop_mini
is Opera Mini.
Let’s also look at the dependencies
and scripts
:
{
"dependencies": {
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-scripts": "2.1.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
The only dependencies we have right now are
react
, which is of course our frontend framework,react-dom
, which allows us to insert React components directly into the DOM, andreact-scripts
, which is a collection of scripts that comes withcreate-react-app
. The options explain themselves:start
starts the server,build
builds your app, andtest
runs the test harness.eject
is how you get out of thecreate-react-app
ecosystem.
In fact, create-react-app
is hiding quite a lot of dependencies from us, and running npm run eject
will show them. It’ll also reveal the Webpack configuration, some Jest configuration, and a host of other things. For now we’ll leave all this in the box, though.
src/
App.css, index.css
This is just CSS. I’ll ignore it for now. index.css
in particular is autogenerated by create-react-app
.
index.js
This is the first file compiled, and all it does is tell react-dom
to render the App
component, so there’s not much to see here.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
App.js
Here we get the real meat of the application. First, the imports.
import React, { useState, useEffect, useCallback } from 'react';
import './App.css';
import TasksList from './components/TaskList';
The remainder of the application is a React component, written as a function. The more traditional style is to write this as a class that extends React.Component
.
(Talk some about how these styles differ – hooks vs state.)
const App = () => {
...
};
Within this component, the first thing we encounter is some app-level state. This is implemented through React’s useState
hook, which returns a getter and setter for a state field. We just pass in the field’s initial value.
const [tasks, setTasks] = useState([]);
const [newTaskTitle, setNewTaskTitle] = useState('');
Next we encounter a React effect hook. This specifies something that will happen when the component loads. The name effect is intended to evoke side effect.
const getTasks = useCallback(() => {
fetch('/api/tasks')
.then(res => res.json())
.then(setTasks);
});
useEffect(() => {
getTasks();
}, []);
The effect getTasks
runs an API call to pull the task list from Mongo. We saw the meat of this API call in the backend post. Then it sets the task list inside the app.
The next thing we come to is an event handler.
const clickAddTask = event => {
event.preventDefault();
fetch('/api/tasks/add', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTaskTitle }),
}).then(() => {
setNewTaskTitle('');
getTasks();
});
};
As we’ll see below, and as the name implies, this fires upon clicking the “Add Task” button.
Ordinarily, clicking on the submit button would reload the page; we call event.preventDefault()
to stop this from happening.
Then we send an API call to the server to actually add the task to Mongo. Having done that, we reset the value of the title box (setNewTaskTitle('')
) and update the task list (getTasks()
.)
Finally, we have the component’s actual HTML structure. This would appear under the render()
method in a component class.
return (
<div className="App">
<h1>My Tasks</h1>
<TasksList tasks={tasks} updateTasks={getTasks} />
<form onSubmit={clickAddTask}>
<input
type="text"
size="30"
placeholder="New Task"
value={newTaskTitle}
onChange={event => setNewTaskTitle(event.target.value)}
/>
<input className="btn-primary" type="submit" value="Add" />
</form>
</div>
);
The form structure is nothing new, although some keywords (className
, for one) have been changed to avoid collision with JavaScript reserved words.
The interesting part is the TasksList
line:
<TasksList tasks={tasks} updateTasks={getTasks} />
This renders a TaskList
component with two properties. We’ll see in a moment where these properties are used.
components/TaskList.js
This file specifies the structure of the TaskList
component. There are two component methods and the HTML structure to look at, but first let’s examine the signature.
const TaskList = ({ tasks, updateTasks }) => [...]
If this component were a class, its constructor would take an optional props
argument containing component-level properties. These properties, tasks
and updateTasks
, are what we just saw above.
updateTasks
is a function, so thinking of it as a property may be strange. It’s set up this way because updateTasks
is defined along with tasks
at the application level. It may be a poor architectural decision, but for an app of this size it’s probably not worth worrying about.
Let’s look at the component methods.
const clickDeleteTask = (event, task) => {
event.preventDefault();
fetch(`/api/tasks/delete/${task._id}`, {
method: 'delete',
})
.then(res => res.json())
.then(() => updateTasks());
};
This is an event handler. Nothing new here; this simply stops anything that would normally happen when you click on the trigger, runs an API call to delete the passed task instead, and updates the task list.
const toggleDone = task => {
fetch(`/api/tasks/update/${task._id}`, {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ done: !task.done }),
}).then(() => updateTasks());
};
More of the same here. API call to update the task, then update the task list.
Now let’s check out the HTML structure.
return (
<ul className="tasks">
{tasks.map(task => (
<li key={task._id}>
<label className={task.done ? 'done' : ''}>
<input
type="checkbox"
checked={task.done}
onChange={() => toggleDone(task)}
/>{' '}
{task.title}
<svg
onClick={event => clickDeleteTask(event, task)}
className="delete-button"
[...]
>
[...]
</svg>
</label>
</li>
))}
</ul>
);
This simply renders a <li>
for each task containing a toggle box, a title, and an SVG (whose fiddly details I have omitted) that functions as the delete button. (It renders to an X.)
There’s only one new feature to discuss:
<li key={task._id}>
A React key is just what it sounds like: a unique identifier. (For this task item, we just use the unique ID that Mongo assigns it.) Without them, we’d have to rerender the entire component every time one of the tasks changed. With them, React will search for the key and rerender only the item that matches.
src/public/
This contains some files autogenerated by create-react-app
. index.js
plugs in here. Otherwise there’s not much to say.