Back to Freecodecamp

Front-End Libraries Review

curriculum/challenges/english/blocks/review-front-end-libraries/6724e2dbf723fe1c8883cc69.md

latest75.9 KB
Original Source

--description--

JavaScript Libraries and Frameworks

  • JavaScript libraries and frameworks offer quick solutions to common problems and speed up development by providing pre-built code.

  • Libraries are generally more focused on providing solutions to specific tasks, such as manipulating the DOM, handling events, or managing AJAX requests.

  • A couple of examples of JavaScript libraries are jQuery and React.

  • Frameworks, on the other hand, provide a more defined structure for building applications. They often come with a set of rules and conventions that developers need to follow.

  • Examples of frameworks include Angular and Next.js, a meta framework for React.

  • Single-page applications (SPAs) are web applications that load a single HTML page and dynamically update that page as the user interacts with the application without reloading the entire page.

  • SPAs use JavaScript to manage the application's state and render content. This is often done using frameworks which provide great tools for building complex user interfaces.

  • Some issues surrounding SPAs include:

    • Screen readers struggle with dynamically updated content.
    • The URL does not change when the user navigates within the application, which can make it difficult to bookmark, backtrack or share specific pages.
    • Initial load time can be slow if the application is large as all the assets need to be loaded upfront.

React

  • React is a popular JavaScript library for building user interfaces and web applications.
  • A core concept of React is the creation of reusable UI components that can update and render independently as data changes.
  • React allows developers to describe how the UI should look like based on the application state. React then updates and renders the right components when the data or the state changes.

React Components

  • Components are the building blocks of React applications that allow developers to break down complex user interfaces into smaller, manageable pieces.
  • The UI is described using JSX, an extension of JavaScript syntax, that allows developers to write HTML-like code within JavaScript.
  • Components are basically JS functions or classes that return a piece of UI.

Here is an example of a simple React component that renders a greeting message:

jsx
function Greeting() {
  const name = 'Anna';
  return <h1>Welcome, {name}!</h1>;
}

To use the component, you can simply call:

jsx
  <Greeting />

Importing and Exporting React components

  • React components can be exported from one file and imported into another file.
  • Let's say you have a component named City in a file named City.js. You can export the component using the export keyword:
jsx
// City.js
function City() {
  return <p>New York</p>;
}

export default City;
  • To import the City component into another file, you can use the import keyword:
jsx
// App.js
import City from './City';

function App() {
  return (
    <div>
      <h1>My favorite city is:</h1>
      <City />
    </div>
  );
}
  • The default keyword is used as it is the default export from the City.js file.

  • You can also choose to export the component on the same line as the component definition like this:

jsx
export default function City() {
  return <p>New York</p>;
}

Setting up a React project using Vite

  • Project setup tools and CLIs provide a quick & easy way to start new projects, allowing developers to focus on writing code rather than dealing with configuration.
  • Vite is a popular project setup tool and can be used with React.
  • To create a new project with Vite, you can use the following command in your terminal:
bash
npm create vite@latest my-react-app -- --template react

This command creates a new React project named my-react-app using Vite's React template. In the project directory, you will see a package.json file with the project dependencies and commands listed in it.

  • To run the project, navigate to the project directory and run the following commands:
bash
cd my-react-app # path to the project directory
npm install # installs the dependencies listed in the package.json file
  • Once the dependencies are installed, you should notice a new folder in your project called node_modules.

  • The node_modules folder is where all the packages and libraries required by your project are stored.

  • To run your project, use the following command:

bash
npm run dev
  • After that, open your browser and navigate to http://localhost:5173 to see your React application running.

  • To actually see the code for the starter template, you can go into your project inside the src folder and you should see the App.jsx file.

Passing props in React components

  • In React, props (short for properties) are a way to pass data from a parent component to a child component. This mechanism is needed to create reusable and dynamic UI elements.
  • Props can be any JavaScript value. To pass props from a parent to a child component, you add the props as attributes when you use the child component in the parent's JSX. Here's a simple example:
jsx
// Parent component
function Parent() {
  const name = 'Anna';
  return <Child name={name} />;
}

// Child component
function Child(props) {
  return <h1>Hello, {props.name}!</h1>;
}

You can pass multiple props using the spread operator (...), after converting them to an object. Here's an example:

jsx
// Parent component
function Parent() {
  const person = {
    name: 'Anna',
    age: 25,
    city: 'New York'
  };
  return <Child {...person} />;
}

In this code, the spread operator {...person} converts the person object into individual props that are passed to the Child component.

Conditional rendering in React

  • Conditional rendering in React allows you to create dynamic user interfaces. It is used to show different content based on certain conditions or states within your application.
  • There are several ways to conditionally render content in React. One common approach is to use the ternary operator. Here's an example:
jsx
function Greeting({ isLoggedIn }) {
  return (
    <div>
      {isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please log in</h1>}
    </div>
  );
}
  • Another way to conditionally render content is to use the logical AND (&&) operator. This is useful when you want to render content only if a certain condition is met. Here's an example:
jsx
function Greeting({ user }) {
  return (
    <div>
      {user && <h1>Welcome, {user.name}!</h1>}
    </div>
  );
}

In the code above, the h1 element is only rendered if the user object is truthy.

You can also use a direct if statement this way:

js
function Greeting({ isLoggedIn }) {
  if (isLoggedIn) {
    return <h1>Welcome back!</h1>;
  }
  return <h1>Please sign in</h1>;
}

Rendering lists in React

  • Rendering lists in React is a common task when building user interfaces.
  • Lists can be rendered using the JS array map() method to iterate over an array of items and return a new array of JSX elements.
  • For example, if you have an array of names that you want to render as a list, you can do the following:
jsx
function NameList({ names }) {
  return (
    <ul>
      {names.map((name, index) => (
        <li key={`${name}-${index}`}>{name}</li>
      ))}
    </ul>
  );
}
  • Always remember to provide a unique key for each list item to help React manage the updating and rendering roles. With these techniques, you can create flexible, efficient, and dynamic lists in your React applications.

Inline styles in React

  • Inline styles in React allow you to apply CSS styles directly to JSX elements using JavaScript objects.
  • To apply inline styles in React, you can use the style attribute on JSX elements. The style attribute takes an object where the keys are CSS properties in camelCase and the values are the corresponding values. Here's an example:
js
function Greeting() {
  return (
    <h1
      style={{ color: 'blue', fontSize: '24px', backgroundColor: 'lightgray' }}
    >
      Hello, world!
    </h1>
  );
}

export default Greeting;

You can also extract the styles into a separate object and reference it in the style attribute this way:

jsx
function Greeting() {

  const styles = {
    color: 'blue',
    fontSize: '24px',
    backgroundColor: 'lightgray'
  };

  return <h1 style={styles}>Hello, world!</h1>;
}

export default Greeting;
  • Inline styles support dynamic styling by allowing you to conditionally apply styles based on props or state. Here is an example of how you can conditionally apply styles based on a prop:
jsx
function Greeting({ isImportant }) {

  const styles = {
    color: isImportant ? 'red' : 'black',
    fontSize: isImportant ? '24px' : '16px'
  };

  return <h1 style={styles}>Hello, world!</h1>;
}

export default Greeting;
  • In the code above, the color and fontSize styles are conditionally set based on the isImportant prop.

Working with Events in React

  • Synthetic Event System: This is React's way of handling events. It serves as a wrapper around the native events like the click, keydown, and submit events. Event handlers in React use the camel casing naming convention. (Ex. onClick, onSubmit, etc)

Here is an example of using the onClick attribute for a button element in React:

jsx
function handleClick() {
  console.log("Button clicked!");
}

<button onClick={handleClick}>Click Me</button>;

In React, event handler functions usually start with the prefix handle to indicate they are responsible for handling events, like handleClick or handleSubmit.

When a user action triggers an event, React passes a Synthetic Event object to your handler. This object behaves much like the native Event object in vanilla JavaScript, providing properties like type, target, and currentTarget.

To prevent default behaviors like browser refresh during an onSubmit event, for example, you can call the preventDefault() method:

jsx
function handleSubmit(event) {
  event.preventDefault();
  console.log("Form submitted!");
}

<form onSubmit={handleSubmit}>
  <input type="text" />
  <button>Submit</button>
</form>;

You can also wrap a handler function in an arrow function like this:

jsx
function handleDelete(id) {
  console.log("Deleting item:", id);
}

<button onClick={() => handleDelete(1)}>Delete Item</button>;

Working with State and the useState Hook

  • Definition for state: In React, state is an object that contains data for a component. When the state updates the component will re-render. React treats state as immutable, meaning you should not modify it directly.
  • useState() Hook: The useState hook is a function that lets you declare state variables in functional components.  Here is a basic syntax:
js
const [stateVariable, setStateFunction] = useState(initialValue);

In the state variable you have the following:

  • stateVariable holds the current state value
  • setStateFunction (the setter function) updates the state variable
  • initialValue sets the initial state

Here is a complete example for a Counter component:

jsx
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>{count}</h2>

      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default Counter;

Rendering and React Components

  • Definition: In React, rendering is the process by which components appear in the user interface (UI), usually the browser. The rendering process consists of three stages: trigger, render, and commit.

The trigger stage occurs when React detects that something has changed and the user interface (UI) might need to be updated. This change is often due to an update in state or props.

Once the trigger happens, React enters the render stage. Here, React re-evaluates your components and figures out what to display. To do this, React uses a lightweight copy of the "real" DOM called the virtual DOM. With the virtual DOM, React can quickly check what needs to change in the component.

The commit stage is where React takes the prepared changes from the virtual DOM and applies them to the real DOM. In other words, this is the stage where you see the final result on the screen.

Updating Objects and Arrays in State

  • Updating Objects in State: If you need to update an object in state, then you should make a new object or copy an existing object first, then set the state for that new object. Any object put into state should be considered as read-only. Here is an example of setting a user's name, age and city. The handleChange function is used to handle updates to the user's information:
jsx
import { useState } from "react";

function Profile() {
  const [user, setUser] = useState({ name: "John Doe", age: 31, city: "LA" });

  const handleChange = (e) => {
    const { name, value } = e.target;

    setUser((prevUser) => ({...prevUser, [name]: value}));
  };

  return (
    <div>
      <h1>User Profile</h1>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
      <p>City: {user.city}</p>

      <h2>Update User Age </h2>
      <input type="number" name="age" value={user.age} onChange={handleChange} />

      <h2>Update User Name </h2>
      <input type="text" name="name" value={user.name} onChange={handleChange} />

      <h2>Update User City </h2>
      <input type="text" name="city" value={user.city} onChange={handleChange} />
    </div>
  );
}

export default Profile;
  • Updating Arrays in State: When updating arrays in state, it is important not to directly modify the array using methods like push() or pop(). Instead you should create a new array when updating state:
jsx
const addItem = () => {
  const newItem = {
    id: items.length + 1,
    name: `Item ${items.length + 1}`,
  };

  // Creates a new array
  setItems((prevItems) => [...prevItems, newItem]);
};

If you want to remove items from an array, you should use the filter() method, which returns a new array after filtering out whatever you want to remove:

jsx
const removeItem = (id) => {
  setItems((prevItems) => prevItems.filter((item) => item.id !== id));
};

Referencing Values Using Refs

  • ref Attribute: You can access a DOM node in React by using the ref attribute. Here is an example to showcase a ref to focus an input element. The current property is used to access the current value of that ref:
jsx
import { useRef } from "react";

const Focus = () => {
  const inputRef = useRef(null);

  const handleFocus = () => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Enter text" />
      <button onClick={handleFocus}>Focus Input</button>
    </div>
  );
};

export default Focus;

Working with the useEffect Hook

  • useEffect() Hook: In React, an effect is anything that happens outside the component rendering process. That is, anything React does not handle directly as part of rendering the UI. Common examples include fetching data, updating the browser tab's title, reading from or writing to the browser's local storage, getting the user's location, and much more. These operations interact with the outside world and are known as side effects. React provides the useEffect hook to let you handle those side effects. useEffect lets you run a function after the component renders or updates.
js
import { useEffect } from "react";

useEffect(() => {
 // Your side effect logic (usually a function) goes here
}, [dependencies]);

The effect function runs after the component renders, while the optional dependencies argument controls when the effect runs.

Note that dependencies can be an array of "reactive values" (state, props, functions, variables, and so on), an empty array, or omitted entirely. Here's how all of those options control how useEffect works:

  • If dependencies is an array that includes one or more reactive values, the effect will run whenever they change.

  • If dependencies is an empty array, useEffect runs only once when the component first renders.

  • If you omit dependencies, the effect runs every time the component renders or updates.

How to Create Custom Hooks

  • Custom Hooks: A custom hook allows you to extract reusable logic from components, such as data fetching, state management, toggling, and side effects like tracking online status. In React, all built-in hooks start with the word use, so your custom hook should follow the same convention.

Here is an example of creating a useDebounce hook:

jsx
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

export { useDebounce };

Working with Forms in React

  • Controlled Inputs: This is when you store the input field value in state and update it through onChange events. This gives you complete control over the form data and allows instant validation and conditional rendering.
jsx
import { useState } from "react";

function App() {
  const [name, setName] = useState("");

  const handleChange = (e) => {
    setName(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(name);
  };

  return (
    <>
      <form onSubmit={handleSubmit}>
        <label htmlFor="name">Your name</label> 

        <input value={name} id="name" onChange={handleChange} type="text" />
        <button type="submit">Submit</button>
      </form>
    </>
  );
}

export default App;
  • Uncontrolled Inputs: Instead of handling the inputs through the useState hook, uncontrolled inputs in HTML maintain their own internal state with the help of the DOM. Since the DOM controls the input values, you need to pull in the values of the input fields with a ref.
jsx
import { useRef } from "react";

function App() {
 const nameRef = useRef();

 const handleSubmit = (e) => {
   e.preventDefault();
   console.log(nameRef.current.value);
 };

 return (
   <form onSubmit={handleSubmit}>
     <label htmlFor="name">Your</label>{" "}
     <input type="text" ref={nameRef} id="name" />
     <button type="submit">Submit</button>
   </form>
 );
}

export default App;

Working with the useActionState Hook

  • Server Actions: These are functions that run on the server to allow form handling right on the server without the need for API endpoints. Here is an example from a Next.js application:
js
"use server";

async function submitForm(formData) {
 const name = formData.get("name");
 return { message: `Hello, ${name}!` };
}

The "use server" directive marks the function as a server action.

  • useActionState Hook: This hook updates state based on the outcome of a form submission. Here's the basic syntax of the useActionState hook:
js
const [state, action, isPending] = useActionState(actionFunction, initialState, permalink);
  • state is the current state the action returns.
  • action is the function that triggers the server action.
  • isPending is a boolean that indicates whether the action is currently running or not.
  • actionFunction parameter is the server action itself.
  • initialState is the parameter that represents the starting point for the state before the action runs.
  • permalink is an optional string that contains the unique page URL the form modifies.

Data Fetching in React

  • Options For Fetching Data: There are many different ways to fetch data in React. You can use the native Fetch API, or third party tools like Axios or SWR.
  • Commonly Used State Variables When Fetching Data: Regardless of which way you choose to fetch your data in React, there are some pieces of state you will need to keep track of. The first is the data itself. The second will track whether the data is still being fetched. The third is a state variable that will capture any errors that might occur during the data fetching process.
js
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

Since data fetching is a side effect, it's best to use the Fetch API inside of a useEffect hook.

js
useEffect(() => {
  const fetchData = async () => {
    try {
      const res = await fetch("https://jsonplaceholder.typicode.com/posts");
      
      if (!res.ok) {
        throw new Error("Network response was not ok");
      }

      const data = await res.json();
      setData(data);
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  };

  fetchData();
}, []);

Then you can render a loading message if the data fetching is not complete, an error message if there was an error fetching the data, or the results.

jsx
if (loading) {
  return <p>Loading...</p>;
}

if (error) {
  return <p>{error.message}</p>;
}

return (
  <ul>
    {data.map((post) => (
      <li key={post.id}>{post.title}</li>
    ))}
  </ul>
);

If you want to use Axios, you need to install and import it:

bash
npm i axios
js
import axios from "axios";

Then you can fetch the data using axios.get:

js
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  const fetchData = async () => {
    try {
      const res = await axios.get(
        "https://jsonplaceholder.typicode.com/users"
      );
      setData(res.data);
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  };

  fetchData();
}, []);

To fetch data using the useSWR hook, you need to first install and import it.

bash
npm i swr
js
import useSWR from "swr";

Here is how you can use the hook to fetch data:

jsx
import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

const FetchTodos = () => {
 const { data, error } = useSWR(
   "https://jsonplaceholder.typicode.com/todos",
   fetcher
 );

 if (!data) {
   return <h2>Loading...</h2>;
 }
 if (error) {
   return <h2>Error: {error.message}</h2>;
 }

 return (
   <>
     <h2>Todos</h2>
     <div>
       {data.map((todo) => (
         <h3 key={todo.id}>{todo.title}</h3>
       ))}
     </div>
   </>
 );
};

export default FetchTodos;

Working with the useOptimistic Hook

  • useOptimistic Hook: This hook is used to keep UIs responsive while waiting for an async action to complete in the background. It helps manage "optimistic updates" in the UI, a strategy in which you provide immediate updates to the UI based on the expected outcome of an action, like waiting for a server response.

Here is the basic syntax:

js
const [optimisticState, addOptimistic] = useOptimistic(actualState, updateFunction);
  • optimisticState is the temporary state that updates right away for a better user experience.
  • addOptimistic is the function that applies the optimistic update before the actual state changes.
  • actualState is the real state value that comes from the result of an action, like fetching data from a server.
  • updateFunction is the function that determines how the optimistic state should update when called.

Here is an example of using the useOptimistic hook in a TaskList component:

js
"use client";

import { useOptimistic } from "react";

export default function TaskList({ tasks, addTask }) {
  const [optimisticTasks, addOptimisticTask] = useOptimistic(
    tasks,
    (state, newTask) => [...state, { text: newTask, pending: true }]
  );

  async function handleSubmit(e) {
    e.preventDefault();
    const formData = new FormData(e.target);

    addOptimisticTask(formData.get("task"));

    addTask(formData);
    e.target.reset();
  }

  return <></>;
}
  • startTransition: This is used to render part of the UI and mark a state update as a non-urgent transition. This allows the UI to be responsive during expensive updates. Here is the basic syntax:
js
startTransition(action)

The action performs a state update or triggers some transition-related logic. This ensures that urgent UI updates (like typing or clicking) are not blocked.

Working with the useMemo Hook

  • Memoization: This is an optimization technique in which the result of expensive function calls are cached (remembered) based on specific arguments. When the same arguments are provided again, the cached result is returned instead of re-computing the function.
  • useMemo Hook: This hook is used to memoize computed values. Here is an example of memoizing the result of sorting a large array. The expensiveSortFunction will only run when largeArray changes:
js
const memoizedSortedArray = useMemo(
  () => expensiveSortFunction(largeArray),
  [largeArray]
);

Working with the useCallback Hook

  • useCallback Hook: This is used to memoize function references.
js
const handleClick = useCallback(() => {
 // code goes here
}, [dependency]);
  • React.memo: This is used to memoize a component to prevent it from unnecessary re-renders when its prop has not changed.
js
const MemoizedComponent = React.memo(({ prop }) => {
 return (
   <>
   </>
 )
});

Dependency Management Tools

  • Dependency Definition: In software, a dependency is where one component or module in an application relies on another to function properly. Dependencies are common in software applications because they allow developers to use pre-built functions or tools created by others. The two core dependencies needed for a React project will be the react and react-dom packages:
json
"dependencies": {
  "react": "^18.3.1",
  "react-dom": "^18.3.1"
}
  • Package Manager Definition: To manage software dependencies in a project, you will need to use a package manager. A package manager is a tool used for installation, updates and removal of dependencies. Many popular programming languages like JavaScript, Python, Ruby and Java, all use package managers. Popular package managers for JavaScript include npm, Yarn and pnpm.
  • package.json File: This is a key configuration file in projects that contains metadata about your project, including its name, version, and dependencies. It also defines scripts, licensing information, and other settings that help manage the project and its dependencies.
  • package-lock.json File: This file will lock down the exact versions of all packages that your project is using. When you update a package, then the new versions will be updated in the lock file as well.
  • node_modules Folder: This folder contains the actual code for the dependencies listed in your package.json file, including both your project's direct dependencies and any dependencies of those dependencies.
  • Dev Dependencies: These are packages that are only used for development and not in production. An example of this would be a testing library like Jest. You would install Jest as a dev dependency because it is needed for testing your application locally but not needed to have the application run in production.
json
"devDependencies": {
  "@eslint/js": "^9.17.0",
  "@types/react": "^18.3.18",
  "@types/react-dom": "^18.3.5",
  "@vitejs/plugin-react": "^4.3.4",
  "eslint": "^9.17.0",
  "eslint-plugin-react": "^7.37.2",
  "eslint-plugin-react-hooks": "^5.0.0",
  "eslint-plugin-react-refresh": "^0.4.16",
  "globals": "^15.14.0",
  "vite": "^6.0.5"
}

React Router

  • Introduction: React Router is a third party library that allows you to add routing to your React applications. To begin, you will need to install React Router in an existing React project like this:
bash
npm i react-router

Then inside of the main.jsx or index.jsx file, you will need to setup the route structure like this:

jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router";
import App from "./App.jsx";

import "./index.css";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />} />
      </Routes>
    </BrowserRouter>
  </StrictMode>
);

The path and element are used to couple the URL and UI components together. In this case, we are setting up a route for the homepage that points to the App component.

  • Multiple Views and Route Setup: It is common in larger applications to have multiple views and routes setup like this:
jsx
<Routes>
  <Route index element={<Home />} />
  <Route path="about" element={<About />} />

  <Route path="products">
    <Route index element={<ProductsHome />} />
    <Route path=":category" element={<Category />} />
    <Route path=":category/:productId" element={<ProductDetail />} />
    <Route path="trending" element={<Trending />} />
  </Route>
</Routes>

The index prop in these examples is meant to represent the default route for a given path segment. So the Home component will be shown at the root / path while the ProductsHome component will be shown at the /products path.

  • Nesting Routes: You can nest routes inside other routes which results in the path of the child route being appended to the parent route's path.
jsx
<Route path="products">
  <Route path="trending" element={<Trending />} />
</Route>

In the example above, the path for the trending products will be products/trending.

  • Dynamic Segments: A dynamic segment is where any part of the URL path is dynamic.
jsx
<Route path=":category" element={<Category />} />

In this example we have a dynamic segment called category. When a user navigates to a URL like products/brass-instruments, then the view will change to the Category component and you can dynamically fetch the appropriate data based on the segment.

  • useParams Hook: This hook is used to access the dynamic parameters from a URL path.
js
import { useParams } from "react-router";

export default function Category() {
  let params = useParams();
}

React Frameworks

  • Introduction: React frameworks provide features like routing, image optimizations, data fetching, authentication and more. This means that you might not need to set up separate front-end and back-end applications for certain use cases. Examples of React Frameworks include Next.js and Remix.
  • Next.js Routing: This routing system includes support for dynamic routes, parallel routes, route handlers, redirects, internalization and more.

Here is an example of creating a custom request handler:

js
export async function GET() {
  const res = await fetch("https://example-api.com");
  const data = await res.json();

  return Response.json({ data });
}
  • Next.js Image Optimization: The Image component extends the native HTML img element and allows for faster page loads and size optimizations. This means that images will only load when they enter the viewport and the Image component will automatically serve correctly sized images for each device.
jsx
import Image from "next/image";

export default function Page() {
  return (
    <Image src="link-to-image-goes-here" alt="descriptive-title-goes-here" />
  );
}

Prop Drilling

  • Definition: Prop drilling is the process of passing props from a parent component to deeply nested child components, even when some of the child components don't need the props.

State Management

  • Context API: Context refers to when a parent component makes information available to child components without needing to pass it explicitly through props. createContext is used to create a context object which represents the context that other components will read. The Provider is used to supply context values to the child components.
js
import { useState, createContext } from "react";

const CounterContext = createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  return (
    <CounterContext.Provider value={{ count, setCount }}>
      {children}
    </CounterContext.Provider>
  );
};

export { CounterContext, CounterProvider };
  • Redux: Redux handles state management by providing a central store and strict control over state updates. It uses a predictable pattern with actions, reducers, and middleware. Actions are payloads of information that send data from your application to the Redux store, often triggered by user interactions. Reducers are functions that specify how the state should change in response to those actions, ensuring the state is updated in an immutable way. Middleware, on the other hand, acts as a bridge between the action dispatching and the reducer, allowing you to extend Redux's functionality (e.g., logging, handling async operations) without modifying the core flow.

  • Zustand: This state management solution is ideal for small to medium-scale applications. It works by using a useStore hook to access state directly in components and pages. This lets you modify and access data without needing actions, reducers, or a provider.

Debugging React Components Using the React DevTools

  • React Developer Tools: This is a browser extension you can use in Chrome, Firefox and Edge to inspect React components and identify performance issues. For Safari, you will need to install the react-devtools npm package. After installing React DevTools and opening a React app in the browser, open the browser developer tools to access the two extra tabs provided for debugging React – Components and Profiler.
  • Components Tab: This tab displays each component for you in a tree view format. Here are some things you can do in this tab:
    • view the app's component hierarchy
    • check and modify props, states, and context values in real time
    • check the source code for each selected component
    • log the component data to the console
    • inspect the DOM elements for the component
  • Profiler Tab: This tab helps you analyze component performance. You can record component performance so you can identify unnecessary re-renders, view commit durations, and subsequently optimize slow components.

React Server Components

  • Definition: React Server Components are React components that render exclusively on the server, sending only the final HTML to the client. This means those components can directly access server-side resources and dramatically reduce the amount of JavaScript sent to the browser.

Differences Between Real and Perceived Performance

  • Perceived Performance: This is how users perceive the performance of a website. It's how they evaluate it in terms of responsiveness and reliability. This is a subjective measurement, so it's hard to quantify it, but it's very important, since the user experience determines the success or failure of a website.
  • Real Performance: This is the objective and measurable performance of the website. It's measured using metrics like page load time, server response time, and rendering time. These measurements are influenced by multiple factors related to the network and to the code itself.

Techniques for Improving Perceived Performance

  • Lazy Loading: This technique reduces the initial load time as much as possible by loading non-essential resources in the background.
  • Minimize Font Delays: If your website has custom fonts, you should also try to minimize font loading delays, since this may result in flickering or in showing the fallback font while the custom font is being loaded. A suggestion for this is using a fallback font that is similar to the custom font, so in case this happens, the change will be more subtle.
  • Use of Loading Indicators: Showing a loading indicator for a long-running process as soon as the user clicks on an element can help the user feel connected and engaged with the process, making the wait time feel shorter.

Core Performance Concepts

  • Source order: This refers to the way HTML elements are structured in the document. This determines what loads first and can significantly impact performance and accessibility.

Some best practices for source order include:

  • Placing critical content such as headings, navigation or main text higher in the HTML structure.
  • Deferring non-essential scripts such as ones for analytics, or third-party widgets, so they don't block rendering.
  • Using progressive enhancement, to ensure the core experience works even before styles and scripts load. Progressive enhancement is a way of building websites and applications based on the idea that you should make your page work with HTML first.

Here is an example of good source order, using the best practices we just went through:

html
<h1>Welcome to FastSite!</h1>
<p>Critical information loads first.</p>
<script src="slow-script.js" defer></script>
  • Critical Rendering Path: This is the sequence of steps the browser follows to convert code into pixels on the screen.
  • Latency: This is the time it takes for a request to travel between the browser and the server. So in other words, high latency equals slow pages.

Some ways of reducing latency include:

  • Using CDNs, or Content Delivery Networks, to serve files from closer locations.
  • Enabling compression using things such as Gzip to reduce file sizes.
  • Optimizing images and using lazy loading.
html

Improving INP

  • Definition: INP (Interaction to Next Paint) assesses a page's overall responsiveness by measuring the time from when a user interacts, like a click or key press, to the next time the browser updates the display. A lower INP indicates a more responsive page.

Here are some ways to improve INP:

  • Reduce main thread work by breaking up long JavaScript tasks.
  • Use requestIdleCallback() for non-critical scripts. This will queue a function to be called during a browser's idle periods.
  • Defer or lazy-load heavy assets which were covered earlier.
  • Optimize event handlers. If these handlers run too frequently or perform heavy operations, they can slow down the page and increase INP. The solution for this is debouncing. Debouncing ensures that the function only runs after the user stops typing for a short delay - so for example 300ms. This prevents unnecessary calculations and improves performance.

How Rendering Works in the Browser

  • How Rendering works: First the browser parses the HTML and builds the DOM. Next, the browser processes the CSS, constructing the CSS Object Model, or CSSOM. This is another tree structure that dictates how elements should be styled. Finally, the browser paints the pixels to the screen, rendering each element based on the calculated styles and layout. In complex pages, this might involve multiple layers that are composited together to form the final visual output.

How Performance Impacts Sustainability

  • Background Information: The internet accounts for around 2% of global carbon emissions—that's the same as the airline industry! Every byte transferred requires electricity, from data centers to user devices. Larger files and inefficient scripts mean more power consumption. A high-performance website isn't just faster, it also reduces unnecessary processing and energy use.

Ways to Reduce Page Loading Times

  • Optimize Media Assets: Large images and videos are common culprits for slow load times. By optimizing these assets, you can significantly speed up your site. This includes things like compressing images, using modern formats like WebP and using lazy loading for assets.
  • Leverage Browser Caching: Caching allows browsers to store parts of your website locally, reducing load times for returning visitors.
  • Minify and Compress Files: Reducing the size of your files can lead to quicker downloads. This includes reducing the size of transmitted files and minifying CSS and JavaScript files.

Improving "time to usable"

  • Definition: "time to usable" is the interval from when a user requests a page to when they can meaningfully interact with it. To improve "time to usable" you can lazy load your asset or minimize render-blocking resources.

Key Metrics for Measuring Performance

  • First Contentful Paint or FCP: It measures how quickly the first piece of content—text or image—appears on the screen. A good FCP is regarded as a time below 1.8 seconds, and a poor FCP is above 3 seconds. You can check your FCP using Chrome DevTools and checking the performance tab.
  • Total Blocking Time: This shows how long the main thread is blocked by heavy JavaScript tasks. If Total Blocking Time (TBT) is high, users experience sluggish interactions. To improve TBT, break up long tasks and defer non-essential scripts.
  • Bounce Rate: This is the percentage of visitors who leave without interacting. If your site has high bounce rates it might be because your page is too slow.
  • Unique Users: This metric tracks how many individual visitors come to your site. To view the Bounce Rate and Unique Users, you can use Google Analytics. It will allow you to monitor these metrics and improve engagement.

Common Performance Measurement Tools

  • Chrome DevTools: Chrome DevTools is a built-in tool inside Google Chrome that lets you analyze and debug performance in real-time. DevTools will show loading times, CPU usage, and render delays. It's especially useful for measuring First Contentful Paint, or FCP, is how fast a user sees the first visible content. If your website feels slow, DevTools will help you spot the bottlenecks.
  • Lighthouse: This is an automated tool that checks performance, SEO, and accessibility.
  • WebPageTest: This tool lets you test how your site loads from different locations and devices. This tool gives you a detailed breakdown of your site's Speed Index, Total Blocking Time, and other key performance metrics. If you want to know how real users experience your site globally, WebPageTest is the tool for that.
  • PageSpeed Insights: This tool analyzes your website and suggests quick improvements for both mobile and desktop. It will tell you what's slowing your site down and give specific recommendations like optimizing images, removing render-blocking scripts, and reducing server response times. PageSpeed Insights is a fast and easy way to check how Google sees your site's performance.
  • Real User Monitoring: RUM tools track actual user behavior, showing how real visitors experience your site. Popular RUM tools include Google Analytics, which tracks page load times and bounce rates, and New Relic or Datadog, which monitor real-time performance issues. If you want data from actual users, RUM tools are essential.

Working with Performance Web APIs

  • Definition: Performance Web APIs let developers track how efficiently a webpage loads and responds directly in the code. These APIs allow you to measure page load times, track rendering and interaction delays and analyze JavaScript execution time.
  • performance.now(): This API gives you high-precision timestamps (in milliseconds) to measure how long different parts of your site take to load.
js
const start = performance.now();  
// Run some code here  
const end = performance.now();  

console.log(`Execution time: ${end - start}ms`);
  • Performance Timing API: This API gives you a breakdown of every stage of page loading, from DNS lookup to DOMContentLoaded.
js
const timing = performance.timing;  

const pageLoadTime = timing.loadEventEnd - timing.navigationStart;  
console.log(`Page load time: ${pageLoadTime}ms`);
  • PerformanceObserver: This API listens for performance events such as layout shifts, long tasks, and user interactions.
js
const observer = new PerformanceObserver((list) => {  
  list.getEntries().forEach((entry) => {  
    console.log(`Long task detected: ${entry.duration}ms`);  
  });  
});  

observer.observe({ type: "longtask", buffered: true });

Techniques for Improving CSS Performance

  • CSS Animations: Animating certain CSS properties, such as dimensions, position, and layout, triggers a process called "reflow", during which the browser recalculates the position and geometry of certain elements on the page. This requires a repaint, which is computationally expensive. Therefore, it's recommended to reduce the number of CSS animations as much as possible or at least give the user an option to toggle them on or off.

Techniques for Improving JavaScript Performance

  • Code Splitting: Splitting your JavaScript code into modules that perform critical and non-critical tasks is also helpful. This way, you'll be able to preload the critical ones as soon as possible and defer the non-critical ones to render the page as fast as possible.
  • DOM Manipulation: Remember that DOM Manipulation refers to the process of dynamically changing the content of a page with JavaScript by interacting with the Document Object Model (DOM). Manipulating the DOM is computationally expensive. By reducing the amount of DOM manipulation in your JavaScript code will improve performance.

CSS Frameworks

  • CSS frameworks: CSS frameworks can speed up your workflow, create a uniform visual style across a website, make your design look consistent across multiple browsers, and keep your CSS code more organized.
  • Popular CSS frameworks: Some of the popular CSS frameworks are Tailwind CSS, Bootstrap, Materialize, and Foundation.
  • Potential disadvantages:
    • The CSS provided by the framework might conflict with your custom CSS.
    • Your website might look similar to other websites using the same framework.
    • Large frameworks might cause performance issues.

Two Types of CSS Frameworks

  • Utility-first CSS frameworks: These frameworks have small classes with specific purposes, like setting the margin, padding, or background color. You can assign these small classes directly to the HTML elements as needed. Tailwind CSS is categorized as a utility-first CSS framework.

Here is an example of using Tailwind CSS to style a button.

html
<button class="bg-blue-500 text-white font-bold py-2 px-4 rounded-full hover:bg-blue-700">
  Button
</button>
  • Component-based CSS frameworks: These frameworks have pre-built components with pre-defined styles that you can add to your website. The components are available in the official documentation of the CSS framework, and you can copy and paste them into your project. Bootstrap is categorized as a component-based CSS framework.

Here is an example of using Bootstrap to create a list group. Instead of applying small classes to your HTML elements, you will add the entire component, including the HTML structure.

html
<div class="card" style="width: 25rem;">
  <ul class="list-group list-group-flush">
    <li class="list-group-item">HTML</li>
    <li class="list-group-item">CSS</li>
    <li class="list-group-item">JavaScript</li>
  </ul>
</div>

Tailwind CSS

Tailwind is a utility-first CSS framework. Instead of writing custom CSS rules, you build your designs by combining small utility classes directly in your HTML.

Responsive Design Utilities

Tailwind uses prefixes such as sm:, md:, and lg: to apply styles at different screen sizes.

html
<div class="w-full md:w-1/2 lg:flex-row">
  Responsive layout
</div>

Flexbox Utilities

Classes like flex, flex-col, justify-around, and items-center make it easy to create flexible layouts.

html
<div class="flex flex-col md:flex-row justify-around items-center">
  <p>Column on small screens</p>
  <p>Row on medium and larger screens</p>
</div>

Grid Utilities

Tailwind includes utilities for CSS Grid, like grid, grid-cols-1, and md:grid-cols-3.

html
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
  <div class="bg-gray-100 p-4">Column 1</div>
  <div class="bg-gray-100 p-4">Column 2</div>
  <div class="bg-gray-100 p-4">Column 3</div>
</div>

Spacing Utilities

Utilities like mt-8, mx-auto, p-4, and gap-4 help create consistent spacing without writing CSS.

html
<div class="mt-8 p-4 bg-indigo-600 text-white">
  Spaced content
</div>

Typography Utilities

Utilities like uppercase, font-bold, font-semibold, and text-4xl control text appearance.

You can set font sizes that adjust at breakpoints, such as text-3xl and md:text-5xl.

html
<h1 class="text-3xl md:text-5xl font-semibold text-center">
  Responsive Heading
</h1>

Colors and Hover States

Tailwind provides a wide color palette, such as text-red-700, bg-indigo-600, and bg-gray-100.

Classes like hover:bg-pink-600 make interactive effects simple.

html
<a href="#" class="bg-pink-500 hover:bg-pink-600 text-white px-4 py-2 rounded-md">
  Hover Me
</a>

Borders, Rings, and Effects

  • Borders: border-2 border-red-300 adds borders with specified thickness and colors.
  • Rings: ring-1 ring-gray-300 creates outline-like effects often used for focus or cards.
  • Rounded corners and scaling: Classes like rounded-md, rounded-xl, and scale-105 add polish.
html
<div class="p-6 rounded-xl ring-2 ring-fuchsia-500 scale-105">
  Highlighted card
</div>

Gradients

Tailwind supports gradient utilities like bg-gradient-to-r from-fuchsia-500 to-indigo-600.

html
<div class="p-4 text-white bg-gradient-to-r from-fuchsia-500 to-indigo-600">
  Gradient background
</div>

CSS Preprocessors

  • CSS preprocessor: A CSS preprocessor is a tool that extends standard CSS. It compiles the code with extended syntax into a native CSS file. It can be helpful for writing cleaner, reusable, less repetitive, and scalable CSS for complex projects.
  • Features: Some of the features that can be provided by CSS preprocessors are variables, mixins, nesting, and selector inheritance.
  • Popular CSS preprocessors: Some of the popular CSS preprocessors are Sass, Less, and Stylus.
  • Potential disadvantages:
    • Compiling the CSS rules into standard CSS might cause overhead.
    • The compiled code may be difficult to debug.

Sass

  • Sass: It is one of the most popular CSS preprocessors. Sass stands for "Syntactically Awesome Style Sheets."
  • Features supported by Sass: Sass supports features like variables, nested CSS rules, modules, mixins, inheritance, and operators for basic mathematical operations

Two Syntaxes Supported by Sass

  • SCSS syntax: The SCSS (Sassy CSS) expands the basic syntax of CSS. It is the most widely used syntax for Sass. SCSS files have an .scss extension.

Here is an example of defining and using a variable in SCSS.

scss
$primary-color: #3498eb;

header {
  background-color: $primary-color;
}
  • Indented syntax: The indented syntax was Sass's original syntax and is also known as the "Sass syntax."

Here is an example of defining and using a variable in the indented syntax.

sass
$primary-color: #3498eb

header
  background-color: $primary-color

Mixins

  • Mixins: Mixins allow you to group multiple CSS properties and their values under the name and reuse that block of CSS code throughout your stylesheet.

Here is an example of defining a mixin in SCSS syntax. In this case, the mixin is called center-flex. It has three CSS properties to center elements using flexbox.

scss
@mixin center-flex {
  display: flex;
  justify-content: center;
  align-items: center;
}

Here is an example of using the mixin you defined.

scss
section {
  @include center-flex;
  height: 500px;
  background-color: #3289a8;
}

Manual and Automated Testing

  • Manual Testing: In manual testing, a tester will manually go through each part of the application and test out different features to make sure it works correctly. If any bugs are uncovered in the testing process, the tester will report those bugs back to the software team so they can be fixed.
  • Automated Testing: In automated testing, you can automate your tests by writing a separate program that checks whether your application behaves as expected.

Unit Testing

  • Unit Testing: In unit testing, you test each function to ensure that everything is working as expected. Unit tests can also serve as a form of documentation for your application because they are meant to represent the expected behavior for your code.
  • Single Responsibility Principle: The single responsibility principle recommends keeping each function small and responsible for one thing.
  • Common JavaScript Testing Frameworks: Some common testing frameworks include Jest, Mocha, and Vitest. Jest is a popular testing framework for unit tests.

Here is an example of unit tests using Jest.

First, you can create a function that is responsible for returning a newly formatted string:

javascript
export function getFormattedWord(str) {
  if (!str) return '';
  return str.charAt(0).toUpperCase() + str.slice(1);
}

In a separate getFormattedWord.test.js file, you can write some tests to verify that the function is working as expected. The getFormattedWord.test.js file will look like this:

javascript
import { getFormattedWord } from "./getFormattedWord.js";
import { test, expect } from "jest";

test('capitalizes the first letter of a word', () => {
  expect(getFormattedWord('hello')).toBe('Hello');
});
  • expect Function: The expect function is used to test a value.
  • Matcher: Matcher is a function that checks whether the value behaves as expected. In the above example, the matcher is toBe(). Jest has a variety of matchers.

To use Jest, you first need to install the jest package by using npm i jest. You will also need to add a script to your package.json file like this:

js
"scripts": {
  "test": "jest"
},

Then, you can run the command npm run test to run your tests.

Software Development Lifecycle

  • Different Stages of Software Development Lifecycle:

    • Planning Stage: The development team collects requirements for the proposed work from the stakeholders.
    • Design Stage: The software team breaks down the requirements and decides on the best approaches for solutions.
    • Implementation Stage: The software team breaks down the requirements into manageable tasks and implements them.
    • Testing Stage: This involves manual and automated testing for the new work. Sometimes, the team tests out the application throughout the entire development stage to catch and fix any issues that come up.
    • Deployment Stage: The team deploys the new changes to a build or testing environment.
    • Maintenance Stage: This involves fixing any issues that arise from customers in the production application.
  • Different Models of the Software Development Lifecycle:

    • Waterfall Model: The Waterfall model is where each phase of the lifecycle needs to be completed before the next phase can begin.
    • Agile Model: The Agile model focuses on iterative development by breaking down work into sprints.

BDD and TDD

  • TDD: Test-driven development is a methodology that emphasizes writing tests first. Writing tests before building out features provides real-time feedback to developers during the development process.
  • BDD: Behavior-driven development is the approach of aligning a series of tests with business goals. The test scenarios in BDD should be written in a language that can be understood by both technical and non-technical individuals. An example of such syntax is Gherkin.
  • BDD Testing Frameworks: Examples of BDD testing frameworks include Cucumber, JBehave, and SpecFlow.

Assertions in Unit Testing

  • Assertion: Assertions are used to test that the code is behaving as expected.
  • Assertion Libraries: Chai is a commonly used assertion library. Other common JavaScript assertion libraries are should.js and expect.js.

Here is an example of an assertion using Chai that checks that the return value from the addThreeAndFour function is equal to the number 7:

js
assert.equal(addThreeAndFour(), 7);
  • Best Practices: Regardless of which assertion library you use, you should write clear assert and failure messages that will help you understand which tests are failing and why.

Mocking, Faking, and Stubbing

  • Mocking: Mocking is the process of replacing real data with false data that simulates the behavior of real components. For example, you could mock the API response in testing instead of making continuous API calls to fetch the data.
  • Stubbing: Stubs are objects that return pre-defined responses or dummy data for an expected behavior in an application. For example, you can stub the behavior for a database connection in your tests without having to rely on an actual database connection.
  • Faking: Fakes are simplified versions of real components without the complexity or side effects. For example, you can fake a database by storing the data in memory instead of interacting with the real database. This will allow you to mimic database operations in memory, which will be much faster than dealing with the real database.

Functional Testing

  • Functional Testing: Functional testing checks if the features and functions of the application work as expected. The goal of functional testing is to test the system as a whole against multiple scenarios.
  • Non-Functional Testing: Non-functional testing focuses on things like performance and reliability.
  • Smoke testing: Smoke testing is a preliminary check on the system for basic or critical issues before beginning more extensive testing.

End-to-End Testing

  • End-to-End Testing: End-to-end testing, or E2E, tests real-world scenarios from the user's perspective. End-to-end tests help ensure that your application behaves correctly and is predictable for users. However, it is time-consuming to set up, design, and maintain.
  • End-to-End Testing Frameworks: Playwright is a popular end-to-end testing framework developed by Microsoft. Other examples of end-to-end testing tools include Cypress, Selenium, and Puppeteer.

Here is an example of E2E tests from the freeCodeCamp codebase using Playwright. The beforeEach hook will run before each of the tests. The tests check that the donor has a supporter link in the menu bar, as well as a special stylized border around their avatar:

js
test.beforeEach(async ({ page }) => {
  execSync("node ./tools/scripts/seed/seed-demo-user --set-true isDonating");
  await page.goto("/donate");
});

...

test("The menu should have a supporters link", async ({ page }) => {
  const menuButton = page.getByTestId("header-menu-button");
  const menu = page.getByTestId("header-menu");

  await expect(menuButton).toBeVisible();
  await menuButton.click();

  await expect(menu).toBeVisible();

  await expect(page.getByRole("link", { name: "Supporters" })).toBeVisible();
});

test("The Avatar should have a special border for donors", async ({ page }) => {
  const container = page.locator(".avatar-container");
  await expect(container).toHaveClass("avatar-container gold-border");
});

Usability Testing

  • Usability Testing: Usability testing is when you have real users interacting with the application to discover if there are any design, user experience, or functionality issues in the app. Usability testing focuses on the intuitiveness of the application by users.
  • Four Common Types of Usability Testing:
    • Explorative: Explorative usability testing involves users interacting with the different features of the application to better understand how they work.
    • Comparative: Comparative testing is where you compare your application's user experience with similar applications in the marketplace.
    • Assessment: Assessment testing is where you study how intuitive the application is to use.
    • Validation: Validation testing is where you identify any major issues that will prevent the user from using the application effectively.
  • Usability Testing Tools: Examples of tools for usability testing include Loop11, Maze, Userbrain, UserTesting, and UXTweak.

Compatibility Testing

  • Compatibility Testing: The goal of compatibility testing is to ensure that your application works in different computing environments.
  • Different Types of Compatibility Testing:
    • Backwards Compatibility: Backwards compatibility refers to when the software is compatible with earlier versions.
    • Forwards Compatibility: Forwards compatibility refers to when the software and systems will work with future versions.
    • Hardware Compatibility: Hardware compatibility is the software's ability to work properly in different hardware configurations.
    • Operating Systems Compatibility: Operating systems compatibility is the software's ability to work on different operating systems, such as macOS, Windows, and Linux distributions like Ubuntu and Fedora.
    • Network Compatibility: Network compatibility means the software can work in different network conditions, such as different network speeds, protocols, security settings, etc.
    • Browser Compatibility: Browser compatibility means the web application can work consistently across different browsers, such as Google Chrome, Safari, Firefox, etc.
    • Mobile Compatibility: It is important to ensure that your software applications work on a variety of Android and iOS devices, including phones and tablets.

Performance Testing

  • Performance Testing: In performance testing, you test an application's speed, responsiveness, scalability, and stability under different workloads. The goal is to resolve any type of performance bottleneck.
  • Different Types of Performance Testing:
    • Load Testing: Load testing determines how a system behaves during normal and peak load times.
    • Stress Testing: Stress testing is where you test your application in extreme loads and see how well your system responds to the higher load.
    • Soak Testing (Endurance Testing): Soak testing or endurance testing is a type of load testing where you test the system with a higher load for an extended period of time.
    • Spike Testing: Spike testing is where you dramatically increase and decrease the loads and analyze the system's reactions to the changes.
    • Breakpoint Testing (Capacity Testing): Breakpoint testing or capacity testing is where you slowly increment the load over time to the point where the system starts to fail or degrade.

Security Testing

  • Security Testing: Security testing helps identify vulnerabilities and weaknesses.
  • Security Principles:
    • Confidentiality: This protects against the release of sensitive information to other recipients that aren't the intended recipient.
    • Integrity: This involves preventing malicious users from modifying user information.
    • Authentication: This involves verifying the user's identity to ensure that they are allowed to use that system.
    • Authorization: This is the process of determining what actions authenticated users are allowed to perform or which parts of the system they are permitted to access.
    • Availability: This ensures that information and services are available to authorized users when they need it.
    • Non-Repudiation: This ensures that both the sender and recipient have proof of delivery and verification of the sender's identity. It protects against the sender denying having sent the information.
  • Common Security Threats:
    • Cross-Site Scripting (XSS): XSS attacks happen when an attacker injects malicious scripts into a web page and then executes them in the context of the victim's browser.
    • SQL Injection: SQL injection allows malicious users to inject malicious code into a database.
    • Denial-of-Service (DoS) Attack: DoS attack is when malicious users flood a website with a high number of requests or traffic, causing the server to slow down and possibly crash, making the site unavailable to users.
  • Categories of Security Testing Tools:
    • Static Application Security Testing: These tools evaluate the source code for an application to identify security vulnerabilities.
    • Dynamic Application Security Testing: These tools interface with the application's front-end to uncover potential security weaknesses. DAST tools do not have access to the source code.
  • Penetration Testing (pentest): Penetration testing is a type of security testing that involves creating simulated cyberattacks on the application to identify any vulnerabilities in the system.

A/B Testing

  • A/B Testing: A/B testing involves comparing two versions of a page or application and studying which version performs better. It is also known as bucket or split testing. A/B testing allows you to make more data-driven decisions and continually improve the user experience.
  • Tools for A/B Testing: Examples of tools to use for A/B testing include GrowthBook and LaunchDarkly.

Alpha and Beta Testing

Once the initial development and software testing are complete, it is important to have the application tested by testers and real users. This is where alpha and beta testing come in.

  • Alpha Testing: Alpha testing is done by a select group of testers who go through the application to ensure there are no bugs before it is released into the marketplace. Alpha testing is part of acceptance testing and utilizes both white and black box testing techniques.
  • Beta Testing: Beta testing is where the application is made available to real users. Users can interact with the application and provide feedback. Beta testing is also a form of user acceptance testing.
  • Acceptance Testing: Acceptance testing ensures that the software application meets the business requirements and the needs of users before its release.
  • Black Box Testing: Black box testing only focuses on the expected behavior of the application.
  • White Box Testing: White box testing involves the tester knowing the internal components and performing tests on them.

Regression Testing

  • Regression: Regression refers to situations where new changes unintentionally break existing functionality.
  • Regression Testing: Regression testing helps catch regression issues. In regression testing, you re-run functional tests against parts of your application to ensure that everything still works as expected.
  • Tools for Regression Testing: Tools that you can use to perform regression testing include Puppeteer, Playwright, Selenium, and Cypress.
  • Techniques for Regression Testing:
    • Unit Regression Testing: This is where you have a list of items that need to be tested each time major changes or fixes are implemented into the app.
    • Partial Regression Testing: This involves targeted approaches to ensure that new changes have not broken specific aspects of the application.
    • Complete Regression Testing: This runs tests against all the functionalities in the codebase. This is the most time-consuming and detailed option.
  • Retesting: Retesting is used to check for known issues and ensure that they have been resolved. In contrast, regression testing searches for unknown issues that might have occurred through recent changes in the codebase.

What is TypeScript

  • JavaScript: JavaScript is a dynamically-typed language. This means that variables can receive any values at run time. The challenge of a dynamically-typed language is that the lack of type safety can introduce errors.

For example, even if your JavaScript function expects an array, you can still call it with a number:

javascript
const getRandomValue = (array) => {
  return array[Math.floor(Math.random() * array.length)];
}

console.log(getRandomValue(10));

The console output for the example above will be undefined.

  • TypeScript: TypeScript extends the JavaScript language to include static typing, which helps catch errors caused by type mismatches before you run your code.

For example, you can define a type for the array parameter as follows:

typescript
const getRandomValue = (array: string[]) => {
  return array[Math.floor(Math.random() * array.length)];
}

This type definition tells TypeScript that the array parameter must be an array of strings. Then, when you call getRandomValue and pass it a number, you get an error called a compiler error.

  • Compiler: You first need to compile TypeScript code into regular JavaScript. When you run the compiler, TypeScript will evaluate your code and throw an error for any issues where the types don't match.

Data Types in TypeScript

  • Primitive Data Types in TypeScript: For the primitive data types stringnullundefinednumberboolean, and bigint, TypeScript offers corresponding type keywords.
typescript
let str: string = "Naomi";
let num: number = 42;
let bool: boolean = true;
let nope: null = null;
let nada: undefined = undefined;
  • Array: You can define an array of specific type with two different syntaxes.
typescript
const arrOne: string[] = ["Naomi"];
const arrTwo: Array<string> = ["Camperchan"];
  • Objects: You can define the exact structure of an object.
typescript
const obj: { a: string, b: number } = { a: "Naomi", b: 42 };

If you want an object with any keys, but where all values must be strings, there are two ways to define it:

typescript
const objOne: Record<string, string> = {};
const objTwo: { [key: string]: string } = {};
  • Other Helpful Types in TypeScript:

    • any: any indicates that a value can have any type. It tells the compiler to stop caring about the type of that variable.
    • unknown: unknown tells TypeScript that you do care about the type of the value, but you don’t actually know what it is. unknown is generally preferred over any.
    • void: This is a special type that you’ll typically only use when defining functions. Functions that don’t have a return value use a return type of void.
    • never: It represents a type that will never exist.
  • type Keyword: This keyword is like const, but instead of declaring a variable, you can declare a type.

It is useful for declaring custom types such as union types or types that include only specific values:

typescript
type stringOrNumber = string | number;
type bot = "camperchan" | "camperbot" | "naomi";
  • interface: Interfaces are like classes for types. They can implement or extend other interfaces, are specifically object types, and are generally preferred unless you need a specific feature offered by a type declaration.
typescript
interface wowie {
  zowie: boolean;
  method: () => void;
}
  • Defining Return Type: You can also define the return type of the function.

The example below defines the return value as a string. If you try to return anything else, TypeScript will give a compiler error.

typescript
const getRandomValue = (array: string[]): string => {
  return array[Math.floor(Math.random() * array.length)];
}

Generics

  • Defining Generic Type: You can define a generic type and refer to it in your function. It can be thought of as a special parameter you provide to a function to control the behavior of the function's type definition.

Here is an example of defining a generic type for a function:

typescript
const getRandomValue = <T>(array: T[]): T => {
  return array[Math.floor(Math.random() * array.length)];
}
const val = getRandomValue([1, 2, 4])

The <T> syntax tells TypeScript that you are defining a generic type T for the function. T is a common name for generic types, but you can use any name.

Then, you tell TypeScript that the array parameter is an array of values matching the generic type, and that the returned value is a single element of that same type.

  • Specifying Type Argument in Function Call: You can pass a type argument to a function call using angle brackets between the function name and its parameters.

Here is an example of passing a type argument to a function call:

typescript
const email = document.querySelector<HTMLInputElement>("#email");
console.log(email.value);

This tells TypeScript that the element you expect to find will be an input element.

Type Narrowing

  • Narrowing by Truthiness: In the example below, you get a compiler error trying to access the value property of email because email might be null.
typescript
const email = document.querySelector<HTMLInputElement>("#email");
console.log(email.value);

You can use a conditional statement to confirm email is truthy before accessing the property:

typescript
const email = document.querySelector<HTMLInputElement>("#email");
if (email) {
  console.log(email.value);
}

Truthiness checks can also work in the reverse direction:

typescript
const email = document.querySelector<HTMLInputElement>("#email");
if (!email) {
  throw new ReferenceError("Could not find email element!")
}
console.log(email.value);

Throwing an error ends the logical execution of this code, which means when you reach the console.log() call, TypeScript knows email cannot be null.

  • Optional Chaining: Optional chaining ?. is also a form of type narrowing, under the same premise that the property access can't happen if the email value is null.
typescript
const email = document.querySelector<HTMLInputElement>("#email");
console.log(email?.value);
  • typeof Operator: You can use a conditional to check the type of the variable using the typeof operator.
typescript
const myVal = Math.random() > 0.5 ? 222 : "222";
if (typeof myVal === "number") {
  console.log(myVal / 10);
}
  • instanceof Keyword: If the object comes from a class, you can use the instanceof keyword to narrow the type.
typescript
const email = document.querySelector("#email");

if (email instanceof HTMLInputElement) {
    console.log(email.value);
}
  • Casting: When TypeScript cannot automatically determine the type of a value, such as the result from request.json() method in the example below, you'll get a compiler error. One way to resolve this is by casting the type, but doing so weakens TypeScript's ability to catch potential errors.
typescript
interface User {
    name: string;
    age: number;
}

const printAge = (user: User) => 
  console.log(`${user.name} is ${user.age} years old!`)

const request = await fetch("url")
const myUser = await request.json() as User;
printAge(myUser);
  • Type Guard: Instead of casting the type, you can write a type guard:
typescript
interface User {
    name: string;
    age: number;
}

const isValidUser = (user: unknown): user is User => {
  return !!user && 
    typeof user === "object" &&
    "name" in user &&
    "age" in user;
}

The user is User syntax indicates that your function returns a boolean value, which when true means the user value satisfies the User interface.

tsconfig File

  • tsconfig.json: TypeScript's compiler settings live in a tsconfig.json file in the root directory of your project.
json
{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./prod",
    "lib": ["ES2023"],
    "target": "ES2023",
    "module": "ES2022",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "strict": true
  },
  "exclude": ["test/"]
}

Here are descriptions of the compiler options used in the example above:

  • compilerOptions: The compilerOptions property is where you control how the TypeScript compiler behaves.
  • rootDir and outDir: The rootDir and outDir tell TypeScript which directory holds your source files, and which directory should contain the transpiled JavaScript code.
  • lib: The lib property determines which type definitions the compiler uses, and allows you to include support for specific ES releases, the DOM, and more.
  • module and moduleResolution: The module and moduleResolution work in tandem to manage how your package uses modules - either CommonJS or ECMAScript.
  • esModuleInterop: The esModuleInterop allows for smoother interoperability between CommonJS and ES modules by automatically creating namespace objects for imports.
  • skipLibCheck: The skipLibCheck option skips validating .d.ts files that aren't referenced by imports in your code.
  • strict: The strict flag enables several checks, such as ensuring proper handling of nullable types and warning when TypeScript falls back to any.
  • exclude: The top-level exclude property tells the compiler to ignore these TypeScript files during compilation.

--assignment--

Review the Front-End Libraries topics and concepts.