Dynamic HTML

6,926 words

Post contents

Previously, we learned how to create components for our file application. These components included a way to create a component tree, add inputs to each component to pass data, and add an output of data back to a parent component.

Where we last left off, we manually input a list of files, which included file names and dates inside a button. Let's take a look back at our existing file component to start:

const File = ({ href, fileName, isSelected, onSelected }) => {	// `href` is temporarily unused	return (		<button			onClick={onSelected}			style={				isSelected					? { backgroundColor: "blue", color: "white" }					: { backgroundColor: "white", color: "blue" }			}		>			{fileName}			<FileDate inputDate={new Date()} />		</button>	);};

React Outputs - StackBlitz

Edit

Files

  • src
import { createRoot } from "react-dom/client";import { useState } from "react";const FileDate = ({ inputDate }) => {	const [dateStr, setDateStr] = useState(formatDate(inputDate));	const [labelText, setLabelText] = useState(formatReadableDate(inputDate));	return <span aria-label={labelText}>{dateStr}</span>;};const File = ({ href, fileName, isSelected, onSelected }) => {	// `href` is temporarily unused	return (		<button			onClick={onSelected}			style={				isSelected					? { backgroundColor: "blue", color: "white" }					: { backgroundColor: "white", color: "blue" }			}		>			{fileName}			<FileDate inputDate={new Date()} />		</button>	);};const FileList = () => {	const [selectedIndex, setSelectedIndex] = useState(-1);	const onSelected = (idx) => {		if (selectedIndex === idx) {			setSelectedIndex(-1);			return;		}		setSelectedIndex(idx);	};	return (		<ul>			<li>				<File					isSelected={selectedIndex === 0}					onSelected={() => onSelected(0)}					fileName="File one"					href="/file/file_one"				/>			</li>			<li>				<File					isSelected={selectedIndex === 1}					onSelected={() => onSelected(1)}					fileName="File two"					href="/file/file_two"				/>			</li>			<li>				<File					isSelected={selectedIndex === 2}					onSelected={() => onSelected(2)}					fileName="File three"					href="/file/file_three"				/>			</li>		</ul>	);};createRoot(document.getElementById("root")).render(<FileList />);function formatDate(inputDate) {	// Month starts at 0, annoyingly	const month = inputDate.getMonth() + 1;	const date = inputDate.getDate();	const year = inputDate.getFullYear();	return month + "/" + date + "/" + year;}function formatReadableDate(inputDate) {	const months = [		"January",		"February",		"March",		"April",		"May",		"June",		"July",		"August",		"September",		"October",		"November",		"December",	];	const monthStr = months[inputDate.getMonth()];	const dateSuffixStr = dateSuffix(inputDate.getDate());	const yearNum = inputDate.getFullYear();	return monthStr + " " + dateSuffixStr + "," + yearNum;}function dateSuffix(dayNumber) {	const lastDigit = dayNumber % 10;	if (lastDigit == 1 && dayNumber != 11) {		return dayNumber + "st";	}	if (lastDigit == 2 && dayNumber != 12) {		return dayNumber + "nd";	}	if (lastDigit == 3 && dayNumber != 13) {		return dayNumber + "rd";	}	return dayNumber + "th";}

This is a strong basis for a component without needing many changes.

We would love to add the ability to see folders listed alongside files. While we could - and arguably should - add in a component that copies/pastes the code from the File component to create a new Folder component, let's reuse what we already have!

To do this, we'll create a new property called isFolder, which hides the date when set to true.

Conditional Rendering

One way we can hide the date from displaying the user is by reusing an HTML attribute we introduced in the last chapter's challenge: hidden.

<div hidden="true">	<!-- This won't display to the user -->	<FileDate /></div>

This works but introduces a potential problem; while the contents are not shown to the user (and are similarly hidden from screen-readers), they are still present within the DOM.

This means that a large number of these HTML elements marked as hidden will still be in the DOM. They can still impact performance and memory usage as if they were being displayed to the user.

This might sound counterintuitive at first, but in-memory, non-displayed UI elements have their place; they're particularly useful when building animation systems that visually transition items in and out of view.

To sidestep these performance concerns, React, Angular, and Vue all have a method to "conditionally render" HTML elements based off of a boolean. This means that if you pass false, it will entirely remove the child HTML elements from the DOM.

Let's see what that looks like in usage:

const ConditionalRender = ({ bool }) => {	return <div>{bool && <p>Text here</p>}</div>;};

React Conditional Render - StackBlitz

Edit

Files

  • src
import { createRoot } from "react-dom/client";const ConditionalRender = ({ bool }) => {	return <div>{bool && <p>Text here</p>}</div>;};const App = () => {	return (		<div>			<h1>Shown contents</h1>			<ConditionalRender bool={true} />			<h1>Hidden contents</h1>			<ConditionalRender bool={false} />		</div>	);};createRoot(document.getElementById("root")).render(<App />);

We're using React's {} JavaScript binding to add an AND statement. This works by using Boolean logic of "short-circuiting". This means that if we have:

const val = true && {};

val will be set to {}, while if we have:

const val = false && {};

val will be set to false.

React then uses this return value to render the value when the condition inside the curly braces is not undefined or null.

This means that these examples will render their contained values:

<div>{0}</div><div>{"Hello"}</div><div>{true && <Comp/>}</div><div>{true}</div>// Renders as<div>0</div><div>Hello</div><div><Comp/></div><div>true</div>

But the following examples will not render their contained values:

<div>{undefined}</div><div>{false}</div>// Both render as<div></div>

In this example, when we pass bool as true, the component's HTML is rendered as:

<div><p>Text here</p></div>

But when bool is set to false, it instead renders the following HTML:

<div></div>

This is possible because React, Angular, and Vue control what is rendered on the screen. Using this, they can remove or add HTML rendered to the DOM with nothing more than a boolean instruction.

Knowing this, let's add conditional rendering to our application.

Conditional Rendering Our Date

Right now, we have a list of files to present to the user. However, if we look back at our mockups, we'll notice that we wanted to list folders alongside files.

A list of directory contents with files and folders listed alongside one another.

Luckily for us, our File component already manages much of the behavior we'd like to have with a potential Folder component as well. For example, just like files, we want to select a folder when the user has clicked on it so that we can select multiple files and folders at once.

However, unlike files, folders do not have a creation date since there may be ambiguity of what the "Last modified" date would mean for a folder. Is the last modified date when the folder was renamed? Or was it when a file within said folder was last modified? It's unclear, so we'll axe it.

Despite this difference in functionality, we can still reuse our File component for folders as well. We can reuse this component by conditionally rendering the date if we know we're showing a folder instead of a file.

Let's add an input to our File component called isFolder and prevent the date from rendering if said input is set to `true``.

const File = ({ href, fileName, isSelected, onSelected, isFolder }) => {	return (		<button			onClick={onSelected}			style={				isSelected					? { backgroundColor: "blue", color: "white" }					: { backgroundColor: "white", color: "blue" }			}		>			{fileName}			{!isFolder && <FileDate inputDate={new Date()} />}		</button>	);};const FileList = () => {	return (		<ul>			<li>				<File fileName="File one" href="/file/file_one" />			</li>			<li>				<File fileName="Folder one" href="/file/folder_one/" isFolder={true} />			</li>		</ul>	);};

React Conditional Date - StackBlitz

Edit

Files

  • src
import { createRoot } from "react-dom/client";import { useState } from "react";const FileDate = ({ inputDate }) => {	const [dateStr, setDateStr] = useState(formatDate(inputDate));	const [labelText, setLabelText] = useState(formatReadableDate(inputDate));	return <span aria-label={labelText}>{dateStr}</span>;};const File = ({ href, fileName, isSelected, onSelected, isFolder }) => {	return (		<button			onClick={onSelected}			style={				isSelected					? { backgroundColor: "blue", color: "white" }					: { backgroundColor: "white", color: "blue" }			}		>			{fileName}			{!isFolder && <FileDate inputDate={new Date()} />}		</button>	);};const FileList = () => {	return (		<ul>			<li>				<File fileName="File one" href="/file/file_one" />			</li>			<li>				<File fileName="Folder one" href="/file/folder_one/" isFolder={true} />			</li>		</ul>	);};createRoot(document.getElementById("root")).render(<FileList />);function formatDate(inputDate) {	// Month starts at 0, annoyingly	const month = inputDate.getMonth() + 1;	const date = inputDate.getDate();	const year = inputDate.getFullYear();	return month + "/" + date + "/" + year;}function formatReadableDate(inputDate) {	const months = [		"January",		"February",		"March",		"April",		"May",		"June",		"July",		"August",		"September",		"October",		"November",		"December",	];	const monthStr = months[inputDate.getMonth()];	const dateSuffixStr = dateSuffix(inputDate.getDate());	const yearNum = inputDate.getFullYear();	return monthStr + " " + dateSuffixStr + "," + yearNum;}function dateSuffix(dayNumber) {	const lastDigit = dayNumber % 10;	if (lastDigit == 1 && dayNumber != 11) {		return dayNumber + "st";	}	if (lastDigit == 2 && dayNumber != 12) {		return dayNumber + "nd";	}	if (lastDigit == 3 && dayNumber != 13) {		return dayNumber + "rd";	}	return dayNumber + "th";}

Conditional Branches

We're now able to conditionally show the user the last modified date depending on the isFolder boolean. However, it may still be unclear to the user what is a folder and what is a file, as we don't have this information clearly displayed to the user yet.

Let's use conditional rendering to show the type of item displayed based on the isFolder boolean.

<div>	{isFolder && <span>Type: Folder</span>}	{!isFolder && <span>Type: File</span>}</div>

While working on this, it might become clear that we're effectively reconstructing an if ... else statement, similar to the following logic in JavaScript.

// This is pseudocode for the above using JavaScript as the syntaxif (isFolder) return "Type: Folder";else return "Type: File";

Like the JavaScript environment these frameworks run in, they also implement a similar else-style API for this exact purpose.

One of the benefits of React's JSX templating language is that you're able to embed JavaScript directly inside an element. This embedded JavaScript will then render the return value of the JavaScript inside.

For example, we can use a JavaScript ternary to return a different value if a boolean is true or false:

// Will show "Folder" if `isFolder` is true, otherwise show "File"const displayType = isFolder ? "Folder" : "File";

We can combine this information with JSX's ability to treat a tag as a value you can assign to memory to create a if...else-style render in React:

<div>{isFolder ? <span>Type: Folder</span> : <span>Type: File</span>}</div>

React Conditional Branches - StackBlitz

Edit

Files

  • src
import { createRoot } from "react-dom/client";import { useState } from "react";const FileDate = ({ inputDate }) => {	const [dateStr, setDateStr] = useState(formatDate(inputDate));	const [labelText, setLabelText] = useState(formatReadableDate(inputDate));	return <span aria-label={labelText}>{dateStr}</span>;};const File = ({ href, fileName, isSelected, onSelected, isFolder }) => {	return (		<button			onClick={onSelected}			style={				isSelected					? { backgroundColor: "blue", color: "white" }					: { backgroundColor: "white", color: "blue" }			}		>			{fileName}			{isFolder ? <span>Type: Folder</span> : <span>Type: File</span>}			{!isFolder && <FileDate inputDate={new Date()} />}		</button>	);};const FileList = () => {	return (		<ul>			<li>				<File fileName="File one" href="/file/file_one" />			</li>			<li>				<File fileName="Folder one" href="/file/folder_one/" isFolder={true} />			</li>		</ul>	);};createRoot(document.getElementById("root")).render(<FileList />);function formatDate(inputDate) {	// Month starts at 0, annoyingly	const month = inputDate.getMonth() + 1;	const date = inputDate.getDate();	const year = inputDate.getFullYear();	return month + "/" + date + "/" + year;}function formatReadableDate(inputDate) {	const months = [		"January",		"February",		"March",		"April",		"May",		"June",		"July",		"August",		"September",		"October",		"November",		"December",	];	const monthStr = months[inputDate.getMonth()];	const dateSuffixStr = dateSuffix(inputDate.getDate());	const yearNum = inputDate.getFullYear();	return monthStr + " " + dateSuffixStr + "," + yearNum;}function dateSuffix(dayNumber) {	const lastDigit = dayNumber % 10;	if (lastDigit == 1 && dayNumber != 11) {		return dayNumber + "st";	}	if (lastDigit == 2 && dayNumber != 12) {		return dayNumber + "nd";	}	if (lastDigit == 3 && dayNumber != 13) {		return dayNumber + "rd";	}	return dayNumber + "th";}

Here, if isFolder is true, the following will be rendered:

<div><span>Type: Folder</span></div>

Otherwise, if isFolder is false, this will be rendered:

<div><span>Type: File</span></div>

Expanded Branches

While an if ... else works wonders if you only have a single Boolean value you need to check, you'll often need more than a single conditional branch to check against.

For example, what if we added an isImage Boolean to differentiate between images and other file types?

While we could move back to a simple if statement for each condition:

<div>	{isFolder && <span>Type: Folder</span>}	{!isFolder && isImage && <span>Type: Image</span>}	{!isFolder && !isImage && <span>Type: File</span>}</div>

This can get hard to read with multiple conditionals in a row. As a result, these frameworks have tools that you can use to make things a bit more readable.

We can chain together ternary operations to treat them as nested if statements.

By doing so, we can represent the following JavaScript pseudo-syntax:

function getType() {	// JavaScript	if (isFolder) {		return "Folder";	} else {		if (isImage) {			return "Image";		} else {			return "File";		}	}}

As the following React JSX

<div>	{isFolder ? (		<span>Type: Folder</span>	) : isImage ? (		<span>Type: Image</span>	) : (		<span>Type: File</span>	)}</div>

Rendering Lists

While we've primarily focused on improvements to our File component in this chapter, let's take another look at our original FileList component.

const FileList = () => {	const [selectedIndex, setSelectedIndex] = useState(-1);	const onSelected = (idx) => {		if (selectedIndex === idx) {			setSelectedIndex(-1);			return;		}		setSelectedIndex(idx);	};	return (		<ul>			<li>				<File					isSelected={selectedIndex === 0}					onSelected={() => onSelected(0)}					fileName="File one"					href="/file/file_one"					isFolder={false}				/>			</li>			<li>				<File					isSelected={selectedIndex === 1}					onSelected={() => onSelected(1)}					fileName="File two"					href="/file/file_two"					isFolder={false}				/>			</li>			<li>				<File					isSelected={selectedIndex === 2}					onSelected={() => onSelected(2)}					fileName="File three"					href="/file/file_three"					isFolder={false}				/>			</li>		</ul>	);};

Upon second glance, something that might immediately jump out at you is just how long these code samples are! Interestingly, this is primarily due to the copy-pasted nature of our File component being repeated.

What's more, this method of hard-coding file components means that we cannot create new files in JavaScript and display them in the DOM.

Let's fix that by replacing the copy-pasted components with a loop and an array.

React uses JavaScript's built-in Array.map method to loop through each item and map them to some React component.

const filesArray = [	{		fileName: "File one",		href: "/file/file_one",		isFolder: false,	},	{		fileName: "File two",		href: "/file/file_two",		isFolder: false,	},	{		fileName: "File three",		href: "/file/file_three",		isFolder: false,	},];const FileList = () => {	const [selectedIndex, setSelectedIndex] = useState(-1);	const onSelected = (idx) => {		if (selectedIndex === idx) {			setSelectedIndex(-1);			return;		}		setSelectedIndex(idx);	};	// This code sample is missing something and will throw a warning in development mode.	// We'll explain more about this later.	return (		<ul>			{filesArray.map((file, i) => (				<li>					<File						isSelected={selectedIndex === i}						onSelected={() => onSelected(i)}						fileName={file.fileName}						href={file.href}						isFolder={file.isFolder}					/>				</li>			))}		</ul>	);};

We can then use the second argument inside the map to gain access to the index of the looped item.

React Rendering Lists - StackBlitz

Edit

Files

  • src
import { createRoot } from "react-dom/client";import { useState } from "react";const FileDate = ({ inputDate }) => {	const [dateStr, setDateStr] = useState(formatDate(inputDate));	const [labelText, setLabelText] = useState(formatReadableDate(inputDate));	return <span aria-label={labelText}>{dateStr}</span>;};const File = ({ href, fileName, isSelected, onSelected, isFolder }) => {	return (		<button			onClick={onSelected}			style={				isSelected					? { backgroundColor: "blue", color: "white" }					: { backgroundColor: "white", color: "blue" }			}		>			{fileName}			{isFolder ? <span>Type: Folder</span> : <span>Type: File</span>}			{!isFolder && <FileDate inputDate={new Date()} />}		</button>	);};const filesArray = [	{		fileName: "File one",		href: "/file/file_one",		isFolder: false,	},	{		fileName: "File two",		href: "/file/file_two",		isFolder: false,	},	{		fileName: "File three",		href: "/file/file_three",		isFolder: false,	},];const FileList = () => {	const [selectedIndex, setSelectedIndex] = useState(-1);	const onSelected = (idx) => {		if (selectedIndex === idx) {			setSelectedIndex(-1);			return;		}		setSelectedIndex(idx);	};	// This code sample is missing something and will throw a warning in development mode.	// We'll explain more about this later.	return (		<ul>			{filesArray.map((file, i) => (				<li>					<File						isSelected={selectedIndex === i}						onSelected={() => onSelected(i)}						fileName={file.fileName}						href={file.href}						isFolder={file.isFolder}					/>				</li>			))}		</ul>	);};createRoot(document.getElementById("root")).render(<FileList />);function formatDate(inputDate) {	// Month starts at 0, annoyingly	const month = inputDate.getMonth() + 1;	const date = inputDate.getDate();	const year = inputDate.getFullYear();	return month + "/" + date + "/" + year;}function formatReadableDate(inputDate) {	const months = [		"January",		"February",		"March",		"April",		"May",		"June",		"July",		"August",		"September",		"October",		"November",		"December",	];	const monthStr = months[inputDate.getMonth()];	const dateSuffixStr = dateSuffix(inputDate.getDate());	const yearNum = inputDate.getFullYear();	return monthStr + " " + dateSuffixStr + "," + yearNum;}function dateSuffix(dayNumber) {	const lastDigit = dayNumber % 10;	if (lastDigit == 1 && dayNumber != 11) {		return dayNumber + "st";	}	if (lastDigit == 2 && dayNumber != 12) {		return dayNumber + "nd";	}	if (lastDigit == 3 && dayNumber != 13) {		return dayNumber + "rd";	}	return dayNumber + "th";}

If we look at the rendered output, we can see that all three files are listed as expected!

Using this code as a base, we could extend this file list to any number of files just by adding another item to the hard-coded filesArray list; no templating code changes are required!

Keys

Regardless of the framework, you may have encountered an error in the previous code sample that read like the following:

Warning: Each child in a list should have a unique "key" prop.

This is because, in these frameworks, you're expected to pass a special property called the key (or, track in Angular), which the respective framework uses to keep track of which item is which.

Without this key prop, the framework doesn't know which elements have been unchanged and, therefore, must destroy and recreate each element in the array for every list re-render. This can cause massive performance problems and stability headaches.

If you're confused, no worries — there was a lot of technical speech in that last paragraph. Continue reading to see what this means in practical terms and don't be afraid to come back and re-read this section when you're done with the chapter.

Say you have the following:

const WordList = () => {	const [words, setWords] = useState([]);	const addWord = () => {		const newWord = getRandomWord();		// Remove ability for duplicate words		if (words.includes(newWord)) return;		setWords([...words, newWord]);	};	const removeFirst = () => {		const newWords = [...words];		newWords.shift();		setWords(newWords);	};	return (		<div>			<button onClick={addWord}>Add word</button>			<button onClick={removeFirst}>Remove first word</button>			<ul>				{words.map((word) => {					return (						<li>							{word.word}							<input type="text" />						</li>					);				})}			</ul>		</div>	);};const wordDatabase = [	{ word: "who", id: 1 },	{ word: "what", id: 2 },	{ word: "when", id: 3 },	{ word: "where", id: 4 },	{ word: "why", id: 5 },	{ word: "how", id: 6 },];function getRandomWord() {	return wordDatabase[Math.floor(Math.random() * wordDatabase.length)];}

React Unkeyed Demo - StackBlitz

Edit

Files

  • src
import { createRoot } from "react-dom/client";import { useState } from "react";const WordList = () => {	const [words, setWords] = useState([]);	const addWord = () => {		const newWord = getRandomWord();		// Remove ability for duplicate words		if (words.includes(newWord)) return;		setWords([...words, newWord]);	};	const removeFirst = () => {		const newWords = [...words];		newWords.shift();		setWords(newWords);	};	return (		<div>			<button onClick={addWord}>Add word</button>			<button onClick={removeFirst}>Remove first word</button>			<ul>				{words.map((word) => {					return (						<li>							{word.word}							<input type="text" />						</li>					);				})}			</ul>		</div>	);};const wordDatabase = [	{ word: "who", id: 1 },	{ word: "what", id: 2 },	{ word: "when", id: 3 },	{ word: "where", id: 4 },	{ word: "why", id: 5 },	{ word: "how", id: 6 },];function getRandomWord() {	return wordDatabase[Math.floor(Math.random() * wordDatabase.length)];}createRoot(document.getElementById("root")).render(<WordList />);

Without using some kind of key prop (or, when you use track obj without a property in Angular), your list will be destroyed and recreated every time you run addWord.

This can be demonstrated by typing some text into the input and pressing the "Remove first word" button. When you do so, the typed text behaves in a strange way.

In Angular, the input text simply disappears. In React and Vue, however, the text moves to the line of the word below the one you originally typed inside.

Both of these behaviors are quite peculiar — we've seemingly not modified the li that contains the input in question; why are its contents moving or being removed entirely?

The reason the input text changes is that the framework isn't able to detect which item in your array has changed and, as a result, marks all DOM elements as "outdated". These "outdated" elements are then destroyed by the framework, only to be immediately reconstructed to ensure the most up-to-date information is displayed to the user.

When a render occurs, each item in the array that doesn't have a key also gets re-rendered

Instead, we can tell the framework which list item is which with a unique "key" associated with every list item. This key is then able to allow the framework to intelligently prevent the destruction of items that were not changed in a list data change.

When a key is assigned to an element in a list, it can avoid duplicative renders, like when a new item in a list is added

Let's see how we can do this in each framework.

<div>	<button onClick={addWord}>Add word</button>	<ul>		{words.map((word) => {			return <li key={word.id}>{word.word}</li>;		})}	</ul></div>

Here, we're using the key property to tell React which li is related to which word via the word's unique id field.

React Keyed Demo - StackBlitz

Edit

Files

  • src
import { createRoot } from "react-dom/client";import { useState } from "react";const WordList = () => {	const [words, setWords] = useState([]);	const addWord = () => {		const newWord = getRandomWord();		// Remove ability for duplicate words		if (words.includes(newWord)) return;		setWords([...words, newWord]);	};	const removeFirst = () => {		const newWords = [...words];		newWords.shift();		setWords(newWords);	};	return (		<div>			<button onClick={addWord}>Add word</button>			<button onClick={removeFirst}>Remove first word</button>			<ul>				{words.map((word) => {					return (						<li key={word.id}>							{word.word}							<input type="text" />						</li>					);				})}			</ul>		</div>	);};const wordDatabase = [	{ word: "who", id: 1 },	{ word: "what", id: 2 },	{ word: "when", id: 3 },	{ word: "where", id: 4 },	{ word: "why", id: 5 },	{ word: "how", id: 6 },];function getRandomWord() {	return wordDatabase[Math.floor(Math.random() * wordDatabase.length)];}createRoot(document.getElementById("root")).render(<WordList />);