Search code examples
reactjsformsfetch-apijson-server

Can't use data in js object when displaying old data on a form


I have a update button that lets someone update information through a form in Update.js

I'm trying to get the old data stored in my db.json file through json-server and show them by default on the input fields in the Update form in Update.js

Fetching data part is done through a custom hook called useFetch.

But all I get is a blank page and the object I get from useFetch is null for some reason (I have included the error I got at bottom)

I've read similar questions here but none seems have the exact problem I'm facing.

Update.js

import { useParams } from "react-router-dom";
import useFetch from "./useFetch";
import { useState } from "react";

const Update = () => {
    
    const {id} = useParams();
    
    const {data: old, isLoading, error} =  useFetch(`http://localhost:8000/blogs/${id}`)

    // console.log(old.title);
    // line 14
    const [newTitle, setNewTitle] = useState(old.title);
    const [newAuthor, setNewAuthor] = useState('');
    const [newBody, setNewBody] = useState('');

    // const handleUpdate = () => {
    //     fetch(`http://localhost:8000/blogs/${id}`,{
    //     method: 'PUT',
    //     headers: {'Content-Type': 'application/json'},
    //     body:  JSON.stringify()
    // })
    // }

    return (     
        <div className="update-article">
            {/* <p>{old.title}</p> */}
            <form action="">
                <label htmlFor="">Blog Title</label>
                <input type="text" 
                    value={newTitle} 
                    onChange={(e) => setNewTitle(e.target.value)} 
                />
            </form>
        </div>       
     );
}
 
export default Update;

I have used useFetch to get data in another component and it works without issues there.

UseFetch.js

import { useState, useEffect } from "react";

const useFetch = (url) => {
        // vars for blog items in an array
        const [data, setData] = useState(null);

        //loader
        const [isLoading, setIsLoading] = useState(true);
    
        //error storing
        const [error, setError] = useState(null);

    useEffect(() => {
        fetch(url) 
                .then((response) => {
                    //check for fetch failure
                    if(!response.ok){
                        console.log("Server error");
                        throw Error("Could not fetch data for that resource");
                    }
                    return response.json();
                })
                .then((data) => {
                    setData(data);
                    setIsLoading(false);
                    setError(null);        
                })
                .catch((error) => {
                    setError(error.message);
                    setIsLoading(false);
                })         
        return () => console.log("Cleanup running...");
    }, [url]);


    return {data, isLoading, error}
}

export default useFetch;

When I try to run this, I get a blank page and in the browser console and I get Uncaught TypeError: old is null for line 14 in Update.js Image link

Line 14 is: const [newTitle, setNewTitle] = useState(old.title);

The blank page goes away and shows the input text box if I remove the old.title in line 14 (setting the initial value of newTitle to empty string or null)

And if I remove the old.title as initial value and type console.log(old.title). It shows the correct data in the console. So old is not null (?)

What I have tried:

  1. Tried to assign everything in the old object to temporary variables and assign them to the value field in imput box.

  2. Using defaultValue instead of value in input box.


Solution

  • The problem here is that on the first render old will be null as the api call is still pending. Now after the api call is fulfilled, you sure will receive something and so old is no more null.

    Here you are accessing title on first render as well as subsequent renders (old.title). So on first render you get the error Uncaught TypeError: old is null. You cann't access anything on null or undefined object.

    Now if we talk about your console log, the reason it's printing the actual value is if you are printing object x times in console, it will only give you the last updated value all the x times. Here is a wonderful answer you can take a look at.

    Anyways in order to solve the problem there are 2 ways.

    1. Add ? before accessing title from old so it will become old?.title for line 14 in Update.js. This is the easiest method.

    2. Add a useEffect hook to update newTitle state whenever there is a change in old and there also you need a check for null or undefined. Here is the updated code for Update.js.

      import { useParams } from 'react-router-dom';
      import useFetch from './useFetch';
      import { useState, useEffect } from 'react';
      
      const Update = () => {
        const { id } = useParams();
      
        const {
          data: old,
          isLoading,
          error,
        } = useFetch(`http://localhost:8000/blogs/${id}`);
      
        // console.log(old.title);
        // line 14
        const [newTitle, setNewTitle] = useState(null);
        const [newAuthor, setNewAuthor] = useState('');
        const [newBody, setNewBody] = useState('');
      
        // const handleUpdate = () => {
        //     fetch(`http://localhost:8000/blogs/${id}`,{
        //     method: 'PUT',
        //     headers: {'Content-Type': 'application/json'},
        //     body:  JSON.stringify()
        // })
        // }
      
        useEffect(() => {
          if (old) {
            setNewTitle(old.title);
          }
        }, [old]);
      
        return (
          <div className="update-article">
            {/* <p>{old.title}</p> */}
            <form action="">
              <label htmlFor="">Blog Title</label>
              <input
                type="text"
                value={newTitle}
                onChange={(e) => setNewTitle(e.target.value)}
              />
            </form>
          </div>
        );
      };
      
      export default Update;