a
Rendering a collection, modules
Before starting a new topic, let's recap some of the topics that proved difficult last year.
console.log
What's the difference between an experienced JavaScript programmer and a rookie? The experienced one uses console.log 10-100 times more.
Paradoxically, this seems to be true even though a rookie programmer would need console.log (or any debugging method) more than an experienced one.
When something does not work, don't just guess what's wrong. Instead, log or use some other way of debugging.
NB when you use the command console.log for debugging, don't concatenate things 'the Java way' with a plus. Instead of writing:
console.log('props value is' + props)
separate the things to be printed with a comma:
console.log('props value is', props)
If you concatenate an object with a string and log it to the console (like in our first example), the result will be pretty useless:
props value is [Object object]
On the contrary, when you pass objects as distinct arguments separated by commas to console.log, like in our second example above, the content of the object is printed to the developer console as strings that are insightful. If necessary, read more about debugging React-applications here.
Protip: Visual Studio Code snippets
With Visual studio code it's easy to create 'snippets', i.e. shortcuts for quickly generating commonly re-used portions of code, much like how 'sout' works in Netbeans. Instructions for creating snippets can be found here.
Useful, ready-made snippets can also be found as VS Code plugins, for example here.
The most important snippet is the one for the console.log() command, for example clog. This can be created like so:
{
"console.log": {
"prefix": "clog",
"body": [
"console.log('$1')",
],
"description": "Log output to console"
}
}
JavaScript Arrays
From here on out, we will be using the functional programming methods of the JavaScript array, such as find, filter, and map - all of the time. They operate on the same general principles as streams do in Java 8, which have been used during the last few years in both the 'Ohjelmoinnin perusteet' and 'Ohjelmoinnin jatkokurssi' courses at the university's department of Computer Science, and also in the programming MOOC.
If functional programming with arrays feels foreign to you, it is worth watching at least the first three parts of the YouTube video series Functional Programming in JavaScript:
Event handlers revisited
Based on last year's course, event handling has proven to be difficult. It's worth reading the revision chapter at the end of the previous part event handlers revisited, if it feels like your own knowledge on the topic needs some brushing up.
Passing event handlers to the child components of the App component has raised some questions. A small revision on the topic can be found here.
Rendering collections
We will now do the 'frontend', or the browser-side application logic, in React for an application that's similar to the example application from part 0
Let's start with the following:
import React from 'react'
import ReactDOM from 'react-dom'
const notes = [
{
id: 1,
content: 'HTML is easy',
date: '2019-05-30T17:30:31.098Z',
important: true
},
{
id: 2,
content: 'Browser can execute only Javascript',
date: '2019-05-30T18:39:34.091Z',
important: false
},
{
id: 3,
content: 'GET and POST are the most important methods of HTTP protocol',
date: '2019-05-30T19:20:14.298Z',
important: true
}
]
const App = (props) => {
const { notes } = props
return (
<div>
<h1>Notes</h1>
<ul>
<li>{notes[0].content}</li>
<li>{notes[1].content}</li>
<li>{notes[2].content}</li>
</ul>
</div>
)
}
ReactDOM.render(
<App notes={notes} />,
document.getElementById('root')
)
Every note contains its textual content and a timestamp as well as a boolean value for marking whether the note has been categorized as important or not, and also a unique id.
The code functions due to the fact that there are exactly three notes in the array. A single note is rendered by accessing the objects in the array by referring to a hard-coded index number:
<li>{note[1].content}</li>
This is, of course, not practical. The solution can be made general by generating React-elements from the array objects using the map function.
notes.map(note => <li>{note.content}</li>)
The result is an array of li elements.
[
'<li>HTML is easy</li>',
'<li>Browser can execute only Javascript</li>',
'<li>GET and POST are the most important methods of HTTP protocol</li>',
]
Which can then be put inside ul tags:
const App = (props) => {
const { notes } = props
return (
<div>
<h1>Notes</h1>
<ul> {notes.map(note => <li>{note.content}</li>)} </ul> </div>
)
}
Because the code generating the li tags is JavaScript, it must be wrapped in curly braces in a JSX template just like all other JavaScript code.
It's common in similar situations for the dynamically-generated content to be separated into its own method, which the JSX template calls:
const App = (props) => {
const { notes } = props
const rows = () => notes.map(note => <li>{note.content}</li>)
return (
<div>
<h1>Notes</h1>
<ul>
{rows()} </ul>
</div>
)
}
Key-attribute
Even though the application seems to be working, there is a nasty warning on the console:
As the linked page in the error message instructs, the list items, i.e. the elements generated by the map method, must each have a unique key value: an attribute called key.
Let's add the keys:
const App = (props) => {
const { notes } = props
const rows = () => notes.map(note => <li key={note.id}>{note.content}</li>)
return (
<div>
<h1>Notes</h1>
<ul>
{rows()}
</ul>
</div>
)
}
And the error message disappears.
React uses the key attributes of objects in an array to determine how to update the view generated by a component when the component is re-rendered. More about this here.
Map
Understanding how the array method map works is crucial for the rest of the course.
The application contains an array called notes
const notes = [
{
id: 1,
content: 'HTML is easy',
date: '2019-05-30T17:30:31.098Z',
important: true
},
{
id: 2,
content: 'Browser can execute only Javascript',
date: '2019-05-30T18:39:34.091Z',
important: false
},
{
id: 3,
content: 'GET and POST are the most important methods of HTTP protocol',
date: '2019-05-30T19:20:14.298Z',
important: true
}
]
Let's pause for a moment and examine how map works.
If the following code is added to, let's say, the end of the file:
const result = notes.map(note => note.id)
console.log(result)
[1, 2, 3] will be printed to the console. Map always creates a new array, the elements of which have been created from the elements of the original array by mapping, using the function given as a parameter to the map method.
The function is
note => note.id
Which is an arrow function written in compact form. The full form would be:
(note) => {
return note.id
}
The function gets a note object as a parameter, and returns the value of its id field.
Changing the command to:
const result = notes.map(note => note.content)
results in an array containing the contents of the notes.
This is already pretty close to the React code we used:
notes.map(note => <li key={note.id}>{note.content}</li>)
which generates a li tag containing the contents of the note from each note object.
Because the function parameter of the map method
note => <li key={note.id}>{note.content}</li>
is used to create view elements, the value of the variable must be rendered inside of curly braces. Try to see what happens if the braces are removed.
The use of curly braces will cause some headache in the beginning, but you will get used to them soon enough. The visual feedback from React is immediate.
Better formatting for the helper function returning the rows of notes in our application could be the following version spread over multiple lines:
const rows = () => notes.map(note =>
<li key={note.id}>
{note.content}
</li>
)
This still is an arrow function with only one statement, the statement just happens to be a bit more complicated.
Anti-pattern: array indexes as keys
We could have made the error message on our console disappear by using the array indexes as keys. The indexes can be retrieved by passing a second parameter to the map-method:
notes.map((note, i) => ...)
When called like this, i is assigned the value of the index of the position in the array where the Note resides.
As such, one way to define the row generation without getting errors is:
const rows = () => notes.map((note, i) =>
<li key={i}>
{note.content}
</li>
)
This is, however, not recommended and can cause undesired problems even if it seems to be working just fine. Read more from here.
Refactoring modules
Let's tidy the code up a bit. We are only interested in the field notes of the props, so let's retrieve that directly using destructuring:
const App = ({ notes }) => { // ...
return (
<div>
<h1>Notes</h1>
<ul>
{rows()}
</ul>
</div>
)
}
If you have forgotten what destructuring means and how it works, review this.
We'll separate displaying a single note into its own component Note:
const Note = ({ note }) => { return ( <li>{note.content}</li> )}
const App = ({ notes }) => {
const rows = () => notes.map(note =>
<Note key={note.id} note={note} /> )
return (
<div>
<h1>Notes</h1>
<ul>
{rows()}
</ul>
</div>
)
}
Note, that the key attribute must now be defined for the Note components, and not for the li tags like before.
A whole React application can be written on a single file. Although that is, of course, not very practical. Common practice is to declare each component in their own file as an ES6-module.
We have been using modules the whole time. The first few lines of the file:
import React from 'react'
import ReactDOM from 'react-dom'
imports two modules, enabling them to be used in the code. The react module is placed into a variable called React and react-dom to variable ReactDOM.
Let's move our Note component into its own module.
In smaller applications, components are usually placed in a directory called components , which is in turn placed within the src directory. The convention is to name the file after the component.
Now we'll create a directory called components for our application and place a file named Note.js inside. The contents of the Note.js file are as follows:
import React from 'react'
const Note = ({ note }) => {
return (
<li>{note.content}</li>
)
}
export default Note
Because this is a React-component, we must import React.
The last line of the module exports the declared module, the variable Note.
Now the file using the component, index.js, can import the module:
import React from 'react'
import ReactDOM from 'react-dom'
import Note from './components/Note'
const App = ({ notes }) => {
// ...
}
The component exported by the module is now available for use in the variable Note, just as it was earlier.
Note, that when importing our own components their location must be given in relation to the importing file:
'./components/Note'
The period in the beginning refers to the current directory, so the module's location is a file called Note.js in a sub-directory of the current components. directory. The filename extension can be omitted.
App is a component as well, so let's declare it in its own module as well. Since it is the root component of the application, we'll place it in the src directory. The contents of the file are as follows:
import React from 'react'
import Note from './components/Note'
const App = ({ notes }) => {
const rows = () => notes.map(note =>
<Note
key={note.id}
note={note}
/>
)
return (
<div>
<h1>Notes</h1>
<ul>
{rows()}
</ul>
</div>
)
}
export default App
What's left in the index.js file is:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
const notes = [
// ...
]
ReactDOM.render(
<App notes={notes} />,
document.getElementById('root')
)
Modules have plenty of other uses other than enabling component declarations to be separated into their own files. We will get back into them later in this course.
The current code of the application can be found on GitHub.
Note that the master branch of the repository contains the code for a later version of the application. The current code is in the branch part2-1:
If you clone the project, run the command npm install before starting the application with npm start.
When the application breaks
Early in your programming career (and even after 30 years of coding like yours truly), what often happens is that the application just completely breaks down. This is even more the case with dynamically typed languages, such as JavaScript, where the compiler does not check the data type of, for instance, function variables or return values.
A "React explosion" can for example look like this:
In these situations your best way out is the console.log. The piece of code causing the explosion is this:
const Course = ({ course }) => (
<div>
<Header course={course} />
</div>
)
const App = () => {
const course = {
// ...
}
return (
<div>
<Course course={course} />
</div>
)
}
We'll hone in on the reason for the breakdown by adding console.log commands to the code. Because the first thing to be rendered is the App component, it's worth putting the first console.log there:
const App = () => {
const course = {
// ...
}
console.log('App toimii...')
return (
// ..
)
}
To see the printing on the console, we must scroll up over the long red wall of errors.
When one thing is found to be working, it's time to log deeper. If the component has been declared as a single statement, or a function without a return, it makes printing to the console harder.
const Course = ({ course }) => (
<div>
<Header course={course} />
</div>
)
The component should be changed to its longer form, in order for us to add the printing:
const Course = ({ course }) => {
console.log(course) return (
<div>
<Header course={course} />
</div>
)
}
Quite often the root of the problem is that the props are expected to be of a different type, or called with a different name than they actually are, and destructuring fails as a result. The problem often begins to solve itself when destructuring is removed and we see what the props actually contains.
const Course = (props) => { console.log(props) const { course } = props
return (
<div>
<Header course={course} />
</div>
)
}
If the problem has still not been resolved, there really isn't much to do apart from continuing to bug-hunt by sprinkling more console.log statements around your code.
I added this chapter to the material after the model answer for the next question exploded completely (due to props being of the wrong type), and I had to debug it using console.log.