React custom hooks taking dynamic data properties

Viewed 1981

So I've read these blog posts about using custom hooks to fetch data, so for instance we have a custom hook doing the API call, setting the data, possible errors as well as the spinny isFetching boolean:

export const useFetchTodos = () => {
    const [data, setData] = useState();
    const [isFetching, setIsFetching] = useState(false);
    const [error, setError] = useState();
    useEffect(() => {
      setIsFetching(true);
      axios.get('api/todos')
        .then(response => setData(response.data)
        .catch(error => setError(error.response.data)
        .finally(() => setFetching(false);
      }, []);
     return {data, isFetching, error};
}

And then at the top level of our component we would just call const { data, error, fetching } = useFetchTodos(); and all great we render our component with all the todos fetched.

The thing I don't understand is how would we send dynamic data / parameters to the hook based on the internal state of the component, without breaking the rules of hooks?

For instance, imagine we have a useFetchTodoById(id) hook defined the same way as the above one, how would we pass that id around? Let's say our TodoList component which renders our Todos is the following:

export const TodoList = (props) => {
    const [selectedTodo, setSelectedTodo] = useState();
    
    useEffect(() => {
      useFetchTodoById(selectedTodo.id) --> INVALID HOOK CALL, cannot call custom hooks from useEffect, 
        and also need to call our custom hooks at the "top level" of our component
    }, [selectedTodo]);

    return (<ul>{props.todos.map(todo => (
            <li onClick={() => setSelectedTodo(todo.id)}>{todo.name}</li>)}
        </ul>);
}

I know for this specific usecase we could pass our selectedTodo through props and call our useFetchTodoById(props.selectedTodo.id) at the top of our component, but I'm just illustrating the issue with this pattern I ran into, we won't always have the luxury of receiving the dynamic data that we need in the props.

Also -- how would we apply this pattern for POST/PUT/PATCH requests which take dynamic data properties?

1 Answers

You should have a basic useFetch hook the accepts a url, and fetches whenever the url changes:

const useFetch = (url) => {
  const [data, setData] = useState();
  const [isFetching, setIsFetching] = useState(false);
  const [error, setError] = useState();
  
  useEffect(() => {
    if(!url) return;

    setIsFetching(true);
    axios.get(url)
      .then(response => setData(response.data))
      .catch(error => setError(error.response.data))
      .finally(() => setFetching(false));
  }, [url]);

  return { data, isFetching, error };
};

Now you can create other custom hook from this basic hook:

const useFetchTodos = () => useFetch('api/todos');

And you can also make it respond to dynamic changes:

const useFetchTodoById = id => useFetch(`api/todos/${id}`);

And you can use it in the component, without wrapping it in useEffect:

export const TodoList = (props) => {
  const [selectedTodo, setSelectedTodo] = useState();
  
  const { data, isFetching, error } = useFetchTodoById(selectedTodo.id);

  return (
    <ul>{props.todos.map(todo => (
      <li onClick={() => setSelectedTodo(todo.id)}>{todo.name}</li>)}
    </ul>
  );
};
Related