Post contents
Before we can dive into how many front-end frameworks work, we need to set a baseline of information. If you're already familiar with how the DOM represents a tree and how the browser takes that information and uses it, great; You're ready to read ahead! Otherwise, it's strongly suggested that you look at our post introducing the concepts required to understand some baselines of this post.
You may have heard about various frameworks and libraries that modern front-end developers use to build large-scale applications. Among these frameworks are Angular, React, and Vue. While each of these libraries brings its own strengths and weaknesses, many of the core concepts are shared between them.
With this book, we will outline core concepts shared between them and how you can implement them in code in all three of the frameworks. This should provide a good reference when trying to learn one of these frameworks without pre-requisite knowledge or even trying to learn another framework with some pre-requisite of a different one.
First, let's explain why frameworks like Angular, React, or Vue differ from other libraries that may have come before them, like jQuery.
It all comes down to a single core concept at the heart of each: Componentization.
What's an App, Anyway?
Before we dive into the technical aspects, let's think about what an app consists of at a high level.
Take the following application into consideration.
Our app has many parts to it. For example, a sidebar containing navigation links, a list of files for a user to navigate, and a details pane about the user's selected file.
What's more, each part of the app needs different things.
The sidebar may not require complex programming logic, but we may want to style it with nice colors and highlight effects when the user hovers. Likewise, the file list may contain complex logic to handle a user right-clicking, dragging, and dropping files.
When you break it down, each part of the app has three primary concerns:
- Logic — What does the app do?
- Styling — How does the app look visually?
- Structure — How is the app laid out?
While the mockup above does a decent job of displaying things visually, let's look at what the app looks like structurally:
Here, each section is laid out without any additional styling: Simply a wireframe of what the page will look like, with each section containing blocks laid out in pretty straightforward ways. This is what HTML will help us build.
Now that we understand the structure, let's add some functionality. First, we'll include a small snippet of text in each section to outline our goals. We'd then use these as "acceptance" criteria in the future. This is what our logic will provide to the app.
Great! Now, let's go back and add the styling to recreate the mockup we had before!
We can think of each step of the process like we're adding in a new programming language.
- HTML is used for adding the structure of an application. The side nav might be a
<nav>
tag, for example. - JavaScript adds the logic of the application on top of the structure.
- CSS makes everything look nice and potentially adds some minor UX improvements.
The way I typically think about these three pieces of tech is:
HTML is like building blueprints. It allows you to see the overarching pictures of what the result will look like. They define the walls, doors, and flow of a home.
JavaScript is like the electrical, plumbing, and appliances of the house. They allow you to interact with the building in a meaningful way.
CSS is like the paint and other decors that go into a home. They're what makes the house feel lived in and inviting. Of course, this decor does little without the rest of the home, but without the decor, it's a miserable experience.
Parts of the App
Now that we've introduced what an app looks like, let's go back for a moment. Remember how I said each app is made of parts? Let's explode the app's mockup into smaller pieces and look at them more in-depth.
Here, we can more distinctly see how each part of the app has its own structure, styling, and logic.
The file list, for example, contains the structure of each file being its own item, logic about what buttons do which actions, and some CSS to make it look engaging.
While the code for this section might look something like this:
<section> <button id="addButton"><span class="icon">plus</span></button> <!-- ... --></section><ul> <li> <a href="/file/file_one">File one<span>12/03/21</span></a> </li> <!-- ... --> <ul> <script> var addButton = document.querySelector("#addButton"); addButton.addEventListener("click", () => { // ... }); </script> </ul></ul>
We might have a mental model to break down each section into smaller ones. If we use pseudocode to represent our mental model of the actual codebase, it might look something like this:
<files-buttons> <add-button /></files-buttons><files-list> <file name="File one" /></files-list>
Luckily, by using frameworks, this mental model can be reflected in real code!
Let's look at what <file>
might look like in each framework:
- React
- Angular
- Vue
const File = () => { return ( <div> <a href="/file/file_one"> File one<span>12/03/21</span> </a> </div> );};
Here, we're defining a component that we call File
, which contains a set of instructions for how React is to create the associated HTML when the component is used.
These HTML creation instructions are defined using a syntax very similar to HTML — but in JavaScript instead. This syntax is called "JSX" and powers the show for every React application.
While JSX looks closer to HTML than standard JS, it is not supported in the language itself. Instead, a compiler (or transpiler) like Babel must compile down to regular JS. Under the hood, this JSX compiles down to function calls.
For example, the above would be turned into:
var spanTag = React.createElement("span", null, "12/03/21");var aTag = React.createElement( "a", { href: "/file/file_one", }, "File one", spanTag,);React.createElement("div", null, aTag);
While the above seems intimidating, it's worth mentioning that you'll likely never need to fall back on using
createElement
in an actual production application. Instead, this demonstrates why you need Babel in React applications.You also likely do not need to set up Babel yourself from scratch. Most tools that integrate with React handle it out-of-the-box for you invisibly.
These are called "components." Components have various aspects to them, which we'll learn about throughout the course of this book.
We can see that each framework has its own syntax to display these components, but they often share more similarities than you might think.
Now that we've defined our components, there's a question: how do you use these components in HTML?
Rendering the App
While these components might look like simple HTML, they're capable of much more advanced usage. Because of this, each framework actually uses JavaScript under the hood to "draw" these components on-screen.
This process of "drawing" is called "rendering". This is not a one-and-done, however. A component may render at various times throughout its usage on-screen, particularly when it needs to update data shown on-screen; we'll learn more about this later in the chapter.
Traditionally, when you build out a website with just HTML, you'd define an index.html
file like so:
<!-- index.html --><html> <body> <!-- Your HTML here --> </body></html>
Similarly, all apps built with React, Angular, and Vue start with an index.html
file.
- React
- Angular
- Vue
<!-- index.html --><html> <body> <div id="root"></div> </body></html>
Then, in JavaScript, you "render" a component into an element that acts as the "root" injection site for your framework to build your UI around.
- React
- Angular
- Vue
import { createRoot } from "react-dom/client";const File = () => { return ( <div> <a href="/file/file_one"> File one<span>12/03/21</span> </a> </div> );};createRoot(document.getElementById("root")).render(<File />);
Once a component is rendered, you can do a lot more with it!
For example, just like nodes in the DOM have relationships, so too can components.
Children, Siblings, and More, Oh My!
While our File
component currently contains HTML elements, components may also contain other components!
- React
- Angular
- Vue
const File = () => { return ( <div> <a href="/file/file_one"> File one<span>12/03/21</span> </a> </div> );};const FileList = () => { return ( <ul> <li> <File /> </li> </ul> );};
Looking through our File
component, we'll notice that we're rendering multiple elements inside a single component. Funnily enough, this has the fun side effect that we can also render multiple components inside a parent component.
- React
- Angular
- Vue
const FileList = () => { return ( <ul> <li> <File /> </li> <li> <File /> </li> <li> <File /> </li> </ul> );};
This is a handy feature of components. It allows you to reuse aspects of your structure (and styling + logic, but I'm getting ahead of myself) without repeating yourself. It allows for a very DRY architecture where your code is declared once and reused elsewhere.
That stands for "Don't repeat yourself" and is often heralded as a gold standard of code quality!
It's worth remembering that we're using the term "parent" to refer to our FileList
component in relation to our File
component. This is because, like the DOM tree, each framework's set of components reflects a tree.
This means that the related File
components are "siblings" of one another, each with a "parent" of FileList
.
We can extend this hierarchical relationship to have "grandchildren" and beyond as well:
- React
- Angular
- Vue
const FileDate = () => { return <span>12/03/21</span>;};const File = () => { return ( <div> <a href="/file/file_one"> File one <FileDate /> </a> </div> );};const FileList = () => { return ( <ul> <li> <File /> </li> <li> <File /> </li> <li> <File /> </li> </ul> );};
Logic
HTML isn't the only thing components can store, however! As we mentioned earlier, apps (and, by extension, each part of the respective apps) require three parts:
- Structure (HTML)
- Styling (CSS)
- Logic (JS)
Components can handle all three!
Let's look at how we can declare logic in a component by making file-date
show the current date instead of a static date.
We'll start by adding a variable containing the current date in a human-readable string of MM/DD/YY
.
- React
- Angular
- Vue
const FileDate = () => { const dateStr = `${ new Date().getMonth() + 1 }/${new Date().getDate()}/${new Date().getFullYear()}`; return <span>12/03/21</span>;};
We're not using this new
dateStr
variable yet. This is intentional; we'll use it here shortly.
While this logic to set this variable works, it's a bit verbose (and slow due to recreating the Date
object thrice) - let's break it out into a method contained within the component.
function formatDate() { const today = new Date(); // Month starts at 0, annoyingly const monthNum = today.getMonth() + 1; const dateNum = today.getDate(); const yearNum = today.getFullYear(); return monthNum + "/" + dateNum + "/" + yearNum;}
- React
- Angular
- Vue
function formatDate() { const today = new Date(); // Month starts at 0, annoyingly const monthNum = today.getMonth() + 1; const dateNum = today.getDate(); const yearNum = today.getFullYear(); return monthNum + "/" + dateNum + "/" + yearNum;}const FileDate = () => { const dateStr = formatDate(); return <span>12/03/21</span>;};
Because React can easily access functions outside the component declaration, we decided to move it outside the component scope. This allows us to avoid re-declaring this function in every render, which the other frameworks don't do, thanks to different philosophies.
Intro to Side Effects
Let's verify that our formatDate
method outputs the correct value by telling our components, "Once you're rendered on screen, console.log
the value of that data."
- React
- Angular
- Vue
import { useEffect } from "react";function formatDate() { const today = new Date(); // Month starts at 0, annoyingly const month = today.getMonth() + 1; const date = today.getDate(); const year = today.getFullYear(); return month + "/" + date + "/" + year;}const FileDate = () => { const dateStr = formatDate(); useEffect(() => { console.log(dateStr); }, []); return <span>12/03/21</span>;};
Here, we're telling each respective framework to log the value of dateStr
to the console once the component is rendered for the first time.
Wait, "for the first time?"
Yup! React, Angular, and Vue can all update (or "re-render") when needed.
For example, let's say you want to show dateStr
to a user, but later in the day, the time switches over. While you'd have to handle the code to keep track of the time, the respective framework would notice that you've modified the values of dateStr
and re-render the component to display the new value.
While the method each framework uses to tell when to re-render is different, they all have a highly stable method of doing so.
This feature is arguably the most significant advantage of building an application with one of these frameworks.
This ability to track data being changed relies on the concept of handling "side effects". While we'll touch on this more in our future chapter called "Side effects", you can think of a "side effect" as any change made to a component's data: Either through a user's input or the component's output changing.
Speaking of updating data on-screen - let's look at how we can dynamically display data on a page.
Display
While displaying the value in the console works well for debugging, it's not much help to the user. After all, more than likely, your users won't know what a console even is. Let's show dateStr
on-screen
- React
- Angular
- Vue
function formatDate() { const today = new Date(); // Month starts at 0, annoyingly const monthNum = today.getMonth() + 1; const dateNum = today.getDate(); const yearNum = today.getFullYear(); return monthNum + "/" + dateNum + "/" + yearNum;}const FileDate = () => { const dateStr = formatDate(); return <span>{dateStr}</span>;};
Here, we're using each framework's method of injecting the state into a component. For React, we'll use the {}
syntax to interpolate JavaScript into the template, while Vue and Angular both rely on {{}}
syntax.
Live Updating
But what happens if we update dateStr
after the fact? Say we have a setTimeout
call that updates the date to tomorrow's date after 5 minutes.
Let's think about what that code might look like:
// This is non-framework-specific pseudocodesetTimeout(() => { // 24 hours, 60 minutes, 60 seconds, 1000 milliseconds const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000); const tomorrowDate = formatDate(tomorrow); dateStr = tomorrowDate; // This is not a real method in any of these frameworks // But the idea of re-rendering after data has changed IS // an integral part of these frameworks. They just do it differently rerender();}, 5000);
Let's see what that looks like in practice for each framework:
- React
- Angular
- Vue
In the pseudocode sample we wrote before, we update the value of dateStr
and then re-render the containing component to update a value on-screen using two lines of code.
In React, we use a single line of code to do both and have a special useState
method to tell React what data needs changing.
import { useState, useEffect } from "react";const FileDate = () => { const [dateStr, setDateStr] = useState(formatDate(new Date())); useEffect(() => { setTimeout(() => { // 24 hours, 60 minutes, 60 seconds, 1000 milliseconds const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000); const tomorrowDate = formatDate(tomorrow); setDateStr(tomorrowDate); }, 5000); }, []); return <span>{dateStr}</span>;};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;}
useState
is what React uses to store data that the developer wants to persist between renders. Its first argument (that we're passing a string into) sets the initial value.
We then use array destructuring to convert the returned array into two variables. Another way to write this code is:
const dateArr = useState( `${ new Date().getMonth() + 1 }/${new Date().getDate()}/${new Date().getFullYear()}`,);const dateStr = dateArr[0];const setDateStr = dateArr[1];
Here, we're using setDateStr
to tell React that it should re-render, which will update the value of dateStr
. This differs from Angular and Vue, where you don't have to explicitly tell the framework when to re-render.
Rules of React Hooks
useState
and useEffect
are both what are known as "React Hooks". Hooks are React's method of "hooking" functionality into React's framework code. They allow you to do a myriad of functionalities in React components.
Hooks can be identified as a function that starts with the word "use
". Some other Hooks we'll touch on in the future will include useMemo
, useReducer
, and others.
Something to keep in mind when thinking about Hooks is that they have limitations placed on them by React itself. Namely, React Hooks must:
- Be called from a component* (no normal functions)
- Not be called conditionally inside a component (no
if
statements) - Not be called inside a loop (no
for
orwhile
loops)
// ❌ Not allowed, component names must start with a capital letter, otherwise it's seen as a normal functionconst windowSize = () => { const [dateStr, setDateStr] = useState(formatDate(new Date())); // ...};
// ❌ Not allowed, you must use a hook _inside_ a componentconst [dateStr, setDateStr] = useState(formatDate(new Date()));const Component = () => { return <p>The date is: {dateStr}</p>;};
// ❌ Not allowed, you cannot `return` before using a hookconst WindowSize = () => { if (bool) return "Today"; const [dateStr, setDateStr] = useState(formatDate(new Date())); // ...};
We'll learn more about the nuances surrounding Hooks in the future, but for now, just remember that they're the way you interface with React's APIs.
If you sit on these screens for a while, you'll see that they update automatically!
This idea of a data update triggering other code is called "reactivity" and is a central part of these frameworks.
While the frameworks detect reactive changes under the hood differently, they all handle updating the DOM for you. This allows you to focus on the logic intended to update what's on-screen as opposed to the code that updates the DOM itself.
This is important because to update the DOM in an efficient way requires significant heavy lifting. In fact, two of these frameworks (React and Vue) store an entire copy of the DOM in memory to keep that update as lightweight as possible. In the third book of this book series, "Internals", we'll learn how this works under the hood and how to build our work version of this DOM mirroring.
Attribute Binding
Text isn't the only thing that frameworks are capable of live updating, however!
Just like each framework has a way to have state rendered into text on-screen, it can also update HTML attributes for an element.
Currently, our date
component doesn't read out particularly kindly to screen-readers since it would only read out as numbers. Let's change that by adding an aria-label
of a human-readable date to our date
component.
- React
- Angular
- Vue
const FileDate = () => { const [dateStr, setDateStr] = useState(formatDate(new Date())); // ... return <span aria-label="January 10th, 2023">{dateStr}</span>;};
Now, when we use a screen reader, it'll read out "January 10th" instead of "One dash ten".
However, while this may have worked before the date
was dynamically formatted, it won't be correct for most of the year. (Luckily for us, a broken clock is correct at least once a day.)
Let's correct that by adding in a formatReadableDate
method and reflect that in the attribute:
- React
- Angular
- Vue
import { useState, useEffect } from "react";const FileDate = () => { const [dateStr, setDateStr] = useState(formatDate(new Date())); const [labelText, setLabelText] = useState(formatReadableDate(new Date())); // ... return <span aria-label={labelText}>{dateStr}</span>;};// ...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";}
Notice the
{}
used after the=
to assign the attribute value. This is pretty similar to the syntax to interpolate text into the DOM!
This code isn't exactly what you might expect to see in production. If you're looking to write production code, you may want to look into derived values to base the
labelText
anddate
values off of the sameDate
object directly. This would let you avoid callingnew Date
twice, but I'm getting ahead of myself - we'll touch on derived values in a future section.
Awesome! Now, it should read the file's date properly to a screen reader!
Inputs
Our file list is starting to look good! That said, a file list containing the same file repeatedly isn't much of a file list. Ideally, we'd like to pass the file's name into our File
component to add a bit of variance.
Luckily for us, components accept arguments just like functions! These arguments are most often called "inputs" or "properties" (shortened to "props") in the component world.
Let's have the file name be an input to our File
component:
- React
- Angular
- Vue
const File = (props) => { return ( <div> <a href="/file/file_one"> {props.fileName} <FileDate /> </a> </div> );};const FileList = () => { return ( <ul> <li> <File fileName={"File one"} /> </li> <li> <File fileName={"File two"} /> </li> <li> <File fileName={"File three"} /> </li> </ul> );};
React uses an object to contain all properties that we want to pass to a component. We can use parameter destructuring to get the properties without having to use
props
before the name of the parameter we really want, like so:const File = ({ fileName }) => { return ( <div> <a href="/file/file_one"> {fileName} <FileDate /> </a> </div> );};
Since this is extremely common in production React applications, we'll be using this style going forward.
Similarly, while
{}
is required to bind a variable to an input or attribute in React, since we're only passing a string here, we could alternatively write:<File fileName="File one" />
Here, we can see each File
being rendered with its own name.
One way of thinking about passing properties to a component is to "pass down" data to our children's components. Remember, these components make a parent/child relationship to one another.
It's exciting what progress we're making! But oh no - the links are still static! Each file has the same href
property as the last. Let's fix that!
Multiple Properties
Like functions, components can accept as many properties as you'd like to pass. Let's add another for href
:
- React
- Angular
- Vue
const File = ({ fileName, href }) => { return ( <div> <a href={href}> {fileName} <FileDate /> </a> </div> );};const FileList = () => { return ( <ul> <li> <File fileName="File one" href="/file/file_one" /> </li> <li> <File fileName="File two" href="/file/file_two" /> </li> <li> <File fileName="File three" href="/file/file_three" /> </li> </ul> );};
Object Passing
While we've been using strings to pass values to a component as an input, this isn't always the case.
Input properties can be of any JavaScript type. This can include objects, strings, numbers, arrays, class instances, or anything in between!
To showcase this, let's add the ability to pass a Date
class instance to our file-date
component. After all, each file in the list of our files will likely be created at different times.
- React
- Angular
- Vue
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 }) => { return ( <div> <a href={href}> {fileName} <FileDate inputDate={new Date()} /> </a> </div> );};
Once again, I have to add a minor asterisk next to this code sample. Right now, if you update the
inputDate
value after the initial render, it will not show the new date string inFileDate
. This is because we're setting the value ofdateStr
andlabelText
only once and not updating the values.Each framework has a way of live-updating this value for us, as we might usually expect, by using a derived value, but we'll touch on that in a future section.
Props Rules
While it's true that a component property can be passed a JavaScript object, there's a rule you must follow when it comes to object props:
You must not mutate component prop values.
For example, here's some code that will not work as expected:
- React
- Angular
- Vue
const GenericList = ({ inputArray }) => { // This is NOT allowed and will break things inputArray.push("some value"); // ...};
You're not intended to mutate properties because it breaks two key concepts of application architecture with components:
Event Binding
Binding values to an HTML attribute is a powerful way to control your UI, but that's only half the story. Showing information to the user is one thing, but you must also react to a user's input.
One way you can do this is by binding a DOM event emitted by a user's behavior.
In the mockup we saw before, the list of our files has a hover state for the file list. However, when the user clicks on a file, it should be highlighted more distinctly.
Let's add an isSelected
property to our file
component to add hover styling conditionally, then update it when the user clicks on it.
While we're at it, let's migrate our File
component to use a button
instead of a div
. After all, it's important for accessibility and SEO to use semantic elements to indicate what element is which in the DOM.
- React
- Angular
- Vue
const File = ({ fileName }) => { const [isSelected, setSelected] = useState(false); const selectFile = () => { setSelected(!isSelected); }; return ( <button onClick={selectFile} style={ isSelected ? { backgroundColor: "blue", color: "white" } : { backgroundColor: "white", color: "blue" } } > {fileName} <FileDate inputDate={new Date()} /> </button> );};
There are three major things of note in this code sample:
-
The
style
property differs from what you might see in a typical HTML code sample.EG:
"background-color: blue; color: white;"
becomes{backgroundColor: 'blue', color: 'white'}
.This is required to embed CSS directly inside JSX's
style
property. If you move this code out to a dedicated CSS file and use classes, you can use the more traditional syntax. -
We're using the second value of the
useState
returned array.The second value of the array returned by
useState
is used to update the value assigned to the first variable. So, whensetSelected
is called, it will then update the value ofisSelected
, and the component is re-rendered.
-
We prefix the event name we're listening for with
on
and capitalize the first letter of the event name.EG:
click
becomesonClick
.
Outputs
Components aren't limited to only being able to receive a value from its parent; You're also able to send values back to the parent from the child component.
The way this usually works is by passing data upwards via a custom event, much like those emitted by the browser. Similar to how our event binding used some new syntax along with familiar concepts, we'll do the same with event emitting.
While listening for a click
event in our File
component works well enough when we only have one file, it introduces some odd behavior with multiple files. Namely, it allows us to select more than one file at a time simply by clicking. Let's assume this isn't the expected behavior and instead emit a selected
custom event to allow for only one selected file at a time.
- React
- Angular
- Vue
React expects you to pass in a function as opposed to emitting an event and listening for it.
This differs slightly from Vue and Angular but has the same fundamental idea of "sending data to a parent component."
import { useState } from "react";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} {/* ... */} </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> );};
Keep in mind: This code isn't quite production-ready. There are some accessibility concerns with this code and might require things like
aria-selected
and more to fix.
Here, we're using a simple number-based index to act as an id
of sorts for each file. This allows us to keep track of which file is currently selected or not. Likewise, if the user selects an index that's already been selected, we will set the isSelected
index to a number that no file has associated.
You may notice that we've also removed our isSelected
state and logic from our file
component. This is because we're following the practices of "raising state".
Challenge
Now that we have a solid grasp on the fundamentals of components, let's build some ourselves!
Namely, I want us to create a primitive version of the following:
To do this, let's:
- Create a sidebar component
- Add a list of buttons with sidebar items' names
- Make an
ExpandableDropdown
component - Add a
name
input to the dropdown and display it - Add an
expanded
input to the dropdown and display it - Use an output to toggle the
expanded
input - Make our
expanded
property functional
Creating Our First Components
Let's kick off this process by creating our index.html
and a basic component to render:
- React
- Angular
- Vue
<!-- index.html --><html> <body> <div id="root"></div> </body></html>
import { createRoot } from "react-dom";const Sidebar = () => { return <p>Hello, world!</p>;};createRoot(document.getElementById("root")).render(<Sidebar />);
Now that we have an initial testbed for our component let's add a list of buttons with the names of the sidebar list items:
- React
- Angular
- Vue
const Sidebar = () => { return ( <div> <h1>My Files</h1> <div> <button>Movies</button> </div> <div> <button>Pictures</button> </div> <div> <button>Concepts</button> </div> <div> <button>Articles I'll Never Finish</button> </div> <div> <button>Website Redesigns v5</button> </div> <div> <button>Invoices</button> </div> </div> );};
This repeated div
and button
combo makes me think that we should extract each of these items to a component since we want to both:
- Reuse the HTML layout
- Expand the current functionality
Start by extracting the div
and button
to their own component, which we'll call ExpandableDropdown
.
- React
- Angular
- Vue
const ExpandableDropdown = ({ name }) => { return ( <div> <button>{name}</button> </div> );};const Sidebar = () => { return ( <div> <h1>My Files</h1> <ExpandableDropdown name="Movies" /> <ExpandableDropdown name="Pictures" /> <ExpandableDropdown name="Concepts" /> <ExpandableDropdown name="Articles I'll Never Finish" /> <ExpandableDropdown name="Website Redesigns v5" /> <ExpandableDropdown name="Invoices" /> </div> );};
We should now see a list of buttons with a name associated with each!
Making Our Components Functional
Now that we've created the initial structure of our components, let's work on making them functional.
To start, we'll:
- Create an
expanded
property for each button - Pass the
expanded
property using an input - Display the value of
expanded
inside of ourExpandableDropdown
component
- React
- Angular
- Vue
const ExpandableDropdown = ({ name, expanded }) => { return ( <div> <button>{name}</button> <div>{expanded ? "Expanded" : "Collapsed"}</div> </div> );};const Sidebar = () => { // Just to show that the value is displaying properly const [moviesExpanded, setMoviesExpanded] = useState(true); const [picturesExpanded, setPicturesExpanded] = useState(false); const [conceptsExpanded, setConceptsExpanded] = useState(false); const [articlesExpanded, setArticlesExpanded] = useState(false); const [redesignExpanded, setRedesignExpanded] = useState(false); const [invoicesExpanded, setInvoicesExpanded] = useState(false); return ( <div> <h1>My Files</h1> <ExpandableDropdown name="Movies" expanded={moviesExpanded} /> <ExpandableDropdown name="Pictures" expanded={picturesExpanded} /> <ExpandableDropdown name="Concepts" expanded={conceptsExpanded} /> <ExpandableDropdown name="Articles I'll Never Finish" expanded={articlesExpanded} /> <ExpandableDropdown name="Website Redesigns v5" expanded={redesignExpanded} /> <ExpandableDropdown name="Invoices" expanded={invoicesExpanded} /> </div> );};
Let's now add an output to allow our component to toggle the expanded
input.
- React
- Angular
- Vue
const ExpandableDropdown = ({ name, expanded, onToggle }) => { return ( <div> <button onClick={onToggle}>{name}</button> <div>{expanded ? "Expanded" : "Collapsed"}</div> </div> );};const Sidebar = () => { // Just to show that the value is displaying properly const [moviesExpanded, setMoviesExpanded] = useState(true); const [picturesExpanded, setPicturesExpanded] = useState(false); const [conceptsExpanded, setConceptsExpanded] = useState(false); const [articlesExpanded, setArticlesExpanded] = useState(false); const [redesignExpanded, setRedesignExpanded] = useState(false); const [invoicesExpanded, setInvoicesExpanded] = useState(false); return ( <div> <h1>My Files</h1> <ExpandableDropdown name="Movies" expanded={moviesExpanded} onToggle={() => setMoviesExpanded(!moviesExpanded)} /> <ExpandableDropdown name="Pictures" expanded={picturesExpanded} onToggle={() => setPicturesExpanded(!picturesExpanded)} /> <ExpandableDropdown name="Concepts" expanded={conceptsExpanded} onToggle={() => setConceptsExpanded(!conceptsExpanded)} /> <ExpandableDropdown name="Articles I'll Never Finish" expanded={articlesExpanded} onToggle={() => setArticlesExpanded(!articlesExpanded)} /> <ExpandableDropdown name="Website Redesigns v5" expanded={redesignExpanded} onToggle={() => setRedesignExpanded(!redesignExpanded)} /> <ExpandableDropdown name="Invoices" expanded={invoicesExpanded} onToggle={() => setInvoicesExpanded(!invoicesExpanded)} /> </div> );};
Finally, we can update our ExpandableDropdown
component to hide and show the contents of the dropdown by using an HTML attribute called "hidden". When this attribute is true
, it will hide the contents, but when false
, it will display them.
- React
- Angular
- Vue
const ExpandableDropdown = ({ name, expanded, onToggle }) => { return ( <div> <button onClick={onToggle}> {expanded ? "V " : "> "} {name} </button> <div hidden={!expanded}>More information here</div> </div> );};