Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle/chain synchronous side effects that depend on another with React hooks

I'm trying to rewrite my application from redux to the new context + hooks, but unfortunately I'm having a hard finding a good way to handle series of synchronous side-effects that are depending on the response of the previous.

In my current redux application I make heavy use of synchronous/chained actions and API requests which I typically handle through redux-saga or thunks. So when the response of the first API request is returned, that data is used for the next API request etc.

I've made a custom hook "useFetch" (in this example it does not do much, since it's a simplified version, also I had to make a small adjustment for it to work on codesandbox - see the code below). The problem with that is that due to the "rules of hooks", I cannot use a custom hook inside the useEffect hook. So how to await the response of the first request before doing the next etc, if you have your own hook for fetching data? And even if I ended up giving up on useFetch abstraction and create a vanilla fetch request, how to avoid ending up with a bloated mess of many useEffects hooks? Can this be done a little more elegantly, or is context + hooks still to premature to compete with redux saga/thunk for handling side effects?

The example code below is kept very simple. What it should try to simulate is that:

  1. query person api endpoint to get the person
  2. once we have the person response, query the job endpoint (using the person id in real world scenario)
  3. once we have the person and job, based on the response from the person and job endpoints, query the collegues endpoint to find the persons collegues at a specific job.

Here's the code. Added a delay to useFetch hook to simulate latency in real world:

import React, { useEffect, useState } from "react";
import { render } from "react-dom";

import "./styles.css";

const useFetch = (url, delay = 0) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      // const result = await fetch(url, {
      //  method: "GET",
      //  headers: { "Content-Type": "application/json" }
      // });
      //const response = await result.json();
      const response = await import(url);
      setTimeout(function() {
        setData(response);
      }, delay);
    };

    fetchData();
  }, [url]);

  return data;
};

function App() {
  const [person, setPerson] = useState();
  const [job, setJob] = useState();
  const [collegues, setCollegues] = useState();

  // first we fetch the person /api/person based on the jwt most likely
  const personData = useFetch("./person.json", 5000);
  // now that we have the person data, we use the id to query for the
  // persons job /api/person/1/jobs
  const jobData = useFetch("./job.json", 3000);
  // now we can query for a persons collegues at job x /api/person/1/job/1/collegues
  const colleguesData = useFetch("./collegues.json", 1000);

  console.log(personData);
  console.log(jobData);
  console.log(colleguesData);

  // useEffect(() => {
  //   setPerson(useFetch("./person.json", 5000));
  // }, []);

  // useEffect(() => {
  //   setJob(useFetch("./job.json", 3000));
  // }, [person]);

  // useEffect(() => {
  //   setCollegues(useFetch("./collegues.json",1000));
  // }, [job]);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);

Running example: https://codesandbox.io/s/2v44lron3n?fontsize=14 (you might need to make a change - space or remove a semicolon - to make it work)

Hopefully something like this (or a better solution) is possible, or I will simply not be able to migrate from the awesome redux-saga/thunks to context + hooks.

Best Answer: https://www.youtube.com/watch?v=y55rLsSNUiM

like image 909
Dac0d3r Avatar asked Apr 06 '19 15:04

Dac0d3r


People also ask

Is it possible to use both classes and Hooks in my React code?

You can't use Hooks inside a class component, but you can definitely mix classes and function components with Hooks in a single tree. Whether a component is a class or a function that uses Hooks is an implementation detail of that component.

Which React Hooks causes side effects?

If you're familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount , componentDidUpdate , and componentWillUnmount combined. There are two common kinds of side effects in React components: those that don't require cleanup, and those that do.

When should you not use a hook on a React?

Don't call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders.


2 Answers

Hooks wont replace the way you handle async actions, they are just an abstraction to some things you were used to do, like calling componentDidMount, or handling state, etc.

In the example you are giving, you don't really need a custom hook:

function App() {
  const [data, setData] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const job = await import("./job.json");
      const collegues = await import("./collegues.json");
      const person = await import("./person.json");
      setData({
        job,
        collegues,
        person
      })
    };
    fetchData()
  }, []);

  return <div className="App">{JSON.stringify(data)}</div>;
}

That being said, maybe if you provide an example of an actual redux-saga or thunks code you have, that you want to refactor, we can see what the steps are to accomplish that.

Edit:

That being said if you still want to do something like this, you can take a look at this:

https://github.com/dai-shi/react-hooks-async

import React from 'react';

import { useFetch } from 'react-hooks-async/dist/use-async-task-fetch';

const UserInfo = ({ id }) => {
  const url = `https://reqres.in/api/users/${id}?delay=1`;
  const { pending, error, result, abort } = useFetch(url);
  if (pending) return <div>Loading...<button onClick={abort}>Abort</button></div>;
  if (error) return <div>Error:{error.name}{' '}{error.message}</div>;
  if (!result) return <div>No result</div>;
  return <div>First Name:{result.data.first_name}</div>;
};

const App = () => (
  <div>
    <UserInfo id={'1'} />
    <UserInfo id={'2'} />
  </div>
);

EDIT

This is an interesting approach https://swr.now.sh/#dependent-fetching

like image 150
Luciano Semerini Avatar answered Oct 30 '22 07:10

Luciano Semerini


This is a common scenario in real life,where you want to wait for the first fetch is finished and then do the next fetch.

Please check out the new codesandbox: https://codesandbox.io/s/p92ylrymkj

I used generator when you do fetch request. The data is retrieved in the correct order. After you click fetch data button, go to console and have a look.

Hope this is what you are looking for.

like image 31
MING WU Avatar answered Oct 30 '22 05:10

MING WU