curriculum/challenges/english/blocks/review-front-end-libraries/6724e2dbf723fe1c8883cc69.md
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:
Here is an example of a simple React component that renders a greeting message:
function Greeting() {
const name = 'Anna';
return <h1>Welcome, {name}!</h1>;
}
To use the component, you can simply call:
<Greeting />
City in a file named City.js. You can export the component using the export keyword:// City.js
function City() {
return <p>New York</p>;
}
export default City;
City component into another file, you can use the import keyword:// 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:
export default function City() {
return <p>New York</p>;
}
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.
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:
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.
// 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:
// 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.
function Greeting({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please log in</h1>}
</div>
);
}
&&) operator. This is useful when you want to render content only if a certain condition is met. Here's an example: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:
function Greeting({ isLoggedIn }) {
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
}
return <h1>Please sign in</h1>;
}
map() method to iterate over an array of items and return a new array of JSX elements.function NameList({ names }) {
return (
<ul>
{names.map((name, index) => (
<li key={`${name}-${index}`}>{name}</li>
))}
</ul>
);
}
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:
function Greeting() {
const styles = {
color: 'blue',
fontSize: '24px',
backgroundColor: 'lightgray'
};
return <h1 style={styles}>Hello, world!</h1>;
}
export default Greeting;
function Greeting({ isImportant }) {
const styles = {
color: isImportant ? 'red' : 'black',
fontSize: isImportant ? '24px' : '16px'
};
return <h1 style={styles}>Hello, world!</h1>;
}
export default Greeting;
color and fontSize styles are conditionally set based on the isImportant prop.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:
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:
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:
function handleDelete(id) {
console.log("Deleting item:", id);
}
<button onClick={() => handleDelete(1)}>Delete Item</button>;
useState HookuseState() Hook: The useState hook is a function that lets you declare state variables in functional components. Here is a basic syntax:const [stateVariable, setStateFunction] = useState(initialValue);
In the state variable you have the following:
stateVariable holds the current state valuesetStateFunction (the setter function) updates the state variableinitialValue sets the initial stateHere is a complete example for a Counter component:
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;
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.
handleChange function is used to handle updates to the user's information: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;
push() or pop(). Instead you should create a new array when updating state: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:
const removeItem = (id) => {
setItems((prevItems) => prevItems.filter((item) => item.id !== id));
};
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: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;
useEffect HookuseEffect() 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.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.
use, so your custom hook should follow the same convention.Here is an example of creating a useDebounce hook:
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 };
onChange events. This gives you complete control over the form data and allows instant validation and conditional rendering.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;
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.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;
useActionState Hook"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: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.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.
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.
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:
npm i axios
import axios from "axios";
Then you can fetch the data using axios.get:
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.
npm i swr
import useSWR from "swr";
Here is how you can use the hook to fetch data:
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;
useOptimistic HookuseOptimistic 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:
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:
"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: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.
useMemo HookuseMemo 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:const memoizedSortedArray = useMemo(
() => expensiveSortFunction(largeArray),
[largeArray]
);
useCallback HookuseCallback Hook: This is used to memoize function references.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.const MemoizedComponent = React.memo(({ prop }) => {
return (
<>
</>
)
});
react and react-dom packages:"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
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."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"
}
npm i react-router
Then inside of the main.jsx or index.jsx file, you will need to setup the route structure like this:
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.
<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.
<Route path="products">
<Route path="trending" element={<Trending />} />
</Route>
In the example above, the path for the trending products will be products/trending.
<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.import { useParams } from "react-router";
export default function Category() {
let params = useParams();
}
Here is an example of creating a custom request handler:
export async function GET() {
const res = await fetch("https://example-api.com");
const data = await res.json();
return Response.json({ data });
}
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.import Image from "next/image";
export default function Page() {
return (
<Image src="link-to-image-goes-here" alt="descriptive-title-goes-here" />
);
}
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.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.
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.Some best practices for source order include:
Here is an example of good source order, using the best practices we just went through:
<h1>Welcome to FastSite!</h1>
<p>Critical information loads first.</p>
<script src="slow-script.js" defer></script>
Some ways of reducing latency include:
Here are some ways to improve INP:
requestIdleCallback() for non-critical scripts. This will queue a function to be called during a browser's idle periods.performance.now(): This API gives you high-precision timestamps (in milliseconds) to measure how long different parts of your site take to load.const start = performance.now();
// Run some code here
const end = performance.now();
console.log(`Execution time: ${end - start}ms`);
DOMContentLoaded.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.const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(`Long task detected: ${entry.duration}ms`);
});
});
observer.observe({ type: "longtask", buffered: true });
Here is an example of using Tailwind CSS to style a button.
<button class="bg-blue-500 text-white font-bold py-2 px-4 rounded-full hover:bg-blue-700">
Button
</button>
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.
<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 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.
Tailwind uses prefixes such as sm:, md:, and lg: to apply styles at different screen sizes.
<div class="w-full md:w-1/2 lg:flex-row">
Responsive layout
</div>
Classes like flex, flex-col, justify-around, and items-center make it easy to create flexible layouts.
<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>
Tailwind includes utilities for CSS Grid, like grid, grid-cols-1, and md:grid-cols-3.
<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>
Utilities like mt-8, mx-auto, p-4, and gap-4 help create consistent spacing without writing CSS.
<div class="mt-8 p-4 bg-indigo-600 text-white">
Spaced content
</div>
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.
<h1 class="text-3xl md:text-5xl font-semibold text-center">
Responsive Heading
</h1>
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.
<a href="#" class="bg-pink-500 hover:bg-pink-600 text-white px-4 py-2 rounded-md">
Hover Me
</a>
border-2 border-red-300 adds borders with specified thickness and colors.ring-1 ring-gray-300 creates outline-like effects often used for focus or cards.rounded-md, rounded-xl, and scale-105 add polish.<div class="p-6 rounded-xl ring-2 ring-fuchsia-500 scale-105">
Highlighted card
</div>
Tailwind supports gradient utilities like bg-gradient-to-r from-fuchsia-500 to-indigo-600.
<div class="p-4 text-white bg-gradient-to-r from-fuchsia-500 to-indigo-600">
Gradient background
</div>
.scss extension.Here is an example of defining and using a variable in SCSS.
$primary-color: #3498eb;
header {
background-color: $primary-color;
}
Here is an example of defining and using a variable in the indented syntax.
$primary-color: #3498eb
header
background-color: $primary-color
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.
@mixin center-flex {
display: flex;
justify-content: center;
align-items: center;
}
Here is an example of using the mixin you defined.
section {
@include center-flex;
height: 500px;
background-color: #3289a8;
}
Here is an example of unit tests using Jest.
First, you can create a function that is responsible for returning a newly formatted string:
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:
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.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:
"scripts": {
"test": "jest"
},
Then, you can run the command npm run test to run your tests.
Different Stages of Software Development Lifecycle:
Different Models of the Software Development Lifecycle:
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:
assert.equal(addThreeAndFour(), 7);
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:
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");
});
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.
For example, even if your JavaScript function expects an array, you can still call it with a number:
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.
For example, you can define a type for the array parameter as follows:
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.
string, null, undefined, number, boolean, and bigint, TypeScript offers corresponding type keywords.let str: string = "Naomi";
let num: number = 42;
let bool: boolean = true;
let nope: null = null;
let nada: undefined = undefined;
const arrOne: string[] = ["Naomi"];
const arrTwo: Array<string> = ["Camperchan"];
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:
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:
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.interface wowie {
zowie: boolean;
method: () => void;
}
The example below defines the return value as a string. If you try to return anything else, TypeScript will give a compiler error.
const getRandomValue = (array: string[]): string => {
return array[Math.floor(Math.random() * array.length)];
}
Here is an example of defining a generic type for a function:
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.
Here is an example of passing a type argument to a function call:
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.
value property of email because email might be null.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:
const email = document.querySelector<HTMLInputElement>("#email");
if (email) {
console.log(email.value);
}
Truthiness checks can also work in the reverse direction:
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.
?. is also a form of type narrowing, under the same premise that the property access can't happen if the email value is null.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.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.const email = document.querySelector("#email");
if (email instanceof HTMLInputElement) {
console.log(email.value);
}
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.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);
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 Filetsconfig.json: TypeScript's compiler settings live in a tsconfig.json file in the root directory of your project.{
"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.Review the Front-End Libraries topics and concepts.