javascriptreactjsreact-hooksreact-router-domreact-custom-hooks

React Custom Hook Issue


I'm playing around with React to build a simple little recipe keeper app and I'm coming across an issue I haven't yet figured out.

import { useParams, useHistory, Link } from "react-router-dom";
import useFetch from "./useFetch";

const RecipeDetails = () => {
    const { userId, id } = useParams();
    const history = useHistory();

    const { data: recipe, isPending, error } = useFetch('url to api' + customerId + '/' + id);
        
    return (        
        <div className="recipe-details">
            <div className="recipe-details-actions">
                <Link to={`/my-recipes`}>
                    <span>Back to My Recipes</span>
                </Link>
                &nbsp;&nbsp;
                <span>Edit</span>
                &nbsp;&nbsp;
                <a href='#' onClick={() => {
                    fetch('api delete endpoint'+ customerId + '/' + id, {
                        method: 'DELETE'
                    }).then(res => {
                        if (!res.ok) { // error coming back from server
                            throw Error('server level error');
                        } 
                        else {
                            history.push('/my-recipes');
                        }
                    })
                    .catch(err => {
                        console.log(err.message);
                    })
                }}>
                    Delete
                </a>
            </div>
            <h4>Recipe Details - { id }</h4>
            
            {isPending && <div>Loading...</div>}
            {error && <div>{ error }</div>}
            {recipe && (
                <div>
                    <h2>{recipe.title}</h2>
                    <p>Written by {recipe.author}</p>
                    <p>Serves: {recipe.serves}</p>
                    <p>Prep time: {recipe.prep_time}</p>
                    <p>Cook time: {recipe.cook_time}</p>
                    <p>Dish: {recipe.dish}</p>
                    <br />
                </div>
            )}
        </div>   
    );
    
}
 
export default RecipeDetails;

The above works. May not be perfect, but I'm learning. If I however take the code that's executed when the delete link is pressed (to delete a recipe) and place that in a function, I get an error related to the use of the useHistory hook. See below:

import { useParams, useHistory, Link } from "react-router-dom";
import useFetch from "./useFetch";

const handleDeleteClick = (customerId, recipeId) => {
    console.log('delete -> ' + recipeId);
    const history = useHistory();
    
    fetch('api delete url' + customerId + '/' + id, {
        method: 'DELETE'
    }).then(res => {
        if (!res.ok) { // error coming back from server
            throw Error('server level error');
        } 
        else {
            history.push('/my-recipes');
        }
    })
    .catch(err => {
        console.log(err.message);
    })
}

const RecipeDetails = () => {
    const { customerId, id } = useParams();
    const { data: recipe, isPending, error } = useFetch('api fetch url' + customerId + '/' + id);
        
    return (        
        <div className="recipe-details">
            <div className="recipe-details-actions">
                <Link to={`/my-recipes`}>
                    <span>Back to My Recipes</span>
                </Link>
                <span>Edit</span>
                <a href='#' onClick={() => handleDeleteClick(customerId, id)}>
                    Delete
                </a>
            </div>
            <h4>Recipe Details - { id }</h4>
            
            {isPending && <div>Loading...</div>}
            {error && <div>{ error }</div>}
            {recipe && ingredients && instructions && (
                <div>
                    <h2>{recipe.title}</h2>
                    <p>Written by {recipe.author}</p>
                    <p>Serves: {recipe.serves}</p>
                    <p>Prep time: {recipe.prep_time}</p>
                    <p>Cook time: {recipe.cook_time}</p>
                    <p>Dish: {recipe.dish}</p>
                </div>
            )}
        </div>   
    );
    
}
 
export default RecipeDetails;

I've also tried putting all the delete code in its own custom hook, which naturally results in the same error as it seems using hooks or custom hooks can't be done outside the component. Just not sure how to do what I'm after, which is idealing putting the delete fetch in its own custom hook and redirecting after the delete operation is completed successfully. Seeing the following error: React Hook "useHistory" is called in function "handleDeleteClick" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".


Solution

  • The original code you have works because the useHistory hook is correctly called at the root of the React function and everything is in scope. The second code you have fails to work because you are trying to use a React hook outside a React function or custom React hook which breaks the rules of hooks.

    Typically you declare callback handlers inside the React component using it, e.g. within the body/scope of the React function. Move handleDeleteClick into the RecipeDetails component and call the react hook in the component body.

    Example:

    import { useParams, useHistory, Link } from "react-router-dom";
    import useFetch from "./useFetch";
    
    const RecipeDetails = () => {
      const { customerId, id } = useParams();
      const history = useHistory();
    
      const { data: recipe, isPending, error } = useFetch('api fetch url' + customerId + '/' + id);
    
      const handleDeleteClick = (customerId, recipeId) => {
        console.log('delete -> ' + recipeId);
        
        fetch('api delete url' + customerId + '/' + id, {
          method: 'DELETE'
        })
          .then(res => {
            if (!res.ok) { // error coming back from server
              throw Error('server level error');
            } else {
              history.push('/my-recipes');
            }
          })
          .catch(err => {
            console.log(err.message);
          });
      }; 
            
      return (        
        <div className="recipe-details">
          <div className="recipe-details-actions">
            ...
            <span>Edit</span>
            <a href='#' onClick={() => handleDeleteClick(customerId, id)}>
              Delete
            </a>
          </div>
          <h4>Recipe Details - { id }</h4>
                
          ...
        </div>   
      );
    }
    

    If you are really wanting to declare handleDeleteClick external to a React component then you can rewrite it to consume also the history reference that would need to be passed to it from the calling scope. The useHistory hook is still called from the RecipeDetails React component scope.

    Example:

    import { useParams, useHistory, Link } from "react-router-dom";
    import useFetch from "./useFetch";
    
    const handleDeleteClick = (customerId, recipeId, history) => {
      console.log('delete -> ' + recipeId);
        
      fetch('api delete url' + customerId + '/' + id, {
        method: 'DELETE'
      })
        .then(res => {
          if (!res.ok) { // error coming back from server
            throw Error('server level error');
          } else {
            history.push('/my-recipes');
          }
        })
        .catch(err => {
          console.log(err.message);
        });
    }; 
    
    const RecipeDetails = () => {
      const { customerId, id } = useParams();
      const history = useHistory();
    
      const { data: recipe, isPending, error } = useFetch('api fetch url' + customerId + '/' + id);
    
      return (        
        <div className="recipe-details">
          <div className="recipe-details-actions">
            ...
            <span>Edit</span>
            <a
              href='#'
              onClick={() => handleDeleteClick(customerId, id, history)}
            >
              Delete
            </a>
          </div>
          <h4>Recipe Details - { id }</h4>
                
          ...
        </div>   
      );
    }
    

    I don't see much value in converting the specific callback instance into a React hook. Do you have some more general "fetch on click" use case that could benefit from some code reuse? If not then I wouldn't chase down that rat hole. If you did though a refactoring to enclose the handleDeleteClick function in a React hook could look similar to the following:

    const useHandleDeleteClick = () => {
      const history = useHistory();
    
      const handleDeleteClick = React.useCallback((customerId, recipeId) => {
        console.log('delete -> ' + recipeId);
        
        fetch('api delete url' + customerId + '/' + id, {
          method: 'DELETE'
        })
          .then(res => {
            if (!res.ok) { // error coming back from server
              throw Error('server level error');
            } else {
              history.push('/my-recipes');
            }
          })
          .catch(err => {
            console.log(err.message);
          });
      }, [history]);
    
      return handleDeleteClick;
    }
    
    const RecipeDetails = () => {
      const { customerId, id } = useParams();
    
      const { data: recipe, isPending, error } = useFetch('api fetch url' + customerId + '/' + id);
    
      const handleDeleteClick = useHandleDeleteClick();
    
      return (        
        <div className="recipe-details">
          <div className="recipe-details-actions">
            ...
            <span>Edit</span>
            <a
              href='#'
              onClick={() => handleDeleteClick(customerId, id)}
            >
              Delete
            </a>
          </div>
          <h4>Recipe Details - { id }</h4>
                
          ...
        </div>   
      );
    }