React
Table of Contents
- Table of Contents
- Strict Mode
- Export and Import Overview
- Importing React in Functional Components
- Example Component
- Pure Components
- The children Property
- Using the Rest Operator (…rest) in React Component Props
- Compound Components
- Running or Binding Functions to Elements in React
- State
- Understanding Re-Renders, Reconciliation, and Re-Paints in React
- Refs
- Hooks
useMemo
HookuseCallback
Hook- Context
- Fetching Data
- Using
fetch()
- Tips & Tricks
Strict Mode
React Strict Mode is a development-only feature that helps identify potential problems in an application. It doesn’t affect production builds but enables additional checks and warnings to improve code quality.
What It Does:
- Double Invokes Certain Functions
- In development, React runs functions like component renders,
useEffect
, anduseState
initializers twice (but not event handlers) to detect side effects that aren’t properly handled.
- In development, React runs functions like component renders,
- Warns About Legacy API Usage
- Flags unsafe lifecycle methods (like
componentWillMount
).
- Flags unsafe lifecycle methods (like
- Detects Side Effects in
useEffect
- Helps catch side effects that aren’t properly cleaned up.
- Ensures Refs Forwarding Compliance
- Validates correct usage of
React.forwardRef()
.
- Validates correct usage of
- Detects Unexpected State Mutations
- Helps find components that modify state directly instead of using
setState
.
- Helps find components that modify state directly instead of using
Wrap our app (or parts of it) in <React.StrictMode>
to enable these checks.
Full documentation: https://react.dev/reference/react/StrictMode
Export and Import Overview
In React, default and named exports are used to organize and manage code modules. Default exports are designed for exporting a single, primary value or function from a module. When importing a default export, you do so without curly braces and can name the import whatever you like. For example, if a module exports a default component like export default Button;, you would import it with import Button from './Button';
. In contrast, named exports allow multiple values or functions to be exported from the same module. Each named export must be imported using curly braces to specify exactly which exports are being used. For instance, if a module exports several components like export const Button = ...;
and export const AnotherComponent = ...;
, you would import them with import { Button, AnotherComponent } from './Button';
. This distinction helps manage and structure code effectively, especially in larger projects.
Importing React in Functional Components
In modern React (version 17 and later), you no longer need to import React to use JSX in functional components. Previously, importing React was necessary because JSX was transformed into React.createElement calls, requiring the React object to be in scope.
Note: Importing React is still necessary if you are using React hooks, such as useState or useEffect, and other advanced features.
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
export default Counter;
Example Component
const Button = ({ text }) => {
return <button>{text}</button>;
};
export default Button;
Pure Components
In React, when you update state (e.g. by calling setCount
), the component re-renders — and so do all of its child components, even if their props haven’t changed.
To avoid unnecessary re-renders, we can make components pure: components that always render the same output for the same props.
React gives us a simple way to do this with React.memo
.
React.memo
Wrap a component with React.memo
to tell React:
“Only re-render this component if its props have changed.”
This is useful for components that don’t rely on state or context — like icons, decorations, or other static visual elements.
const Decoration = () => {
return <div className="decoration">⛵️</div>;
};
const PureDecoration = React.memo(Decoration);
Now PureDecoration
will not re-render when the parent updates, unless its props change.
The children Property
In React, every component has a children property that represents the content passed between the opening and closing tags of the component. This allows you to nest elements and pass dynamic content to components.
const Card = ({ children }) => {
return <div className="card">{children}</div>;
};
// Usage
<Card>
<h2>Title</h2>
<p>Content goes here</p>
</Card>;
Using the Rest Operator (…rest) in React Component Props
The rest operator (…rest) in React component props is used to gather all remaining props that aren’t explicitly destructured. It is helpful when you want to pass down additional props to a child component without needing to list them individually. Here’s an example:
const Button = ({ onClick, children, ...rest }) => {
return (
<button onClick={onClick} {...rest}>
{children}
</button>
);
};
function App() {
const handleOnClick = () => {
console.log("Logging in...");
};
return (
<main>
<Button onClick={handleOnClick} className="login-button" disabled={false}>
Log in with Google
</Button>
</main>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
Compound Components
Compound Components in React are a design pattern where a parent component manages the state and functionality, while its child components handle rendering. This allows for flexible and reusable UI components, such as tabs, dropdowns, or toggles.
Benefits:
- Prevents Prop Drilling: Instead of passing props through multiple layers, the parent component directly manages state and passes it to the children, making the structure cleaner.
- Flattens Structure: It avoids deeply nested components, simplifying the component hierarchy by allowing children to communicate directly with the parent.
- Increases Flexibility: Users can customize the internal structure and behavior of the components without modifying the core logic, making it easier to reuse and extend.
- This pattern promotes a more maintainable and scalable codebase, especially in complex UI components.
Compound components with dot syntax
Compound components with dot syntax is a React design pattern that allows you to group related components under a common parent component. This way, you can create more modular and flexible UI elements, where child components can communicate with their parent and share state. The dot syntax allows you to define subcomponents as properties of the main component.
Example: Accordion Component
File 1: Accordion.js
import React, { useState, createContext, useContext } from "react";
// Create a context to share state between components
const AccordionContext = createContext();
const Accordion = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
return (
<AccordionContext.Provider value={{ isOpen, toggle }}>
<div>{children}</div>
</AccordionContext.Provider>
);
};
// Accordion.Header component
Accordion.Header = ({ children }) => {
const { toggle } = useContext(AccordionContext);
return <button onClick={toggle}>{children}</button>;
};
// Accordion.Body component
Accordion.Body = ({ children }) => {
const { isOpen } = useContext(AccordionContext);
return isOpen ? <div>{children}</div> : null;
};
export default Accordion;
File 2: App.js
import React from "react";
import Accordion from "./Accordion";
const App = () => (
<Accordion>
<Accordion.Header>Click to Toggle</Accordion.Header>
<Accordion.Body>This is the accordion body.</Accordion.Body>
</Accordion>
);
export default App;
Running or Binding Functions to Elements in React
To run a function only when an event occurs, use a wrapper function:
<button onClick={() => setTheme("dark")}>Toggle theme</button>
Direct calls like onClick={setTheme('dark')}
are function calls that execute immediately during rendering. By using an anonymous wrapper function (() => setTheme('dark')
), you create a function definition
that React executes only when the event (e.g., a click) is triggered.
State
State in React is created using the useState hook. This hook returns an array with two elements:
- The current state value (e.g., count).
- A setter function (e.g., setCount) that updates the state.
You use array destructuring to assign these values:
const [count, setCount] = React.useState(0); // 0 is the initial value.
Supplying an Initial Value
You can supply an initial value directly:
const [count, setCount] = React.useState(0);
Or you can provide an initializer function, which is a function that returns the initial value:
const [count, setCount] = React.useState(() => {
return window.localStorage.getItem('saved-count');
});
This approach is useful for computations like fetching from localStorage because the function is called only once during the component’s first render.
Gotcha: Avoid passing the result of a computation directly (e.g., useState(window.localStorage.getItem(…))), as it runs on every render, which can hurt performance. Always wrap computations in a function when needed.
React State and Its Asynchronous Nature
In React, state setters (functions like setState) are asynchronous. This means that when you call a setter function to update the state, the update doesn’t happen immediately. Instead, React batches multiple state updates together for performance reasons, and the new state value may not be immediately available after the setter is called.
Here’s how it works:
- When you call a state setter (e.g., setCount(newCount)), React schedules a re-render with the new state.
- React doesn’t update the state immediately but waits until it has processed all state updates before updating the virtual DOM.
- The actual state change and re-render only happen once React finishes processing and commits those updates.
This asynchronous behavior can lead to some confusion, especially when you try to access the state value immediately after calling a setter function. The state won’t be updated until the next render cycle, which is why the old state might still be available right after the setter is called.
Understanding Re-Renders, Reconciliation, and Re-Paints in React
A re-render in React is the process where React compares the current UI snapshot with the previous one to identify changes. This comparison is called reconciliation, like a “spot-the-differences” game.
If React detects changes, it commits those updates to the DOM, ensuring it matches the latest snapshot. This may trigger the browser to re-paint, redrawing the affected pixels to reflect the updated UI.
However, not all re-renders lead to re-paints. If no changes are found during reconciliation, React skips DOM updates, avoiding unnecessary re-paints and improving performance.
Refs
In React, refs (short for “references”) are a way to directly access and manipulate DOM elements or React component instances without causing re-renders, unlike state. They provide a way to persist values between renders, but updating a ref does not trigger a re-render of the component.
Refs are commonly used for:
- Managing focus, text selection, or media playback.
- Triggering animations.
- Accessing form inputs.
Example
import React, { useRef } from "react";
const InputFocus = () => {
const inputRef = useRef(null);
const focusInput = () => {
// This sets focus on the input field without causing a re-render
inputRef.current.focus();
};
return (
<div>
<input
ref={inputRef}
type="text"
placeholder="Click the button to focus"
/>
<button onClick={focusInput}>Focus Input</button>
</div>
);
};
export default InputFocus;
In this example, useRef creates a ref, inputRef, which is attached to the input element. When the button is clicked, the input field is focused, but no re-render occurs. This is a common use case where refs help interact with the DOM directly.
Hooks
React Hooks are special functions introduced to allow functional components to manage things like state and side effects, making them as powerful as class components but with simpler, more readable code.
Purpose of Hooks:
- Hooks enable you to “hook into” React features such as state management, lifecycle methods, and context without needing to write class components.
- They make code more reusable and help you organize logic by functionality rather than lifecycle events (as class components do).
Built-in Hooks
React provides several built-in Hooks that handle common tasks:
- useState: Lets you add and manage local state in a functional component.
- useEffect: Manages side effects (e.g., API calls, timers) after renders.
- useRef: Stores references to DOM elements or values that persist across renders without causing re-renders.
- useContext: Accesses global values from a React context without passing props.
- useId: Allows to create and store a unique identifier on the component instance
Custom Hooks
You can also create your own custom Hooks to reuse stateful logic across multiple components. Custom Hooks are just regular functions that use built-in Hooks internally.
In summary, Hooks help simplify component logic, make code more reusable, and allow you to write functional components with features that used to require classes.
useID
Hook
The useId
hook is a built-in React hook that helps generate unique IDs, ensuring that each ID is consistent across renders. This is especially useful for situations where you need stable, unique identifiers for elements, such as for accessibility attributes (htmlFor
in <label>
tags) or linking form controls together.
Why is it important?
Using useId
ensures that IDs are consistent throughout the lifecycle of the component. Without it, you might run into issues where IDs are re-generated on every render, breaking accessibility features or causing other problems in your app. React automatically ensures that the generated IDs do not clash with other components, as long as each component instance uses useId
independently.
Example:
import { useId } from "react";
function Form() {
const id = useId(); // Generates a unique ID
return (
<div>
<label htmlFor={id}>Enter your name:</label>
<input id={id} type="text" />
</div>
);
}
export default Form;
Key Notes:
useId
should not be used for generating globally unique IDs (e.g., for elements across the entire document). It’s scoped to the component instance.- This hook is especially helpful in creating reusable components that require unique identifiers, ensuring consistency and accessibility without the need for manually managing IDs.
useEffect
Hook
The useEffect
hook in React is used to perform side effects in function components. Side effects can include things like fetching data, subscribing to a service, manually changing the DOM, or setting up timers.
It runs after the component renders, and you can control when it runs by passing dependencies (variables or state) as a second argument. If no dependencies are passed, useEffect
runs after every render. If you pass an empty array []
, it only runs once.
Example:
useEffect(() => {
document.title = `Application Name (${notifications})`;
}, []);
In short, useEffect
helps manage operations that need to happen outside of React’s normal render flow, like fetching data or subscribing to events.
Cleanup Effects
When a component unmounts or before re-running an effect due to dependency changes, React allows you to perform cleanup using a return function inside useEffect
. This is useful for clearing subscriptions, aborting API calls, or cleaning up event listeners to avoid memory leaks.
Example:
useEffect(() => {
const handleResize = () => {
console.log("Window resized");
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
In this example, the handleResize
function is added as an event listener when the component mounts and removed when the component unmounts. This prevents unnecessary event listeners from accumulating over time.
useMemo
Hook
Every time a React component re-renders, everything inside the function runs again — including any calculations or objects created inline.
Why use useMemo
?
useMemo
helps you:
- Avoid expensive recalculations on every render
- Stabilize object references to prevent unnecessary re-renders of child components (even if their values didn’t change)
1. Avoiding Expensive Calculations
If you have a slow function that depends on props or state, React will run it every render, even if the result hasn’t changed.
const result = useMemo(() => expensiveFunction(input), [input]);
React will only re-run expensiveFunction
when input
changes — otherwise, it returns the cached result.
Example:
const expensiveCalculation = (count) => {
console.log('Running expensive calculation...');
let total = 0;
for (let i = 0; i < 1e6; i++) {
total += count;
}
return total;
};
const App = () => {
const [count, setCount] = useState(0);
const calculated = useMemo(() => expensiveCalculation(count), [count]);
return (
<div>
<p>Result: {calculated}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
};
Without useMemo
, the function runs on every render — even if count
didn’t change.
2. Stabilizing Object Props
Even if an object has the same values, JavaScript sees it as a new reference each time:
const data = { label: "Click me" }; // new object every render
If you pass this to a memoized child component, React will still re-render it because the object is technically “new.”
Use useMemo
to create the object only when its contents change:
const buttonProps = useMemo(() => ({ label: "Click me" }), []);
Example:
const Button = React.memo(({ label }) => {
console.log('Button rendered');
return <button>{label}</button>;
});
const App = () => {
const [count, setCount] = useState(0);
const props = useMemo(() => ({ label: "Click me" }), []);
return (
<div>
<Button {...props} />
<button onClick={() => setCount(c => c + 1)}>Update Count</button>
</div>
);
};
Without useMemo
, Button
would re-render every time App
re-renders, even though the label
is always the same.
Summary
useMemo
helps avoid redoing work that doesn’t need to change.- It’s not for everything — only use it when you have expensive computations or when object identity matters for memoized components.
useCallback
Hook
Every time a component re-renders, all the functions defined inside it are re-created. That’s normally fine — but if you’re passing those functions to child components (especially memoized ones), it can cause unnecessary re-renders.
What useCallback
Does
useCallback
lets you memoize a function, so it only changes if its dependencies do.
const memoizedFn = useCallback(() => {
// do something
}, [dependencies]);
This helps you preserve the function identity across renders — which is important when passing it as a prop to a React.memo
component.
Why It Matters
Even if the function “does the same thing,” React sees it as a brand new function every time:
const handleClick = () => {
console.log('Clicked');
};
If you pass this to a memoized child component, it will re-render every time — because handleClick
is a new reference.
Example
const Button = React.memo(({ onClick }) => {
console.log('Button rendered');
return <button onClick={onClick}>Click</button>;
});
const App = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // <- memoized once
return (
<div>
<Button onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
};
Here:
Button
is memoized usingReact.memo
handleClick
is memoized usinguseCallback
- Result:
Button
won’t re-render whenApp
re-renders (unlesshandleClick
changes)
Summary
- Use
useCallback
when passing callbacks to memoized child components. - It helps prevent re-renders caused by changing function references.
- It’s like
useMemo
, but for functions instead of values.
Context
Context is used in React to share state or values between components without the need for prop drilling (passing props down multiple layers of components).
createContext()
: This function creates a context object that holds values that can be shared. The context object comes with two components:Provider
: Wraps the components where the context value is available.Consumer
: Optional, used to access the context value. (This is often replaced byuseContext
in functional components.)
useContext()
: This hook allows you to access the context value inside any component that is wrapped by the context’sProvider
. Instead of passing props, the component reads directly from the context.
This is particularly useful for global state management, such as theme switching, user authentication, and more.
Example: Theme Switcher (Light/Dark)
Here’s a simple theme switcher that toggles between a light theme (white background) and a dark theme (black background). It also updates an <h1>
tag to show the current theme (“Dark Theme” or “Light Theme”).
File 1: App.js
import React, { createContext, useContext, useState } from "react";
import ThemeToggleButton from "./Button";
// Create a context for the theme
const ThemeContext = createContext();
const App = () => {
const [isDarkTheme, setIsDarkTheme] = useState(false); // State for theme
// Toggle the theme between dark and light
const toggleTheme = () => setIsDarkTheme((prevTheme) => !prevTheme);
return (
<ThemeContext.Provider value={{ isDarkTheme, toggleTheme }}>
<div
style={{
backgroundColor: isDarkTheme ? "black" : "white",
color: isDarkTheme ? "white" : "black",
}}
>
<h1>{isDarkTheme ? "Dark Theme" : "Light Theme"}</h1>
<ThemeToggleButton />
</div>
</ThemeContext.Provider>
);
};
export default App;
export { ThemeContext };
File 2: Button.js
import React, { useContext } from "react";
import { ThemeContext } from "./App";
const ThemeToggleButton = () => {
const { toggleTheme } = useContext(ThemeContext); // Access context
return <button onClick={toggleTheme}>Toggle Theme</button>;
};
export default ThemeToggleButton;
How it works:
App.js
: Contains the main state (isDarkTheme
) and provides the current theme and thetoggleTheme
function through theThemeContext.Provider
.Button.js
: UsesuseContext
to get access to thetoggleTheme
function and triggers the theme switch when the button is clicked.
React’s Synthetic Event System
React uses a synthetic event system instead of directly handling native DOM events. Synthetic events are a cross-browser wrapper around the browser’s native events, ensuring consistent behavior across all browsers. These events are implemented by React and work the same way regardless of the browser being used.
Why does React do this? Several reasons:
-
It can ensure consistency, removing some edge-case issues with native events being implemented slightly differently between browsers.
-
It can include a few helpful “extras”, to improve the developer experience
Synthetic events are still tied to the browser’s native events under the hood, but React manages them to provide these benefits. If needed, you can still access the native event through event.nativeEvent.
<input
onChange={(event) => {
const realEvent = event.nativeEvent;
console.log(realEvent); // DOM InputEvent object
}}
/>
Fetching Data
Using fetch()
What Is fetch()
?
The fetch()
function is a built-in browser API used to make network requests (usually HTTP). It returns a Promise that resolves to a Response object. You can use fetch()
to request JSON, text, images, etc., but in modern React apps, it’s most commonly used to fetch data from APIs.
fetch('https://api.example.com/data')
By default, fetch()
performs a GET request.
The Fetch Lifecycle
Here’s what happens step-by-step:
- Call fetch with a URL and optional config (e.g., method, headers).
fetch()
returns a Promise.- Once the request completes, it resolves to a
Response
object (even if it’s 404 or 500). - You must check
response.ok
to determine if the request succeeded. - If OK, you usually call
response.json()
to parse the body. - If not OK, you can
throw
an error or handle it manually.
The Response
Object
When fetch()
completes, it gives you a Response
object with the following useful properties and methods:
Property / Method | Description |
---|---|
ok |
true if the HTTP status is between 200–299 |
status |
Numeric HTTP status (e.g. 200 , 404 , 500 ) |
statusText |
Text description of the status (e.g. "OK" , "Not Found" ) |
headers |
A Headers object representing the response headers |
json() |
Parses the response body as JSON (returns a Promise) |
text() |
Returns the response body as plain text |
body |
The raw stream of the body (used for streaming) |
url |
The final URL of the response (after redirects) |
redirected |
Boolean, true if response was redirected |
Example
const res = await fetch('/api/data');
console.log(res.ok); // true or false
console.log(res.status); // 200
console.log(await res.json()); // actual data
Basic Example Using async/await
GET Example
const fetchData = async () => {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch GET error:', error.message);
}
};
POST Example
const postData = async () => {
try {
const response = await fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Elisa',
age: 6
})
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch POST error:', error.message);
}
};
Example with Full State Management
import { useState, useEffect } from 'react';
const MyComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/data');
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const json = await res.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
};
Common Patterns & Hooks
Reusable Pattern: Utility Function
Move the fetch logic to a utility:
// utils/api.js
export async function fetchJson(url) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return await res.json();
}
Now your component stays clean:
useEffect(() => {
fetchJson('/api/data')
.then(setData)
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
Reusable Hook: useFetch
// hooks/useFetch.js
import { useEffect, useState } from 'react';
export function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (!isCancelled) setData(json);
} catch (err) {
if (!isCancelled) setError(err.message);
} finally {
if (!isCancelled) setLoading(false);
}
};
fetchData();
return () => {
isCancelled = true; // prevent state updates after unmount
};
}, [url]);
return { data, error, loading };
}
Using the Hook
const { data, error, loading } = useFetch('/api/data');
Q&A
Why Use try...catch
with fetch()
?
-
Catches network errors
If there’s no internet, or the domain is unreachable,fetch()
will throw — and onlytry...catch
can catch it. -
Catches JSON parsing errors
If the server response isn’t valid JSON,await response.json()
will throw — and withouttry...catch
, your app could crash. -
Centralizes error handling
Instead of relying on the caller to catch errors, you’re handling everything inside the function. Cleaner, safer, easier to debug. -
Prevents uncaught promise rejections
Withouttry...catch
, unhandled rejections may bubble up and break your app silently.
Bonus
You can log helpful messages, show user-friendly alerts, or retry the request — all inside your catch
.
Why Check response.ok
Manually?
fetch()
does not reject the promise on HTTP errors like 404 or 500. It only rejects if:
- The network is unreachable
- There’s a CORS error
- The request is blocked or times out (manually)
So:
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
In fetch()
: Why response.json()
?
When you do:
const res = await fetch('/api');
const data = await res.json();
That res.json()
:
- reads the response body stream
- turns it from text into a JavaScript object
- It’s actually a wrapper around
JSON.parse()
internally
Without Parsing: You Just Have a String
If you skipped .json()
and did:
const text = await res.text();
console.log(typeof text); // "string"
You’d get:
'{"name":"Elisa","age":6}'
Still useful, but not structured or usable until you parse it.
TL;DR
Term | Meaning |
---|---|
JSON |
A string format for representing data (JavaScript Object Notation) |
parse JSON |
Convert a JSON string into a real JavaScript object |
JSON.parse() |
Manual way to parse a string |
response.json() |
Async fetch response parser that returns an object |
What Does It Mean to “Parse JSON”?
To parse JSON means to take a JSON-formatted string (which is just text!) and convert it into a real JavaScript object that your code can work with.
JSON is a String
APIs send data over the network as plain text. For example, this is what an API might respond with:
'{"name":"Elisa","age":6}'
That’s not an object — it’s a string. You can’t access response.name
until you parse it.
Parsing = Turning JSON String Into JS Object
const json = '{"name":"Elisa","age":6}';
const data = JSON.parse(json);
console.log(data.name); // "Elisa"
console.log(typeof json); // "string"
console.log(typeof data); // "object"
JSON.parse()
reads the string and builds a real JavaScript object.
Why use JSON.stringify()
When you want to send data (e.g. a form), you use a POST request with a JSON body. That’s where JSON.stringify()
comes in. This converts a JavaScript object into a JSON-formatted string so it can be sent over the network.
const payload = { name: 'Elisa', age: 6 };
const jsonString = JSON.stringify(payload);
This will result in:
'{"name":"Elisa","age":6}'
Summary of Best Practices
- Always check
response.ok
— don’t assumefetch()
means success. - Use
await response.json()
only if you’re confident it’s JSON. - Throw custom errors when status isn’t OK.
- Use custom hooks or utils to clean up components.
- Handle cancellations if the component might unmount mid-fetch.
Fetching Data with SWR
SWR is a lightweight data-fetching library developed by Vercel that simplifies and streamlines how React applications handle remote data. It follows the stale-while-revalidate strategy, meaning it shows cached data instantly while revalidating it in the background—resulting in a fast and responsive UI with minimal effort.
Key Benefits of SWR
- Built-in caching – Avoids unnecessary network requests by reusing data when possible.
- Automatic revalidation – Keeps data fresh by re-fetching it when the window regains focus or when the network reconnects.
- Cleaner components – Removes the need for
useEffect
,useState
, and manual loading/error handling. - Support for polling – Data can be kept up-to-date on a timer with a simple
refreshInterval
option. - Great developer experience – Minimal configuration, yet powerful enough for real-world apps.
Basic Usage Example
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading user data.</p>;
return <div>Hello, {data.name}!</div>;
}
Tips & Tricks
Using status
State for Button Action States
When building buttons that trigger async actions—like submitting a form, fetching data, or performing any side effect—it’s super helpful to track the state of that action. A simple and effective pattern is using a single status
state:
const [status, setStatus] = React.useState('idle'); // 'idle' | 'loading' | 'success' | 'error'