Getting started - refresh quote

Our application works fine, but we force the user to refresh the page to see a new quote. Let's add a button to refresh the quote.

Insert a button

Let's start by replacing the src/Main.tsx file content with the following:

import { SeqflowFunctionContext } from "seqflow-js";

interface Quote {
	author: string;
	content: string;
}

async function getRandomQuote(): Promise<Quote> {
	const res = await fetch("https://api.quotable.io/random")
	return await res.json();
}

// This is the new component: it receives a quote and renders it
async function Quote(this: SeqflowFunctionContext, { quote }: { quote: Quote }) {
	this.renderSync(
		<>
			<div>{quote.content}</div>
			<div>{quote.author}</div>
		</>
	);
}

async function Loading(this: SeqflowFunctionContext) {
	this.renderSync(
		<p>Loading...</p>
	);
}
async function ErrorMessage(this: SeqflowFunctionContext, data: { message: string }) {
	this.renderSync(
		<p>{data.message}</p>
	);
}

export async function Main(this: SeqflowFunctionContext) {
	this.renderSync(
		<Loading />
	);

	let quote: Quote;
	try {
		quote = await getRandomQuote();
	} catch (error) {
		this.renderSync(
			<ErrorMessage message={error.message} />
		);
		return;
	}

	// create an interactive element: the button
	const button = <button type='button'>Refresh</button>
	this.renderSync(
		<>
			{ /* Render it */ }
			{button}
			{ /* NB: we added the key attribute here!! */ }
			<Quote key="quote" quote={quote} />
		</>
	);

	// Create an async iterator to wait for the button click
	const events = this.waitEvents(
		this.domEvent('click', { el: button })
	)
	// Wait for the button click
	for await (const _ of events) {
		// Refresh the quote
		let quote: Quote;
		try {
			quote = await getRandomQuote();
		} catch (error) {
			// This replace the hole content with the error message
			this.renderSync(
				<ErrorMessage message={error.message} />
			);
			return;
		}
		// Replace only the child with key "quote" with the new quote
		this.replaceChild("quote", () => <Quote key="quote" quote={quote} />);
	}
}

In the above code, we added a button. We use it to wait for a click event and refresh the quote. The Main component renders the button and the Quote component tagging it with key attribute: SeqFlow tracks it internally, so it knows which component to replace when the button is clicked.

After, the Main component waits for the button click event and replaces the Quote component with the new quote.

Anyway, we can improve the above code avoiding duplicated code. Let's see how.

Create a Spot component

Let's start by replacing the src/Main.tsx file content with the following:

import { SeqflowFunctionContext } from "seqflow-js";

interface Quote {
	author: string;
	content: string;
}

async function getRandomQuote(): Promise<Quote> {
	const res = await fetch("https://api.quotable.io/random")
	return await res.json();
}

// This is the new component: it receives a quote and renders it
async function Quote(this: SeqflowFunctionContext, { quote }: { quote: Quote }) {
	this.renderSync(
		<>
			<div>{quote.content}</div>
			<div>{quote.author}</div>
		</>
	);
}

async function Loading(this: SeqflowFunctionContext) {
	this.renderSync(
		<p>Loading...</p>
	);
}
async function ErrorMessage(this: SeqflowFunctionContext, data: { message: string }) {
	this.renderSync(
		<p>{data.message}</p>
	);
}
async function Spot(this: SeqflowFunctionContext) {}

export async function Main(this: SeqflowFunctionContext) {
	// This async arrow function fetches a new quote and renders
	// It use the key `quote` to replace the child with the loader or the new quote
	const fetchAndRender = async () => {
		this.replaceChild("quote", () => <Loading key="quote" />);

		let quote: Quote;
		try {
			quote = await getRandomQuote();
		} catch (error) {
			this.replaceChild("quote", () => <ErrorMessage key="quote" message={error.message} />);
			return;
		}
		this.replaceChild("quote", () => <Quote key="quote" quote={quote} />);
	}

	const button = <button type='button'>Refresh</button>
	// Render the structure of the html
	this.renderSync(
		<>
			{button}
			<Spot key="quote" />
		</>
	);

	// Fetch and render the quote
	await fetchAndRender();

	const events = this.waitEvents(
		this.domEvent('click', { el: button })
	)
	for await (const _ of events) {
		// Refresh the quote
		await fetchAndRender();
	}
}

In the above code, we created a new component called Spot. It is an empty component and it is used to tag the place where the Quote component will be rendered. We use the key attribute to track it.

Avoid double fetch

The above code has a subtle bug: if the user clicks the button twice before the first fetch completes, the application will fetch the quote twice. Let's fix it using the below code:

import { SeqflowFunctionContext } from "seqflow-js";

interface Quote {
	author: string;
	content: string;
}

async function getRandomQuote(): Promise<Quote> {
	const res = await fetch("https://api.quotable.io/random")
	return await res.json();
}

async function Quote(this: SeqflowFunctionContext, { quote }: { quote: Quote }) {
	this.renderSync(
		<>
			<div>{quote.content}</div>
			<div>{quote.author}</div>
		</>
	);
}

async function Loading(this: SeqflowFunctionContext) {
	this.renderSync(
		<p>Loading...</p>
	);
}
async function ErrorMessage(this: SeqflowFunctionContext, data: { message: string }) {
	this.renderSync(
		<p>{data.message}</p>
	);
}
async function Spot(this: SeqflowFunctionContext) {}

export async function Main(this: SeqflowFunctionContext) {
	const fetchAndRender = async () => {
		this.replaceChild("quote", () => <Loading key="quote" />);

		let quote: Quote;
		try {
			quote = await getRandomQuote();
		} catch (error) {
			this.replaceChild("quote", () => <ErrorMessage key="quote" message={error.message} />);
			return;
		}
		this.replaceChild("quote", () => <Quote key="quote" quote={quote} />);
	}

	// SeqFlow JSX doesn't know which HTML element is rendered, so we need to cast it
	const button = <button type='button'>Refresh</button> as HTMLButtonElement;
	this.renderSync(
		<>
			{button}
			<Spot key="quote" />
		</>
	);

	await fetchAndRender();

	const events = this.waitEvents(
		this.domEvent('click', { el: button })
	)
	for await (const _ of events) {
		// Disable the button: this prevents the element to be clicked twice
		button.disabled = true;
		await fetchAndRender();
		// Re-enable the button
		button.disabled = false;
	}
}

When a JSX element is created in SeqFlow, the type is a real HTML element. So, Typescript doesn't understand which real element is. This is why we have to cast the button to HTMLButtonElement. With this cast, we can use the disabled attribute to disable the button while the quote is being fetched.

Conclusion

In this tutorial, we have learned how to handle the click events and how to avoid double fetches. We also learned how to use the key attribute to track the components. Now, we have a fully functional application that shows a random quote and allows the user to refresh it by clicking a button!