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
  • 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
    • POST Request:

      • URL: http://localhost:5000/feedback
      • Body:
     {
       "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
  • Installation:

    • Run npm install concurrently
  • 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
   const [feedback, setFeedback] = useState([]);

Implementing a Loading Spinner

  • Loading State:
    • Use a state variable isLoading to track loading status
   const [isLoading, setIsLoading] = useState(true);
  • Updating Loading State:
    • Set isLoading to false after data is fetched
   fetchData().then(() => setIsLoading(false));
  • Displaying Spinner:
    • Conditional rendering based on isLoading
   {isLoading ? <Spinner /> : <FeedbackList feedback={feedback} />}

Adding Data and Setting Up a Proxy

  • Adding Feedback:
    • Use fetch with POST method
   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 with PUT method
   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 with DELETE method
   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.