By: Alan Nguyen
Table of Contents
- Front-end Engineer
- 1. Introduction
- 2. Web Development Fundamentals
- 3. Improved Styling with CSS
- 4. Making a Website Responsive
- 5. JavaScript Syntax - Part 1
- 6. JavaScript Syntax - Part 2
- 7. Building Interactive Websites
- 8. Making a Website Accessible
- 9. CSS Transitions and Animation
- 10. Command Line - Git - Github
- 11. HTML-CSS-JS Portfolio Project
- 12. JavaScript Syntax - Part 3
- 13. TDD Fundamentals
- 14. Async JavaScript and HTTP Requests
- 15. Web Apps
- 16. React - Part 1
- 17. React - Part 2
- 18. Redux
- 19. Advanced Concepts in TDD
- 20. React & Redux Porfolio Project
- 21. Advanced Web Development
- 22. Linear Data Structures
- 23. Complex Data Structures
- 24. Algorithms
- 25. Search & Graph Search Algorithms
- 26. Intervew Skills
- 27. Final Front-end Portfolio Project
- 28. Next Steps in Your Front-end engineer jouney
- Having a neutral background (any color with low lightness or saturation) is a good base
- a dark background with light colored text can be a reasonable alternative
- Brand color is a hue that should dominate your color palette.
- accounts for roughly 60% of the color used
- Button color can determine a lot in the user’s eyes
- hover state (a shade or tint of the accent color )
- disabled state
- Using a darker gray for text on a white background, or white text on a dark gray background,
- Whitespace, or negative space, refers to the emptiness between elements.
- Color blindness
- red-green (difficult to differentiate between the red and green colors)
- blue-yellow (blues tend to appear greener)
- monochromatic (can’t see color at all)
Accessibility
Iterations: Define - Develop - Test - Research
Cloudflare This is a helpful tool to make it easier to work with color and build palettes.
#HTML Headers
- Differentiating important pieces of text, such as titles and subtitles
- HTML Best Practice
- <h1> — Used for titles. You should only have one header this style per page.
- <h2> — Used for headings. This should be to identify major sections you want the user to immediately be drawn towards.
- <h3> — Used for subheadings.
This should be used for smaller focal points, such as articles, that you want the user to notice when scanning the page.
#Fonts
- have most of your text in a serif font, and then create contrast with headings or subheadings in a sans-serif font
- can contrast between different pieces of text in a number of ways:
- style
- size
- weight
- color
#Text Readability
- Make sure that your text is big enough (over 16px).
- Have a strong contrast between the foreground and the background.
- Make sure there is enough space between lines and letters (remember, whitespace is king!)
How can we adjust whitespace within the text?
- Line spacing (leading) - distance between two lines of text
- Tracking - the space between the letters and the words
- Kerning - the space between two letters
#Text Navigation
- Stick to conventions about showing what is clickable
- Never use blue as a general accent color for text
- Having text on navigation buttons is important
If you organize the text into columns, the user’s eyes will likely not travel across the entire screen, they will travel to the end of a column and then down.
#Text length, columns, and line length
- Text length - The average internet paragraph is only 1-3 sentences.
- Line length - has a larger impact than you would expect on the reader
Each time a reader gets to a new line the mind is slightly energized and encouraged (“Typographie”, E. Ruder).
- users prefer shorter line lengths
- columns contain roughly 50-75 characters per line
#What Content will the Users Notice and Remember?
- Primacy and recency effects
- People will notice and remember the first and last elements of a list or a page
- Image pairing
- Users eyes are easily drawn to images quicker than they are to text.
- This pairing can be accomplished by grouping with card designs (putting them in a div together with a shared background color).
#F-Shaped skimming
- You need to write and format your text with this skimmer in mind.
- Users will scan content on the left of the screen before the right of the screen, and the top of the screen before the bottom of the screen. Our eyes jump back to the left of the screen whenever we finish reading a line.
-
Showing interactivity and clickability through signifiers
-
Browser link styles
- clicked (but not yet followed) links appear with red text,
- previously visited links are styled with purple text.
#Tooltips and titles
<p>
<a href="https://www.codecademy.com" title="Codecademy is an online learning platform">Codecademy</a> is the best place to learn to code!
</p>
#Link States Links have four main states:
- normal (not clicked) - :link
- hover - :hover
- active (clicked) - :active
- visited - :visited.
#Skeuomorphism and Flat Design
- If users can draw a metaphor between a familiar real-life object and an interface element, they are more likely to know how to use it without training.
- Flat design uses simplicity and lack of clutter for its UI elements.
#Buttons: Hover states
- Users expect buttons to be clickable. Since buttons can consist of any number of total elements (rectangular/circular body, text, image(s)), all elements should be clickable
#Affordances
- Objects afford the ability of users to interact with them in various ways.
- Potentials for interaction are collectively called the affordances of an object.
#Signifiers
- Signifiers are aspects of an object that a designer uses to indicate potential and intended affordances of an object.
- The cup's handle is an example of a common user experience pattern.
#UX Patterns
- User experience (UX) patterns establish reusable solutions to common problems.
#Affordances and Signifiers in Web Design
One common example of visual feedback is the cursor image itself
- a pointing hand indicating that an interaction will occur
- an i-beam shape indicating that text can be selected
- a four-directional arrow showing that an element can be moved
- and many more cursor styles and interactions.
A ubiquitous example is the styling of hyperlinks
Further Reading
- Signifiers, not affordances by Don Norman
- UIpatterns.com
Quiz
What is the proper cascade order for pseudo-classes so that they show all link states accurately?
- :link, :visited, :hover, :active
#Intro
- The primary navigation system typically contains the most important links and buttons that need to be displayed on every single page of the site.
- Secondary(breadcrumbs) navigation consists of a clickable list of pages or attributes that led to the current page.
Benefit of using breadcrumbs
- gives an idea of where they're on the website
- hints at the extent of the site.
- provides a way for a user to quickly jump backward in their navigation of the site
#Breadcumb styles
<ul class="breadcrumb">
<li><a href="">Asia</a></li>
<li><a href="">Singapore</a></li>
<li><a href="">Tourism</a></li>
<li><a href="">Hotels</a></li>
</ul>
.breadcrumb {
text-align: left;
}
.breadcrumb li {
float: left;
}
.breadcrumb a {
color: #fff;
background: darkcyan;
text-decoration: none;
position: relative;
height: 30px;
line-height: 30px;
text-align: center;
margin-right: 15px;
padding: 0 5px;
}
.breadcrumb a::before,
.breadcrumb a::after {
content: "";
position: absolute;
border-color: darkcyan;
border-style: solid;
border-width: 15px 5px;
}
.breadcrumb a::before {
left: -10px;
border-left-color: transparent;
}
.breadcrumb a::after {
left: 100%;
}
.breadcrumb a::after {
left: 100%;
border-color: transparent;
border-left-color: darkcyan;
}
.breadcrumb a:hover {
background-color: blue;
}
.breadcrumb a:hover::before {
border-color: blue;
border-left-color: transparent;
}
.breadcrumb a:hover::after {
border-left-color: blue;
}
#Breadcrumb Types
There are three major types of breadcrumbs:
- Location
- Attribute
- Path
In general, the rule of not adding anything extraneous to the design
#CSS Transitions
CSS transitions allow us to control the timing of visual state changes. We can control the following four aspects of an element’s transition:
- Which CSS properties transition
- How long a transition lasts
- How much time there is before a transition begins
- How a transition accelerates
#Duration
To create a simple transition in CSS, we must specify two of the four aspects:
The property that we want to transition.
The duration of the transition.
a {
transition-property: color;
transition-duration: 1s;
}
Resource - all animated properties
#Delay
Much like duration, transition-delay
's value is an amount of time. Delay specifies the time to wait before starting the transition.
transition-property: width;
transition-duration: 750ms;
transition-delay: 250ms;
#Timing Function
transition-timing-function
describes the pace of the transition.
The default value is ease
, which starts the transition slowly, speeds up in the middle, and slows down again at the end.
Other valid values include:
ease-in
— starts slow, accelerates quickly, stops abruptlyease-out
— begins abruptly, slows down, and ends slowlyease-in-out
— starts slow, gets fast in the middle, and ends slowlylinear
— constant speed throughout
#Shorthand
/* property duration timing-function delay */
transition: color 1.5s linear 0.5s;
- Leaving out one of the properties causes the default value for that property to be applied.
- There is one exception: You must set duration if you want to define delay. Since both are time values, the browser will always interpret the first time value it sees as duration.
#Combination
- can describe unique transitions for multiple properties, and combine them.
.button span,
.button div,
.button i {
transition: width 750ms ease-in 200ms, left 500ms ease-out 450ms, font-size 950ms linear;
}
#All
- It is common to use the same duration, timing function, and delay for multiple properties.
transition-property
value toall
will apply the same values to all properties. all
means every value that changes will be transitioned in the same way. You can use all with the separate transition properties, or the shorthand syntax.
transition: all 1.5s linear 0.5s;
Resources
- https://thoughtbot.com/blog/css-animation-for-beginners
- https://css-tricks.com/the-facebook-loading-animation-in-css/
- MDN - Using CSS animations
#Additional resources:
- Article: Web Animation at Work, Rachel Nabors
- Article: Scaling Responsive Animations, Zach Saucier
- keyframes.app
- animista.net
- cubic-bezier.com
- easings.net
#Authentication and OAuth Introduction
- Authentication is the process used by applications to determine and confirm identities of users. It ensures that the correct content is shown to users. More importantly, it ensures that incorrect content is secured and unavailable to unauthorized users.
Password authentication
- most common
- application's server checks the supplied credentials
- upon a successful login, the application will respond with an authentication token (or auth token) for the client to use for additional HTTP requests. This token is then stored on the user’s computer, preventing the need for users to continuously log in.
API keys
- Many apps expose interfaces to their information in the form of an API (application program interface)
- When your application makes a request, API key is sent along with it. The API can then verify that your application is allowed access and provide the correct response based on the permission level of your application.
- The API can track what type and frequency of requests each application is making. This data can be used to throttle requests from a specific application to a pre-defined level of service. This prevents applications from spamming an endpoint or abusing user data.
OAuth
- OAuth is an open standard and is commonly used to grant permission for applications to access user information without forcing users to give away their passwords.
- An open standard is a publicly available definition of how some functionality should work. However, the standard does not actually build out that functionality.
Generic OAuth Flow
- After selecting the service, the user will be redirected to the service to login. This login confirms the user’s identity and typically provides the user with a list of permissions the originating application is attempting to gain on the user’s account.
- If the user confirms they want to allow this access, they will be redirected back to the original site, along with an access token. This access token is then saved by the originating application
OAuth 2
- OAuth 2 allows for different authentication flows depending on the specific application requesting access and the level of access being requested.
Below, we’ll discuss a few of the common OAuth 2 flows and how they are used.
- Client Credentials Grant
- This type of grant is used to access application-level data (similar to the developer API key above) and the end user does not participate in this flow.
- Instead of an API key, a client ID and a client secret (strings provided to the application when it was authorized to use the API) are exchanged for an access token (and sometimes a refresh token).
- It is essential to ensure the client secret does not become public information, just like a password.
- Developers should be careful not to accidentally commit this information to a public git repository.
- To ensure integrity of the secret key, it should not be exposed on the client-side and all requests containing it should be sent server-side.
- This access token is often short-lived, expiring frequently. Upon expiration, a new access token can be obtained by re-sending the client credentials or, preferably, a refresh token.
- Refresh tokens are an important feature of the OAuth 2 updates, encouraging access tokens to expire often and, as a result, be continuously changed
- Authorization Code Grant
- This flow is one of the most common implementations of OAuth (e.g. Facebook, Google)
- It is similar to the OAuth flow described earlier with an added step linking the requesting application to the authentication.
- A user is redirected to the authenticating site, verifies the application requesting access and permissions, and is redirected back to the referring site with an authorization code.
- The requesting application then takes this code and submits it to the authenticating API, along with the application’s client ID and client secret to receive an access token and a refresh token.
- To avoid exposing the client ID and secret, this step of the flow should be done on the server side of the requesting application.
- Since tokens are tied both to users and requesting applications, the API has a great deal of control over limiting access based on user behavior, application behavior, or both.
- Implicit Grant
- The Implicit Grant OAuth flow was designed for applications which may need to access an OAuth API but don’t have the necessary server-side capabilities to keep this information secure.
- This flow prompts the user through similar authorization steps as the Authorization Code flow, but does not involve the exchange of the client secret.
- The result of this interaction is an access token, and typically no refresh token. The access token is then used by the application to make additional requests to the service, but is not sent to the server side of the requesting application.
- This flow allows applications to use OAuth APIs without fear of potentially exposing long-term access to a user or application’s information.
OAuth provides powerful access to a diverse set of sites and information. By using it correctly, you can reduce sign-up friction and enrich user experience in your applications.
#CORS
The web pages make frequent requests to load assets like images, fonts, and more, from many different places across the Internet. If these requests for assets go unchecked, the security of your browser may be at risk.
- For example, your browser may be subject to hijacking, or your browser might blindly download malicious code.
- Many modern browsers follow security policies to mitigate such risks.
What is security policy?
- Security policies on servers mitigate the risks associated with requesting assets hosted on different server.
- same-origin security policy
- is very restrictive. Under this policy, a document (i.e., like a web page) hosted on server A can only interact with other documents that are also on server A.
- the same-origin policy enforces that documents that interact with each other have the same origin.
- An origin is made up of the following three parts:
- protocol
- host
- port number
- not having a security policy can be risky, but a security policy like same-origin is a bit too restrictive.
- there are security policies that strike a mix of both, like cross-origin, which has evolved into the
cross-origin resource sharing standard (CORS)
What is CORS?
- A request for a resource (like an image or a font) outside of the origin is known as a cross-origin request. CORS (cross-origin resource sharing) manages cross-origin requests.
- Allowing cross-origin requests is helpful, as many websites today load resources from different places on the Internet (stylesheets, scripts, images, and more).
- Cross-origin requests mean that servers must implement ways to handle requests from origins outside of their own. CORS allows servers to specify who (i.e., which origins) can access the assets on the server, among many other things.
You can think of these interactions as a building with a security entrance. For example, if you need to borrow a ladder, you could ask a neighbor in the building who has one. The building’s security would likely not have a problem with this request (i.e., same-origin). If you needed a particular tool, however, and you ordered it from an outside source like an online marketplace (i.e., cross-origin), the security at the entrance may request that the delivery person provide identification when your tool arrives.
Why is CORS necessary?
- allows servers to specify not only who can access the assets, but also how they can be accessed.
- With CORS, a server can specify who can access its assets and which HTTP request methods are allowed from external resources.
How does CORS manage requests from external resources?
-
The CORS standard manages cross-origin requests by adding new HTTP headers to the standard list of headers.
-
The following are the new HTTP headers added by the CORS standard:
- Access-Control-Allow-Origin
- Access-Control-Allow-Credentials
- Access-Control-Allow-Headers
- Access-Control-Allow-Methods
- Access-Control-Expose-Headers
- Access-Control-Max-Age
- Access-Control-Request-Headers
- Access-Control-Request-Method
- Origin
-
The Access-Control-Allow-Origin header allows servers to specify how their resources are shared with external domains. When a GET request is made to access a resource on Server A, Server A will respond with a value for the Access-Control-Allow-Origin header. Many times, this value will be *, meaning that Server A will share the requested resources with any domain on the Internet. Other times, the value of this header may be set to a particular domain (or list of domains), meaning that Server A will share its resources with that specific domain (or list of domains). The Access-Control-Allow-Origin header is critical to resource security.
Pre-flight Requests
- When a request is made using any of the following HTTP request methods, a standard preflight request will be made before the original request.
- PUT
- DELETE
- CONNECT
- OPTIONS
- TRACE
- PATCH
- Preflight requests use the OPTIONS header. The preflight request is sent before the original request, hence the term “preflight.” The purpose of the preflight request is to determine whether or not the original request is safe (for example, a DELETE request). The server will respond to the preflight request and indicate whether or not the original request is safe. If the server specifies that the original request is safe, it will allow the original request. Otherwise, it will block the original request.
- The request methods above aren’t the only thing that will trigger a preflight request. If any of the headers that are automatically set by your browser (i.e., user agent) are modified, that will also trigger a preflight request.
How do I implement CORS?
-
Implementing the request headers to set up CORS correctly depends on the language and framework of the backend.
-
Node, you can use
setHeader()
, as shown below:response.setHeader('Content-Type', 'text/html');
-
Express, you can use CORS middleware
$ npm install cors
var express = require('express'); var cors = require('cors'); var app = express(); app.use(cors()); app.get('/hello/:id', function (req, res, next) { res.json({msg: 'Hello world, we are CORS-enabled!'}); }); app.listen(80, function () { console.log('CORS-enabled web server is listening on port 80'); });
Additional resources:
- Documentation: Ajax
- Video: What the Heck is the Event Loop Anyways
#Components
#The State Hook
Array in States
- JavaScript arrays are the best data model for managing and rendering JSX lists.
options
is an array that contains the names of all of the pizza toppings availableselected
is an array representing the selected toppings for our personal pizza
- The
options
array contains static data, meaning that it does not change. We like to define static data models outside of our function components - The
selected
array contains dynamic data, meaning that it changes, usually based on a user’s actions. We initializeselected
as an empty array. When a button is clicked, thetoggleTopping
event handler is called.
import React, { useState } from "react";
const options = ["Bell Pepper", "Sausage", "Pepperoni", "Pineapple"];
export default function PersonalPizza() {
const [selected, setSelected] = useState([]);
const toggleTopping = ({target}) => {
const clickedTopping = target.value;
setSelected((prev) => {
// check if clicked topping is already selected
if (prev.includes(clickedTopping)) {
// filter the clicked topping out of state
return prev.filter(t => t !== clickedTopping);
} else {
// add the clicked topping to our state
return [clickedTopping, ...prev];
}
});
};
return (
<div>
{options.map(option => (
<button value={option} onClick={toggleTopping} key={option}>
{selected.includes(option) ? "Remove " : "Add "}
{option}
</button>
))}
<p>Order a {selected.join(", ")} pizza</p>
</div>
);
}
Objects in State
- When we work with a set of related variables, it can be very helpful to group them in an object.
export default function Login() {
const [formState, setFormState] = useState({});
const handleChange = ({ target }) => {
const { name, value } = target;
setFormState((prev) => ({
...prev,
[name]: value // [name] is computed property
}));
};
return (
<form>
<input
value={formState.firstName}
onChange={handleChange}
name="firstName"
type="text"
/>
<input
value={formState.password}
onChange={handleChange}
type="password"
name="password"
/>
</form>
);
}
A few things to notice:
- We use a state setter callback function to update state based on the previous value
- The spread syntax is the same for objects as for arrays:
{ ...oldObject, newKey: newValue }
- We reuse our event handler across multiple inputs by using the input tag’s
name
attribute to identify which input the change event came from
Seperate Hooks for Separate States
- Managing dynamic data with separate state variables has many advantages, like making our code more simple to write, read, test, and reuse across components.
function Subject() {
const [currentGrade, setGrade] = useState('B');
const [classmates, setClassmates] = useState(['Hasan', 'Sam', 'Emma']);
const [classDetails, setClassDetails] = useState({topic: 'Math', teacher: 'Ms. Barry', room: 201});
const [exams, setExams] = useState([{unit: 1, score: 91}, {unit: 2, score: 88}]);
// ...
}
#The Effect Hook
Why use useEffect
?
A few reasons why we may want to run some code after each render:
- fetch data from a backend service
- subscribe to a stream of data
- manage timers and intervals
- read from and make changes to the DOM
useEffect()
is a function that we’ll use to execute some code after the first render, after each re-render, and after the last render of a function component.
Clean up Effects
useEffect(()=>{
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
})
Control when effects are called
- If we want to only call our effect after the first render, we pass an empty array
[]
touseEffect()
as the second argument.
useEffect(() => {
alert("component rendered for the first time");
return () => {
alert("component is being removed from the DOM");
};
}, []);
Fetch Data
- An empty dependency array signals to the Effect Hook that our effect never needs to be re-run, that it doesn’t depend on anything. Specifying zero dependencies means that the result of running that effect won’t change and calling our effect once is enough.
- A dependency array that is not empty signals to the Effect Hook that it can skip calling our effect after re-renders unless the value of one of the variables in our dependency array has changed. If the value of a dependency has changed, then the Effect Hook will call our effect again!
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if the value stored by count changes
Rules of Hooks
There are two main rules to keep in mind when using Hooks:
- only call Hooks at the top level
- only call Hooks from React functions
React keeps track of the data and functions that we are managing with Hooks based on their order in the function component’s definition. For this reason, we always call our Hooks at the top level; we never call hooks inside of loops, conditions, or nested functions.
Instead of confusing React with code like this:
if (userName !== '') {
useEffect(() => {
localStorage.setItem('savedUserName', userName);
});
}
We can accomplish the same goal, while consistently calling our Hook every time:
useEffect(() => {
if (userName !== '') {
localStorage.setItem('savedUserName', userName);
}
});
Separate Hooks for Separate Effects
- It's a good idea to separate concerns by managing different data with different Hooks.
Compare the complexity here, where data is bundled up into a single object:
const [data, setData] = useState({ position: { x: 0, y: 0 } });
useEffect(() => {
get('/menu').then((response) => {
setData((prev) => ({ ...prev, menuItems: response.data }));
});
const handleMove = (event) =>
setData((prev) => ({
...prev,
position: { x: event.clientX, y: event.clientY }
}));
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
To the simplicity here, where we have separated concerns:
const [menuItems, setMenuItems] = useState(null);
useEffect(() => {
get('/menu').then((response) => setMenuItems(response.data));
}, []);
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (event) =>
setPosition({ x: event.clientX, y: event.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
#Review
In this lesson, we learned how to write effects that manage timers, manipulate the DOM, and fetch data from a server. In earlier versions of React, we could only have executed this type of code in the lifecycle methods of class components, but we can now perform these types of actions in function components as well!
Let’s review the main concepts from this lesson:
useEffect()
- we can import this function from the ‘react’ library and call it in our function components- effect - refers to a function that we pass as the first argument of the useEffect() function. By default, the Effect Hook calls this effect after each render
- cleanup function - the function that is optionally returned by the effect. If the effect does anything that needs to be cleaned up to prevent memory leaks, then the effect returns a cleanup function, then the Effect Hook will call this cleanup function before calling the effect again as well as when the component is being unmounted
- dependency array - this is the optional second argument that the useEffect() function can be called with in order to prevent repeatedly calling the effect when this is not needed. This array should consist of all variables that the effect depends on.
The Effect Hook is all about scheduling when our effect’s code gets executed. We can use the dependency array to configure when our effect is called in the following ways:
Dependency Array | Effect called after first render & … |
---|---|
undefined | every re-render |
Empty array | no re-renders |
Non-empty array | when any value in the dependency array changes |
Hooks gives us the flexibility to organize our code in different ways, grouping related data as well as separating concerns to keep code simple, error-free, reusable, and testable!
- What is Test Coverage
- Mocking, stubbing, and Contract Testing
- Jest mock functions
- Writing tests for react apps using Jest and Enzyme
Running packages
- Using Yarn:
yarn create-react-app my-app
- Using npm:
npm create-react-app my-app
Adding dependencies
- Using Yarn:
yarn add enzyme enzyme-adapter-react-16 --dev
- Using npm:
npm install enzyme enzyme-adapter-react-16 --save-dev
Running package.json scripts
- Using Yarn:
yarn run test
- Using npm:
npm run test
- Selenium
- Automating functional testing using Selenium
- Setting up your own test automation environment
- What build tools can do for you
- Comparison of build tools
- Webpack Boilerplate
- The many jobs of JS build tools
- How to keep your JS libraries up to date
#Why Data Structure?
#Data Structure API
#Conceptual
Intro
- The list is comprised of a series of nodes
- The head node is the node at the beginning of the list.
- Each node contains data and a link (or pointer) to the next node in the list.
- The list is terminated when a node’s link is null. This is called the tail node.
- Since the nodes use links to denote the next node in the sequence, the nodes are not required to be sequentially located in memory.
- allow for quick insertion and removal of nodes
- Can be unidirectional or bidirectional
- Common operations on a linked list may include:
- adding nodes
- removing nodes
- finding a node
- traversing (or travelling through) the linked list
Adding a new node
- Adding a new node to the beginning of the list requires you to link your new node to the current head node.
- This way, you maintain your connection with the following nodes in the list.
Removing a node
- If you accidentally remove the single link to a node, that node’s data and any following nodes could be lost to your application, leaving you with orphaned nodes.
- To properly maintain the list when removing a node from the middle of a linked list, you need to be sure to adjust the link on the previous node so that it points to the following node.
- Depending on the language, nodes which are not referenced are removed automatically. “Removing” a node is equivalent to removing all references to the node.
#JS Code
Constructor and Adding to head
const Node = require('./Node');
class LinkedList {
constructor() {
this.head = null;
}
addToHead(data) {
const newHead = new Node(data);
const currentHead = this.head;
this.head = newHead;
// check if there is a current head to the list, set the list’s head’s next node to currentHead
if (currentHead) {
this.head.setNextNode(currentHead);
}
}
}
module.exports = LinkedList;
Adding to tail
addToTail(data) {
let tail = this.head;
// check if there's no head, add the node to the head of the list
if(!tail) {
this.head = new Node(data);
} else { // iterate through the list until we find the last node
while (tail.getNextNode()) {
tail = tail.getNextNode(data);
}
// add a pointer from the last node to new tail
tail.setNextNode(new Node(data));
}
}
Removing the Head
removeHead() {
const removedHead = this.head;
// check if there is a head
if (!removedHead) {
return
}
// if there is a head, remove it by setting the list’s head = to the original head’s next node
if (removedHead.getNextNode() !== null) {
this.head = removedHead.getNextNode();
}
// return original head
return removedHead.data
}
Printing list
printList() {
let currentNode = this.head;
// This string will holds the data from every node in the list, start at the list's head
let output = '<head> ';
// iterate through the list, adding to the string as we go
while (currentNode !== null) {
output += currentNode.data + ' ';
currentNode = currentNode.getNextNode();
}
output += '<tail>';
console.log(output);
}
Using the Linked list
const LinkedList = require('./LinkedList');
const seasons = new LinkedList();
seasons.printList();
seasons.addToHead('summer');
seasons.addToHead('spring');
seasons.printList();
seasons.addToTail('fall');
seasons.addToTail('winter');
seasons.printList();
seasons.removeHead();
seasons.printList();
Additional Resources
#Swapping elements in a linked list
Given an input of a linked list, data1
, and data2
, the general steps for doing so is as follows:
- Iterate through the list looking for the node that matches
data1
to be swapped (node1
), keeping track of the node’s previous node as you iterate (node1Prev
) - Repeat step 1 looking for the node that matches
data2
(giving younode2
andnode2Prev
) - If
node1Prev
isnull
,node1
was the head of the list, so set the list’s head tonode2
- Otherwise, set
node1Prev
‘s next node tonode2
- If
node2Prev
isnull
, set the list’s head tonode1
- Otherwise, set
node2Prev
‘s next node tonode1
- Set
node1
‘s next node tonode2
‘s next node - Set
node2
‘s next node tonode1
‘s next node
const LinkedList = require('./LinkedList.js')
const testList = new LinkedList();
for (let i = 0; i <= 10; i++) {
testList.addToTail(i);
}
testList.printList();
swapNodes(testList, 2, 5);
testList.printList();
function swapNodes(list, data1, data2) {
console.log(`Swapping ${data1} and ${data2}:`);
let node1Prev = null;
let node2Prev = null;
let node1 = list.head;
let node2 = list.head;
if (data1 === data2) {
console.log('Elements are the same - no swap to be made');
return;
}
while (node1 !== null) {
if (node1.data === data1) {
break;
}
node1Prev = node1;
node1 = node1.getNextNode();
}
while (node2 !== null) {
if (node2.data === data2) {
break;
}
node2Prev = node2;
node2 = node2.getNextNode();
}
if (node1 === null || node2 === null) {
console.log('Swap not possible - one or more element is not in the list');
return;
}
if (node1Prev === null) {
list.head = node2;
} else {
node1Prev.setNextNode(node2);
}
if (node2Prev === null) {
list.head = node1;
} else {
node2Prev.setNextNode(node1);
}
let temp = node1.getNextNode();
node1.setNextNode(node2.getNextNode());
node2.setNextNode(temp);
}
Time and Space Complexity
- The worst case for time complexity in
swapNodes()
is if both while loops must iterate all the way through to the end (either if there are no matching nodes, or if the matching node is the tail). This means that it has a linear big O runtime ofO(n)
, since eachwhile
loop has aO(n)
runtime, and constants are dropped. - There are four new variables created in the function regardless of the input, which means that it has a constant space complexity of
O(1)
.
#Two-Pointer Linked List Techniques
#Practice: Singly Linked Lists
- Easy - Remove Linked List Elements
- Easy - Reverse Linked List
- Easy - Middle of the Linked List
- Medium - Remove Duplicates From Sorted List
- More Practice Problems
#Conceptual
Intro
-
Like a singly linked list, a doubly linked list is comprised of a series of nodes.
-
Each node contains data and two links (or pointers) to the next and previous nodes in the list.
-
Common operations on a doubly linked list may include:
- adding nodes to both ends of the list
- removing nodes from both ends of the list
- finding, and removing, a node from anywhere in the list
- traversing (or traveling through) the list
Adding to the Head
-
When adding to the head of the doubly linked list, we first need to check if there is a current head to the list. If there isn’t, then the list is empty, and we can simply make our new node both the head and tail of the list and set both pointers to null. If the list is not empty, then we will:
- Set the current head’s previous pointer to our new head
- Set the new head’s next pointer to the current head
- Set the new head’s previous pointer to
null
Adding to the Tail
-
Similarly, there are two cases when adding a node to the tail of a doubly linked list. If the list is empty, then we make the new node the head and tail of the list and set the pointers to
null
. If the list is not empty, then we:- Set the current tail’s next pointer to the new tail
- Set the new tail’s previous pointer to the current tail
- Set the new tail’s next pointer to
null
Removing the Head
- Removing the head involves updating the pointer at the beginning of the list. We will set the previous pointer of the new head (the element directly after the current head) to null, and update the head property of the list. If the head was also the tail, the tail removal process will occur as well.
Removing the Tail
- Similarly, removing the tail involves updating the pointer at the end of the list. We will set the next pointer of the new tail (the element directly before the tail) to null, and update the tail property of the list. If the tail was also the head, the head removal process will occur as well.
Removing from the Middle of the List
-
It is also possible to remove a node from the middle of the list. Since that node is neither the head nor the tail of the list, there are two pointers that must be updated:
- We must set the removed node’s preceding node’s next pointer to its following node
- We must set the removed node’s following node’s previous pointer to its preceding node
-
There is no need to change the pointers of the removed node, as updating the pointers of its neighboring nodes will remove it from the list. If no nodes in the list are pointing to it, the node is orphaned.
#JavaScript implementing
Node
class Node {
constructor(data) {
this.data = data;
this.next = null;
this.previous = null;
}
setNextNode(node) {
if (node instanceof Node || node === null) {
this.next = node;
} else {
throw new Error('Next node must be a member of the Node class')
}
}
setPreviousNode(node) {
if (node instanceof Node || node === null) {
this.previous = node;
} else {
throw new Error('Previous node must be a member of the Node class')
}
}
getNextNode() {
return this.next;
}
getPreviousNode() {
return this.previous;
}
}
module.exports = Node;
Doubly linked lists
const Node = require('./Node');
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
}
// Add to head
addToHead(data) {
const newHead = new Node(data);
const currentHead = this.head;
// check if there is a head, update previous and next node of the current head
if (currentHead) {
currentHead.setPreviousNode(newHead);
newHead.setNextNode(currentHead);
}
// set the list's head to the new head
this.head = newHead;
// if the list has no tail, set its tail to the new head
if (!this.tail) {
this.tail = newHead;
}
}
// Add to tail
addToTail(data) {
const newTail = new Node(data);
const currentTail = this.tail;
if (currentTail) {
currentTail.setNextNode(newTail);
newTail.setPreviousNode(currentTail);
}
this.tail = newTail;
if (!this.head) {
this.head = newTail;
}
}
// remove head
removeHead() {
const removedHead = this.head;
// if there is no head, nothing to remove
if (!removedHead) {
return
}
// set the list's head to the next node of the removed head node
this.head = removedHead.getNextNode();
// if the head has value, set the head's previous node to null
if (this.head) {
this.head.setPreviousNode(null);
}
// if the removed head was also the tail of the list
if (removedHead === this.tail) {
this.removeTail();
}
// return the old head
return removedHead.data;
}
// remove tail
removeTail() {
const removedTail = this.tail;
if (!removedTail) {
return;
}
this.tail = removedTail.getPreviousNode();
if (this.tail) {
this.tail.setNextNode(null);
}
if (removedTail === this.head) {
this.removeHead();
}
return removedTail.data;
}
// Remove node by data
removeByData(data) {
let nodeToRemove;
let currentNode = this.head;
while (currentNode) {
// check if current node's data matches data
if (currentNode.data === data) {
nodeToRemove = currentNode;
break;
}
currentNode = currentNode.getNextNode();
}
// if there was no matching node in the list
if (!nodeToRemove) {
return null
}
// Resetting pointers around the node
if (nodeToRemove === this.head) {
this.removeHead();
} else if (nodeToRemove === this.tail) {
this.removeTail();
} else { // not head or tail
const nextNode = nodeToRemove.getNextNode();
const previousNode = nodeToRemove.getPreviousNode();
/* remove the pointers to and from
nodeToRemove and have nextNode and
previousNode point to each other
*/
nextNode.setPreviousNode(previousNode);
previousNode.setNextNode(nextNode);
}
return nodeToRemove
}
printList() {
let currentNode = this.head;
let output = '<head> ';
while (currentNode !== null) {
output += currentNode.data + ' ';
currentNode = currentNode.getNextNode();
}
output += '<tail>';
console.log(output);
}
}
module.exports = DoublyLinkedList;
Using Doubly linked List
const DoublyLinkedList = require('./DoublyLinkedList.js');
const subway = new DoublyLinkedList();
subway.addToHead('TimesSquare');
subway.addToHead('GrandCentral');
subway.addToHead('CentralPark');
subway.printList();
subway.addToTail('PennStation');
subway.addToTail('WallStreet');
subway.addToTail('BrooklynBridge');
subway.printList();
subway.removeHead();
subway.removeTail();
subway.printList();
subway.removeByData('TimesSquare');
subway.printList();
#Additional resources
- Video: Doubly linked lists
- Interactive: Doubly linked lists
- Github Cheat sheet: Doubly linked lists
- Practice : Doubly linked lists
#Conceptual
#JavaScript implementing
#Additional resources
- Video: Stacks & Queues
- Interactive: Queues
- Github Cheat sheet: Queues
#Conceptual
#JavaScript implementing
#Additional resources
- Interactive: Stacks
- Github Cheat sheet: Stacks
- Behavioral interviews: how to prepare for anc ace interview questions
- How to succeed in a behavioral interview
- Cracking the coding interview chap 5
- Google - First Recurring Character
- Facebook - How Many Ways to Decode a Message
- Amazon - Recursive Staircase Problem
- Google - Universal Value Tree
Additional Resources:
- dailycodinproblem.com
- leetcode.com
For this project, you will build an application using everything you’ve learned. Unlike the previous projects, what you build is up to you. We will provide some ideas to get started but we want this project to be something that you are passionate about building.
Think about:
- An application that you wish exists but doesn’t
- What you can build to solve a problem that you, your family, or your friends have
Here are some starting ideas to inspire you:
- A movie finder application the Movie Database API
- A productivity app like Todoist
- A Pokedex using the Pokemon API
- A Gif browser using the Giphy API
These ideas require you to learn a few additional technologies on your own:
- A single/multiplayer game using Canvas or Phaser
- A mobile application using React Native
- A data visualization using D3.js
- A virtual reality application using React 360
- An interactive 3D visualization using Three.js
Project Requirements:
- Build the application using React and Redux
- Version control your application with Git and host the repository on GitHub
- Use a project management tool (GitHub Projects, Trello, etc.) to plan your work
- Write a README (using Markdown) that documents your project including:
- Wireframes
- Technologies used
- Features
- Future work
- Write unit tests for your components
- Write end-to-end tests for your application
- Users can use the application on any device (desktop to mobile)
- Users can use the application on any modern browser
- Users can access your application at a URL
- This means your application should be hosted online
- Users are delighted with a cohesive design system
- Users are delighted with animations and transitions
- Users are able to leave an error states
- Think about bad API calls, network failures. When an event like that happens, your app shouldn’t crash but provide a user a means to get back to a working state (retry button, go back button, etc.)
- Get 90+ scores on Lighthouse
- OPTIONAL: Get a custom domain name and use it for your application
- OPTIONAL: Set up a CI/CD workflow to automatically deploy your application when the master branch in the repository changes
- OPTIONAL: Make your application a progressive web app
If you’d like to build apps for iOS or Android, try React Native. It uses the same component hierarchy as React, which you already know!
JavaScript is powerful and available on every browser, but it lacks one major feature: types. TypeScript is an extension of JavaScript that adds types, which can save you time catching errors and provide fixes before you run code.
If you want to use React for a large-scale application, try Gatsby, an ecosystem of tools that optimizes everything around your application, making it faster, safer, scalable, and accessible.
If you plan on using or building APIs, look into GraphQL, a query language that makes it easier to work with APIs.