Search code examples
javascriptreactjstypescriptreduxreact-redux

Call fetch function in functional component React


I've tried so many ways to fetch data only once before rendering, but I'm still having some issues:

  1. I Can't call dispatch in componentDidMount because there is a rule that I can do it in the Functional component only.

  2. If I try to call the fetch function at the beginning of a Functional component, it starts to rerender infinitely, because the fetch function calls every time and changes the state in the Redux store.

  3. I found a solution with useEffect but it generates an exception "Invalid hook call" like in the first point.

How can I call the fetch function only once in this component?

Here is my component:

import React, { useEffect } from "react";
import { useParams as params} from "react-router-dom";
import { VolunteerCardList } from "./VolunteerCardList";
import { AnimalNeeds } from "./AnimalNeeds";
import { AppState } from "../reducers/rootReducer";
import { connect } from "react-redux";
import { Page404 } from "./404";
import { fetchAnimal } from "../actions/animalAction";
import { Dispatch } from "redux";
import { IAnimalCard } from "../interfaces/Interfaces";

const AnimalCard: React.FC<Props> = ({animal, loading, fetch}) => {
    useEffect(() => {   
            fetch(); //invalid hook call????
    }, [])
    
    
    return (
        <div className="container">
            some HTML
        </div>
    )
}

interface RouteParams {
    shelterid: string,
    animalid: string,
}

interface mapStateToPropsType {
    animal: IAnimalCard,
    loading: boolean
}

const mapStateToProps = (state: AppState) : mapStateToPropsType=> {
    return{
        animal: state.animals.animal,
        loading: state.app.loading
    }
}

interface mapDispatchToPropsType {
    fetch: () => void;
}

const mapDispatchToProps = (dispatch: Dispatch<any>) : mapDispatchToPropsType => ({
    fetch : () => {
        const route = params<RouteParams>();
        dispatch(fetchAnimal(route.shelterid, route.animalid));
    }
})

type Props  = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>;

export default connect(mapStateToProps, mapDispatchToProps as any)(AnimalCard);

This is my reducer:

export const animalReducer = (state: AnimalReducerType = initState, action: IAction) => {

switch (action.type) {
    case AnimalTypes.FETCH_ANIMAL:
            return {...state, animal: action.payload};
        break;

    default:
        return state;
        break;
}

This is action:

export interface IFetchAnimalAction  {
    type: AnimalTypes.FETCH_ANIMAL,
    payload: IAnimalCard
}

export type IAction = IFetchAnimalAction;

export const fetchAnimal = (shelterId : string, animalId: string) => {
    return async (dispatch: Dispatch)  => {
        const response = await fetch(`https://localhost:44300/api/animals/${animalId}`);
        const json = await response.json();
        dispatch<IFetchAnimalAction>({type: AnimalTypes.FETCH_ANIMAL, payload: json})
    }
} 

Solution

  • This runs as old lifecycle method componentDidMount:

    useEffect(() => {   
      fetch(); //invalid hook call????
    }, [])
    

    I guess the behaviour you want to replicate is the one iterated by componentWillMount, which you cannot do by any of the standard hooks. My go-to solution for this is to let the acquire some loadingState, most explicitly as:

    const AnimalCard: React.FC<Props> = ({animal, loading, fetch}) => {
        const [isLoading, setIsLoading] = useState<boolean>(true);
    
        useEffect(() => {   
          fetch().then(res => {
            // Do whatever with res
            setIsLoading(false);
          } 
        }, [])
    
        if(isLoading){
          return null
        }
    
        return (
            <div className="container">
                some html
            </div>
        )
    }