Handling Complex Asynchronous Flows in React with JavaScript Generators (using Typescript)

Asynchronous programming is a fundamental part of building user interfaces. React is no exception. It provides a set of hooks to handle side effects, such as data loading or user interactions. However, handling asynchronous states can be tricky and lead to common problems such as race conditions and callback hell. JavaScript generators solve these challenges by allowing developers to express sequential and parallel flows, handle cancellation and error handling, and improve code readability. In this post, we’ll explore how generators work and can be used in a React application with TypeScript to handle complex asynchronous flows.

Understanding Generators

A generator is a special function that can be paused and resumed, allowing it to maintain its state between invocations. To define a generator function, we use the function* syntax instead of the regular function keyword. Once defined, we can use the yield keyword to pause the execution of the generator function and return a value. We can then use the next() method to resume the execution of the generator and pass a value to the generator.

Here is an example of a generator function:

function* generatorExample() {
yield 1;
yield 2;
yield 3;
}

const myGen = generatorExample();
console.log(myGen.next().value); // 1
console.log(myGen.next().value); // 2
console.log(myGen.next().value); // 3

In the example above, we define a generator function generatorExample that yields the values 1, 2, and 3. We then create an instance of the generator function by calling it, and we use the next() method to get the next value from the generator. The first call next() returns the value 1, the second call returns the value 2, and the third call returns the value 3.

Another important feature of generators is that they can receive values using the yield keyword. The value passed to next(value) is returned by the current yield statement.

function* generatorExample() {
const received = yield 1;
console.log(received);
}

const myGen = generatorExample();
console.log(myGen.next().value); // 1
console.log(myGen.next("Hello Generator").value); // undefined

In the example above, the generator function generatorExample receives the value “Hello Generator” on the first call of next() and prints it in the console.

It’s important to mention that generator functions are not invoked like regular functions, instead, they return an iterator, an object that has a next method. Each time we call the next method on the iterator, the generator function is executed until it finds the next yield statement or the end of the function.

function* generatorExample() {
console.log("Start of the generator function");
yield 1;
console.log("Between yields");
yield 2;
console.log("End of the generator function");
}

const myGen = generatorExample();
console.log(myGen.next().value); // Start of the generator function, 1
console.log(myGen.next().value); // Between yields, 2
console.log(myGen.next().value); // End of the generator function, undefined

In the example above, the generator function is invoked only when we call the next method on the iterator. The first call to next will execute the function until the first yield statement, the second call will execute the function until the second yield statement, and so on.

         +-------------+
| generator() |
+-------------+
|
|
+-------------+-------------+
| Iterator { | |
| next: fn | |
+-------------+ |
|
+-----------v-----------+
| generator function |
| (code & state) |
+-----------------------+

As you can see from the diagram, the generator function and the iterator are two separate entities. The generator function contains the code and the state, while the iterator controls the execution of the generator function.

It’s important to note that the generator function only runs until the next yield statement; it doesn’t run until completion. This allows the generator to maintain its state between invocations so that the next time next() is called, it will pick up where it left off.

Additionally, generators can return values, which also makes the iterator complete, meaning that the generator function can’t be resumed anymore.

function* generatorExample() {
yield 1;
yield 2;
return 3;
}

const myGen = generatorExample();
console.log(myGen.next().value); // 1
console.log(myGen.next().value); // 2
console.log(myGen.next().value); // 3
console.log(myGen.next().value); // undefined

Using generators in a React application

Handling asynchronous flows in React can be a complex task. There are many ways to do it, but most involve a lot of boilerplate code, callbacks, and state management. This can make the code hard to read, test, and debug.

To simplify this process, let’s create a custom hook useAsyncGenerator that will allow us to handle asynchronous flows easily and effectively. With this hook, we can use generator functions to handle asynchronous requests, and it returns an object with a data property that contains the resolved value of the promise returned by the generator, a loading property that indicates whether the generator is still running or not, an optional error property that will be set if there’s an error thrown by the generatorFn and a refetch function to call the generatorFn again and update the data.

type ReturnType<T> = {
data?: T;
loading: boolean;
error?: any;
refetch: () => void;
}

function useAsyncGenerator<T>(generatorFn: () => IterableIterator<Promise<T>>): ReturnType<T> {
const [state, setState] = useState<ReturnType<T>>({ loading: true, refetch: () => {} });

useEffect(() => {
async function executeRequest(gen: IterableIterator<Promise<T>>) {
try {
const { value, done } = await gen.next();
if (!done) {
setState((prevState) => ({ ...prevState, loading: false, data: value as any }));
executeRequest(gen);
} else {
setState((prevState) => ({ ...prevState, loading: false, data: value }));
}
} catch (error) {
setState((prevState) => ({ ...prevState, loading: false, error }));
}
}
function refetch() {
setState((prevState) => ({ ...prevState, loading: true }));
executeRequest(generatorFn());
}
executeRequest( generatorFn());
setState((prevState) => ({ ...prevState, refetch }));
}, []);
return state;
}

Benefits of using the useAsyncGenerator hook:

  • It simplifies handling asynchronous flows in a React component.
  • You can use the generator functions you’re already familiar with, which can be tested and debugged in isolation.
  • The returned object is easy to use. It contains all the necessary information you need to handle the asynchronous flow.
  • The hook can be easily reused across the application.
  • Extracting the logic to a reusable hook makes the component leaner and easier to read, test, and debug.

Now, we will create an arbitrary async function to demonstrate how we can handle async requests within our app. Our request function will have multiple fetch requests to make things more interesting, so we can explore yielding data in intermediate states.

let counter = 0; // Counter for tracking calls (just for demonstration purposes )
async function* handleRequests(): any {
counter++;
console.log("Making Request: ", counter);

const response1: any = await fetch('https://api.publicapis.org/entries');
const data1 = await response1.json();

yield { data1, data2: null }

const response2: any = await fetch('https://catfact.ninja/fact');
const data2 = await response2.json();

return { data1, data2 }
}

Here’s an example of how you can use the useAsyncGenerator hook in a React component:

function App() {
const state = useAsyncGenerator<{ data1: any, data2: any }>(handleRequests);
return (
<div>
<button onClick={state.refetch}>Refetch</button>
<div>
Requests Sent: {counter}
</div>
<pre>
<code>
{state.loading ? 'Loading...' : JSON.stringify(state.data, null, 2)}
</code>
</pre>

{state.error && <div>{state.error.message}</div>}
</div>
);
}

export default App;

Our Component async state is completely separated from the view logic

Final Words

In conclusion, JavaScript generators provide an elegant solution to handle complex asynchronous flows in React. They allow you to use generator functions to handle asynchronous requests, which makes it easy to read, test, and debug your code. By creating a custom hook useAsyncGenerator, we can simplify handling asynchronous flows in a React component and make it more reusable across the application. The hook returns an object with a data property that contains the resolved value of the promise returned by the generator, a loading property that indicates whether the generator is still running or not, an optional error property that will be set if there’s an error thrown by the generator function and a refetch function that can be used to re-execute the generator function and update the data. Using this hook, you can focus on the business logic of your component instead of worrying about handling asynchronous flows.

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job


Handling Complex Asynchronous Flows in React with JavaScript Generators (using Typescript) was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

Asynchronous programming is a fundamental part of building user interfaces. React is no exception. It provides a set of hooks to handle side effects, such as data loading or user interactions. However, handling asynchronous states can be tricky and lead to common problems such as race conditions and callback hell. JavaScript generators solve these challenges by allowing developers to express sequential and parallel flows, handle cancellation and error handling, and improve code readability. In this post, we’ll explore how generators work and can be used in a React application with TypeScript to handle complex asynchronous flows.

Understanding Generators

A generator is a special function that can be paused and resumed, allowing it to maintain its state between invocations. To define a generator function, we use the function* syntax instead of the regular function keyword. Once defined, we can use the yield keyword to pause the execution of the generator function and return a value. We can then use the next() method to resume the execution of the generator and pass a value to the generator.

Here is an example of a generator function:

function* generatorExample() {
yield 1;
yield 2;
yield 3;
}

const myGen = generatorExample();
console.log(myGen.next().value); // 1
console.log(myGen.next().value); // 2
console.log(myGen.next().value); // 3

In the example above, we define a generator function generatorExample that yields the values 1, 2, and 3. We then create an instance of the generator function by calling it, and we use the next() method to get the next value from the generator. The first call next() returns the value 1, the second call returns the value 2, and the third call returns the value 3.

Another important feature of generators is that they can receive values using the yield keyword. The value passed to next(value) is returned by the current yield statement.

function* generatorExample() {
const received = yield 1;
console.log(received);
}

const myGen = generatorExample();
console.log(myGen.next().value); // 1
console.log(myGen.next("Hello Generator").value); // undefined

In the example above, the generator function generatorExample receives the value "Hello Generator" on the first call of next() and prints it in the console.

It’s important to mention that generator functions are not invoked like regular functions, instead, they return an iterator, an object that has a next method. Each time we call the next method on the iterator, the generator function is executed until it finds the next yield statement or the end of the function.

function* generatorExample() {
console.log("Start of the generator function");
yield 1;
console.log("Between yields");
yield 2;
console.log("End of the generator function");
}

const myGen = generatorExample();
console.log(myGen.next().value); // Start of the generator function, 1
console.log(myGen.next().value); // Between yields, 2
console.log(myGen.next().value); // End of the generator function, undefined

In the example above, the generator function is invoked only when we call the next method on the iterator. The first call to next will execute the function until the first yield statement, the second call will execute the function until the second yield statement, and so on.

         +-------------+
| generator() |
+-------------+
|
|
+-------------+-------------+
| Iterator { | |
| next: fn | |
+-------------+ |
|
+-----------v-----------+
| generator function |
| (code & state) |
+-----------------------+

As you can see from the diagram, the generator function and the iterator are two separate entities. The generator function contains the code and the state, while the iterator controls the execution of the generator function.

It’s important to note that the generator function only runs until the next yield statement; it doesn’t run until completion. This allows the generator to maintain its state between invocations so that the next time next() is called, it will pick up where it left off.

Additionally, generators can return values, which also makes the iterator complete, meaning that the generator function can't be resumed anymore.

function* generatorExample() {
yield 1;
yield 2;
return 3;
}

const myGen = generatorExample();
console.log(myGen.next().value); // 1
console.log(myGen.next().value); // 2
console.log(myGen.next().value); // 3
console.log(myGen.next().value); // undefined

Using generators in a React application

Handling asynchronous flows in React can be a complex task. There are many ways to do it, but most involve a lot of boilerplate code, callbacks, and state management. This can make the code hard to read, test, and debug.

To simplify this process, let’s create a custom hook useAsyncGenerator that will allow us to handle asynchronous flows easily and effectively. With this hook, we can use generator functions to handle asynchronous requests, and it returns an object with a data property that contains the resolved value of the promise returned by the generator, a loading property that indicates whether the generator is still running or not, an optional error property that will be set if there's an error thrown by the generatorFn and a refetch function to call the generatorFn again and update the data.

type ReturnType<T> = {
data?: T;
loading: boolean;
error?: any;
refetch: () => void;
}

function useAsyncGenerator<T>(generatorFn: () => IterableIterator<Promise<T>>): ReturnType<T> {
const [state, setState] = useState<ReturnType<T>>({ loading: true, refetch: () => {} });

useEffect(() => {
async function executeRequest(gen: IterableIterator<Promise<T>>) {
try {
const { value, done } = await gen.next();
if (!done) {
setState((prevState) => ({ ...prevState, loading: false, data: value as any }));
executeRequest(gen);
} else {
setState((prevState) => ({ ...prevState, loading: false, data: value }));
}
} catch (error) {
setState((prevState) => ({ ...prevState, loading: false, error }));
}
}
function refetch() {
setState((prevState) => ({ ...prevState, loading: true }));
executeRequest(generatorFn());
}
executeRequest( generatorFn());
setState((prevState) => ({ ...prevState, refetch }));
}, []);
return state;
}

Benefits of using the useAsyncGenerator hook:

  • It simplifies handling asynchronous flows in a React component.
  • You can use the generator functions you’re already familiar with, which can be tested and debugged in isolation.
  • The returned object is easy to use. It contains all the necessary information you need to handle the asynchronous flow.
  • The hook can be easily reused across the application.
  • Extracting the logic to a reusable hook makes the component leaner and easier to read, test, and debug.

Now, we will create an arbitrary async function to demonstrate how we can handle async requests within our app. Our request function will have multiple fetch requests to make things more interesting, so we can explore yielding data in intermediate states.

let counter = 0; // Counter for tracking calls (just for demonstration purposes )
async function* handleRequests(): any {
counter++;
console.log("Making Request: ", counter);

const response1: any = await fetch('https://api.publicapis.org/entries');
const data1 = await response1.json();

yield { data1, data2: null }

const response2: any = await fetch('https://catfact.ninja/fact');
const data2 = await response2.json();

return { data1, data2 }
}

Here’s an example of how you can use the useAsyncGenerator hook in a React component:

function App() {
const state = useAsyncGenerator<{ data1: any, data2: any }>(handleRequests);
return (
<div>
<button onClick={state.refetch}>Refetch</button>
<div>
Requests Sent: {counter}
</div>
<pre>
<code>
{state.loading ? 'Loading...' : JSON.stringify(state.data, null, 2)}
</code>
</pre>

{state.error && <div>{state.error.message}</div>}
</div>
);
}

export default App;

Our Component async state is completely separated from the view logic

Final Words

In conclusion, JavaScript generators provide an elegant solution to handle complex asynchronous flows in React. They allow you to use generator functions to handle asynchronous requests, which makes it easy to read, test, and debug your code. By creating a custom hook useAsyncGenerator, we can simplify handling asynchronous flows in a React component and make it more reusable across the application. The hook returns an object with a data property that contains the resolved value of the promise returned by the generator, a loading property that indicates whether the generator is still running or not, an optional error property that will be set if there's an error thrown by the generator function and a refetch function that can be used to re-execute the generator function and update the data. Using this hook, you can focus on the business logic of your component instead of worrying about handling asynchronous flows.

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job


Handling Complex Asynchronous Flows in React with JavaScript Generators (using Typescript) was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


Print Share Comment Cite Upload Translate
APA
Radovan Stevanovic | Sciencx (2024-03-28T11:15:19+00:00) » Handling Complex Asynchronous Flows in React with JavaScript Generators (using Typescript). Retrieved from https://www.scien.cx/2023/01/16/handling-complex-asynchronous-flows-in-react-with-javascript-generators-using-typescript/.
MLA
" » Handling Complex Asynchronous Flows in React with JavaScript Generators (using Typescript)." Radovan Stevanovic | Sciencx - Monday January 16, 2023, https://www.scien.cx/2023/01/16/handling-complex-asynchronous-flows-in-react-with-javascript-generators-using-typescript/
HARVARD
Radovan Stevanovic | Sciencx Monday January 16, 2023 » Handling Complex Asynchronous Flows in React with JavaScript Generators (using Typescript)., viewed 2024-03-28T11:15:19+00:00,<https://www.scien.cx/2023/01/16/handling-complex-asynchronous-flows-in-react-with-javascript-generators-using-typescript/>
VANCOUVER
Radovan Stevanovic | Sciencx - » Handling Complex Asynchronous Flows in React with JavaScript Generators (using Typescript). [Internet]. [Accessed 2024-03-28T11:15:19+00:00]. Available from: https://www.scien.cx/2023/01/16/handling-complex-asynchronous-flows-in-react-with-javascript-generators-using-typescript/
CHICAGO
" » Handling Complex Asynchronous Flows in React with JavaScript Generators (using Typescript)." Radovan Stevanovic | Sciencx - Accessed 2024-03-28T11:15:19+00:00. https://www.scien.cx/2023/01/16/handling-complex-asynchronous-flows-in-react-with-javascript-generators-using-typescript/
IEEE
" » Handling Complex Asynchronous Flows in React with JavaScript Generators (using Typescript)." Radovan Stevanovic | Sciencx [Online]. Available: https://www.scien.cx/2023/01/16/handling-complex-asynchronous-flows-in-react-with-javascript-generators-using-typescript/. [Accessed: 2024-03-28T11:15:19+00:00]
rf:citation
» Handling Complex Asynchronous Flows in React with JavaScript Generators (using Typescript) | Radovan Stevanovic | Sciencx | https://www.scien.cx/2023/01/16/handling-complex-asynchronous-flows-in-react-with-javascript-generators-using-typescript/ | 2024-03-28T11:15:19+00:00
https://github.com/addpipe/simple-recorderjs-demo