Week 9: APIs and Advanced Hooks
INFO 253A: Front-end Web Architecture
Kay Ashaolu
React Chapter 7: APIs and HTTP Requests
Introduction to Client-Side Applications
-
Current State:
- Complete client-side application
- Feedback items are hardcoded
- Data stored only in memory
-
Limitations:
- Cannot persist data beyond the UI
- No backend interaction for data storage
Understanding Backend APIs and Databases
-
Databases:
- Data is usually stored in databases like MySQL, PostgreSQL, MongoDB
-
Backend API:
- Interacts with the database
- Fetches and manipulates data
- Returns data as JSON
-
Backend Technologies:
- Languages: Node.js, Python, PHP, C#, etc.
- Frameworks: Express (Node.js), Django (Python), Laravel (PHP)
Building a Mock Backend with JSON Server
-
What is JSON Server?
- Acts as a mock backend API
- Allows us to make HTTP requests
- Stores data in a
db.json
file
-
Benefits:
- Quick setup without writing actual backend code
- Can be replaced with a real backend later
HTTP Requests Basics
-
Client-Server Communication:
- Client (React app) makes requests to the server
- Server responds with data, usually in JSON format
-
HTTP Methods:
- GET: Retrieve data
- POST: Submit new data
- PUT/PATCH: Update existing data
- DELETE: Remove data
RESTful APIs and Endpoint Structure
-
REST API:
- Representational State Transfer
- Architectural style for designing networked applications
-
Endpoint Examples:
-
GET /feedback
- Retrieve all feedback items -
GET /feedback/{id}
- Retrieve a specific item -
POST /feedback
- Add a new item -
PUT /feedback/{id}
- Update an item -
DELETE /feedback/{id}
- Delete an item
-
HTTP Status Codes Overview
-
1xx Informational:
- Request received, continuing process
-
2xx Success:
- 200 OK: Successful request
- 201 Created: Resource created successfully
-
3xx Redirection:
- Further action needs to be taken
-
4xx Client Errors:
- 400 Bad Request: Client-side error
- 404 Not Found: Resource not found
-
5xx Server Errors:
- 500 Internal Server Error: Server-side error
Setting Up JSON Server
-
Installation:
- Run
npm install json-server
- Run
-
Creating
db.json
:- Example structure:
{
"feedback": [
{
"id": 1,
"rating": 10,
"text": "This is feedback item 1 coming from the backend"
},
{
"id": 2,
"rating": 9,
"text": "This is feedback item 2 coming from the backend"
},
{
"id": 3,
"rating": 8,
"text": "This is feedback item 3 coming from the backend"
}
]
}
Testing JSON Server with Postman
-
Using Postman:
- Tool for testing HTTP requests
-
Example Requests:
-
GET Request:
- URL:
http://localhost:5000/feedback
- Retrieves all feedback items
- URL:
-
POST Request:
- URL:
http://localhost:5000/feedback
- Body:
- URL:
-
{
"rating": 8,
"text": "New feedback item"
}
```
Running Client and Server Concurrently
-
Problem:
- Need to run React app and JSON Server simultaneously
-
Solution:
- Use
concurrently
package
- Use
-
Installation:
- Run
npm install concurrently
- Run
-
Update
package.json
:- Add the following scripts:
"scripts": {
"server": "json-server --watch db.json --port 5000",
"client": "react-scripts start",
"dev": "concurrently \"npm run server\" \"npm run client\""
}
Fetching Data from the Backend in React
-
Using
useEffect
Hook:- Fetch data when the component mounts
-
Example Code:
useEffect(() => {
fetch('/feedback?_sort=id&_order=desc')
.then(response => response.json())
.then(data => setFeedback(data));
}, []);
-
Managing State:
- Use
useState
to manage feedback data
- Use
const [feedback, setFeedback] = useState([]);
Implementing a Loading Spinner
-
Loading State:
- Use a state variable
isLoading
to track loading status
- Use a state variable
const [isLoading, setIsLoading] = useState(true);
-
Updating Loading State:
- Set
isLoading
tofalse
after data is fetched
- Set
fetchData().then(() => setIsLoading(false));
-
Displaying Spinner:
- Conditional rendering based on
isLoading
- Conditional rendering based on
{isLoading ? <Spinner /> : <FeedbackList feedback={feedback} />}
Adding Data and Setting Up a Proxy
-
Adding Feedback:
- Use
fetch
withPOST
method
- Use
fetch('/feedback', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newFeedback)
})
.then(response => response.json())
.then(data => setFeedback([...feedback, data]));
-
Automatic ID Assignment:
- JSON Server auto-increments IDs
- No need to generate IDs manually
Updating and Deleting Data from JSON Server
-
Updating Feedback:
- Use
fetch
withPUT
method
- Use
fetch(`/feedback/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedFeedback)
})
.then(response => response.json())
.then(data => {
// Update state with new data
});
Updating and Deleting Data from JSON Server
-
Deleting Feedback:
- Use
fetch
withDELETE
method
- Use
fetch(`/feedback/${id}`, { method: 'DELETE' })
.then(() => {
// Remove item from state
});
Importance of Understanding Web Architecture
-
Holistic View:
- Essential to understand frontend-backend interaction
- Facilitates better application design decisions
-
Performance Optimization:
- Efficient data fetching reduces load times
- Enhances user experience
-
Scalability:
- Well-architected applications handle growth effectively
- Easier maintenance and feature addition
Full-Stack Application Flow
-
Client-Side (Frontend):
- User interface built with React
- Manages state and user interactions
-
Server-Side (Backend):
- API endpoints handle requests
- Interacts with database to persist data
-
Communication:
- HTTP requests sent from client to server
- JSON data exchanged between frontend and backend
React Chapter 12: More Advanced React Hooks
- useRef
- useMemo
- useCallback
- Custom Hooks
useRef Hook - Overview and Example 1
-
useRef returns a mutable ref object with a
.current
property. - Used for:
- Accessing DOM elements directly.
- Storing mutable values that persist across renders.
- Example 1: Creating a DOM reference to manipulate or access a DOM element.
Code Sample: useRef to Access DOM Element
import { useRef } from 'react';
function UseRefExample1() {
const inputRef = useRef();
const onSubmit = e => {
e.preventDefault();
console.log(inputRef.current.value);
inputRef.current.value = '';
inputRef.current.focus();
};
return (
<form onSubmit={onSubmit}>
<label>Name:</label>
<input type="text" ref={inputRef} />
<button type="submit">Submit</button>
</form>
);
}
export default UseRefExample1;
useRef Example 2: Accessing Previous State
- useRef can store previous state values without causing re-renders.
- Useful for comparing current and previous state.
Code Sample: useRef to Access Previous State
import { useState, useEffect, useRef } from 'react';
function UseRefExample2() {
const [name, setName] = useState('');
const prevName = useRef('');
useEffect(() => {
prevName.current = name;
}, [name]);
return (
<div>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="Enter your name"
/>
<h2>Current Name: {name}</h2>
<h2>Previous Name: {prevName.current}</h2>
</div>
);
}
export default UseRefExample2;
useRef Example 3: Fixing Memory Leak Errors
- Avoid setting state on unmounted components.
- Use useRef to track if a component is mounted.
- Useful in asynchronous operations like fetch requests.
useRef to Fix Memory Leak
import { useState, useEffect, useRef } from 'react';
function UseRefExample3() {
const [data, setData] = useState(null);
const isMounted = useRef(true);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch('https://api.example.com/data');
if (isMounted.current) {
const result = await res.json();
setData(result);
}
} catch (error) {
console.error(error);
}
};
fetchData();
return () => {
isMounted.current = false;
};
}, []);
return <div>{data ? <div>{data.title}</div> : 'Loading...'}</div>;
}
export default UseRefExample3;
useMemo Hook - Overview and Example
- useMemo memoizes the result of an expensive function.
- Returns a memoized value.
- Recomputes only when dependencies change.
- Useful for performance optimization.
useMemo to Optimize Performance
import { useState, useMemo } from 'react';
function UseMemoExample() {
const [number, setNumber] = useState(0);
const [inc, setInc] = useState(0);
const sqrt = useMemo(() => {
console.log('Expensive function called');
return Math.sqrt(number);
}, [number]);
const onClick = () => {
setInc(prevInc => prevInc + 1);
};
return (
<div>
<h2>Square Root of {number}: {sqrt}</h2>
<input
type="number" value={number}
onChange={e => setNumber(Number(e.target.value))}
/>
<button onClick={onClick}>Re-render</button>
<p>Renders: {inc}</p>
</div>
);
}
export default UseMemoExample;
useCallback Hook - Overview and Example
- useCallback returns a memoized callback function.
- Prevents functions from being recreated on every render.
- Useful when passing callbacks to optimized child components.
Code Sample: useCallback with Child Component
// UseCallbackExample.js
import { useState, useCallback } from 'react';
import Button from './Button';
function UseCallbackExample() {
const [tasks, setTasks] = useState([]);
const addTask = useCallback(() => {
setTasks(prevTasks => [...prevTasks, 'New Task']);
}, [setTasks]);
return (
<div>
<Button addTask={addTask} />
{tasks.map((task, index) => (
<p key={index}>{task}</p>
))}
</div>
);
}
export default UseCallbackExample;
Code Sample: useCallback with Child Component
// Button.js
import React from 'react';
function Button({ addTask }) {
console.log('Button rendered');
return (
<button onClick={addTask}>Add Task</button>
);
}
export default React.memo(Button);
Custom Hooks - Introduction
- Custom Hooks allow you to extract component logic into reusable functions.
- Must start with the "use" prefix.
- Can use built-in hooks inside custom hooks.
Custom Hook 1: useFetch
- Simplifies data fetching logic.
- Handles loading, error, and data states.
- Reusable across components.
Code Sample: useFetch Hook
// useFetch.js
import { useState, useEffect } from 'react';
function useFetch(url, options) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortCont = new AbortController();
const fetchData = async () => {
try {
const res = await fetch(url, { ...options, signal: abortCont.signal });
if (!res.ok) throw new Error('Network response was not ok');
const json = await res.json();
setData(json);
setLoading(false);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
}
};
Code Sample: useFetch Hook
fetchData();
return () => abortCont.abort();
}, [url, options]);
return { data, loading, error };
}
export default useFetch;
// Usage in a component
import useFetch from './useFetch';
function CustomHookExample1() {
const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts', {});
if (loading) return <h3>Loading...</h3>;
if (error) return <h3>Error: {error.message}</h3>;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default CustomHookExample1;
Custom Hook 2: useLocalStorage
- Syncs state with local storage.
- Persists data between sessions.
- Used similarly to the useState hook.
useLocalStorage Hook
// useLocalStorage.js
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const [localStorageValue, setLocalStorageValue] = useState(() => {
try {
const itemFromStorage = window.localStorage.getItem(key);
return itemFromStorage ? JSON.parse(itemFromStorage) : initialValue;
} catch (err) {
console.log(err);
return initialValue;
}
});
const setValue = value => {
try {
const valueToStore =
value instanceof Function ? value(localStorageValue) : value;
setLocalStorageValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (err) {
console.log(err);
}
};
return [localStorageValue, setValue];
}
export default useLocalStorage;
Conclusion
- Advanced hooks optimize React applications.
- useRef, useMemo, and useCallback enhance performance.
- Custom Hooks promote code reusability and cleaner components.
Week 9 - APIs and Advanced Hooks
By kayashaolu
Week 9 - APIs and Advanced Hooks
Course Website: https://www.ischool.berkeley.edu/courses/info/253a
- 30