abelcastro.dev

Testing Async React Server Components in a Next.js Project

2024-08-13

typescripttestingnext.js

Building Fasting Timer 18:6: A Learning Journey with React, Next.js, and More

2024-07-26

typescripttestingtailwind

My blog goes Next.js

2024-07-12

typescripttailwindnext.js
1
...
4
5
6
...
11

Abel Castro 2026 - checkout the source code of this page on GitHub - Privacy Policy

Testing async components in a Next.js project can be tricky, particularly when dealing with React Server Components. The challenge arises from the need to handle asynchronous data fetching and Suspense boundaries properly.

The Challenge

React Server Components allow you to fetch data on the server and send it to the client, enhancing performance by reducing the amount of JavaScript required on the client-side. However, this asynchrony introduces complexities in testing.

Having asynchronous components like this introduces some challenges when writing unit tests

export type PageSearchParams = {
	query?: string;
	page?: string;
  };
  
  export default async function Page({
	searchParams,
  }: {
	searchParams?: PageSearchParams;
  }) {
	const query = searchParams?.query || "";
	const currentPage = Number(searchParams?.page) || 1;
	const asyncData = await fetchSomeDataAsynchronously(query, currentPage);
  
	return (
		<>Do something with {...asyncData}</>
	);
  }

A common issue is that when testing async components, you might encounter empty snapshots. This is illustrated by the following example, which renders an async component but ends up with an empty snapshot:

test("This test will pass but it will generate an empty snapshot", async () => {
  const { container } = render(<Page />);
  expect(container).toMatchSnapshot();
});

The generated snapshot might look like this:

exports[`This test will pass but it will generate an empty snapshot 1`] = `<div />`;

If we try to perform some screen assertions, the test will fail. For example:

test("This test will fail because the string cannot be found", async () => {
  const { container } = render(<Page />);
  await screen.findByText("Some text in your page"); // can't be found
});

Please note that, depending on the version of Next.js you are using, you might encounter errors like this:

Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.

Check the links at the end of the post to see which package versions should help avoid these errors.

The Solution: Using <Suspense> and screen

To solve this issue I discovered a workaround in this GitHub issue.

The workaround is to wrap your Component with <Suspense> to the render call. With that change the screen assert will pass.

test("This will pass but will now pass", async () => {
	const { container } = render(
		<Suspense>
			<Page />
		</Suspense>,
	);
	await screen.findByText("Some text in your page");
});

From the other side, if you are only interested in asserting the snapshot like this, the snapshot will still be empty.

test("This will pass but will still generate an empty snapshot", async () => {
	const { container } = render(
		<Suspense>
			<Page />
		</Suspense>,
	);
	expect(container).toMatchSnapshot();
});

I discovered that for some reason that I cannot understand, if you call first a screen assert the snapshot will finally generate a correct snapshot.

test("This will pass and will now generate a correct snapshot", async () => {
	const { container } = render(
		<Suspense>
			<Page />
		</Suspense>,
	);
	await screen.findByText("Some text in your page");
	expect(container).toMatchSnapshot();
});

Conclusion

As far as I understand, the issues are caused by these features being quite new and still in development, and it is possible that by the time you read this, the issue may already be resolved.

Please note that my proposed solutions are more of a workaround and may rely on unstable package versions.

In any case, I hope this helps others successfully write unit tests for asynchronous server components ✅ 🚀.

You can check the source code of this blog to see these workarounds in action within a real-world app, or review this demo project that showcases the problem with a simple app.

Happy coding! Happy testing!

Fasting Timer 18:6

In my journey of learning more about React and Next.js, I created a new project: Fasting Timer 18:6. In this blog post, I'll take you through the details of the project, the key learnings, and some interesting technical aspects that might be helpful for fellow developers.

You can check out the project repository here and try the live app here.

Project Overview

Fasting Timer 18:6 is a simple yet effective application designed to help users manage their intermittent fasting schedules. The 18:6 fasting method involves fasting for 18 hours and eating during a 6-hour window. This app helps users keep track of their fasting and eating times with ease.

Key Learnings

Working on this project provided me with several valuable learning opportunities:

  1. Testing React Apps with Jest
    • Writing tests for React applications was one of the primary goals of this project. Using Jest, I was able to create a comprehensive test suite to ensure the app functions correctly. Testing not only helps in catching bugs early but also makes the codebase more maintainable.
    • Here's a snippet of a simple test case:
	import { render, screen, fireEvent, act } from "@testing-library/react";
	import Home from "../app/page";
	import storageService from "../app/storage/storage-service";
	
	it("should start the timer and switch to fasting/eating mode after clicking the button", async () => {
    	const { container } = render(<Home />);
    	
    	expect(container).toMatchSnapshot();
    	
    	// Start fasting
    	const button = screen.getByRole("button", { name: /Start fasting/i });
    	await act(async () => {
    	  fireEvent.click(button);
    	});
    	
    	expect(container).toMatchSnapshot();
    	
    	const startFastingTime = storageService.getStartFastingTime();
    	expect(startFastingTime).toBeInstanceOf(Promise<Date>);
    	// ...
	});
  1. Creating a Coverage Badge with Codecov.io
    • Integrating Codecov.io allowed me to monitor test coverage. This step was crucial in ensuring that a significant portion of the code is tested, providing a clear picture of areas that might need more attention.
    • The coverage badge is prominently displayed in the repository, serving as a quick reference for the project's test coverage status.

The StorageService Concept

One of the interesting aspects of the project is the StorageService. This service is designed to manage the storage and handling of fasting and non-fasting times.

Key Points:

  • BrowserStorage as Default Provider: Currently, the default storage provider is BrowserStorage, which stores data in the user's local browser storage.
  • IStorageProvider Interface: To make the storage service flexible, an IStorageProvider interface is defined. This interface outlines the necessary methods that any storage provider must implement.
  • Ease of Changing Persistence Layer: With this setup, changing the persistence layer (e.g., from local storage to a database) becomes straightforward. One only needs to implement the IStorageProvider interface and update the storage service configuration.

Example Implementation:

const storageService = new StorageService(new BrowserStorageProvider());

export default storageService;

With this design, changing the persistence layer is as simple as touching two files—implementing the new storage provider and updating the configuration.

Conclusion

Working on the Fasting Timer 18:6 project has been an enriching experience. From learning to write tests with Jest to creating a flexible storage solution, each step has contributed to my growth as a developer. I hope this blog post provides valuable insights and encourages you to explore and implement these concepts in your projects.

Feel free to check out the repository, try the app, and provide any feedback or contributions. Happy coding!

Try the live app here.

The primary goal of my blog has always been to experiment and explore new technologies rather than getting more views. This journey has seen my blog evolve through various technologies and frameworks, each iteration bringing its own set of learnings and advancements. Here’s a look back at the history and the latest re-implementation of my blog.

The Initial Version: Django, Django Templates, and Bootstrap

The initial version of my blog was launched in 2021. At that time, I chose Django as the primary framework for its robustness and extensive feature set. Django templates allowed for server-side rendering, while Bootstrap provided a responsive and modern look to the blog. This combination was powerful and relatively easy to work with, making it a perfect choice for someone looking to build a solid foundation.

Evolving with htmx and REST API

As time went on, I began exploring more dynamic ways to enhance the user experience. This led to the incorporation of htmx, which allowed for more interactive web pages without the need for full page reloads. Additionally, I implemented a REST API, which opened up possibilities for future integrations and provided a more modular approach to data handling. This version of the blog, which has seen significant evolution, is still live and can be found at https://abelcastro.dev/blog.

Experimenting with Angular

In parallel, I also experimented with Angular, a powerful framework for building dynamic web applications. This version, although not always functional, was hosted separately at https://ng.abelcastro.dev. This experiment allowed me to understand the intricacies of a component-based architecture and single-page applications, providing valuable insights into modern web development practices.

The Latest Iteration: Next.js, Tailwind, and Vercel

The latest iteration of my blog is available at https://blog.abelcastro.dev. This will be the default version of my blog on which I will focus in the future. It represents a significant leap forward in terms of technology and developer experience. Developed with Next.js and styled with Tailwind CSS, this version is sleek, fast, and highly responsive. Next.js offers a perfect balance between server-side rendering and static site generation, ensuring optimal performance and SEO benefits. Tailwind CSS, with its utility-first approach, makes it easy to create a custom and consistent design system.

Hosting this new version on Vercel’s platform as a hobby project has been a delightful experience. After being used to handling server configurations and Docker files, it was amazing to simply link my GitHub repository with Vercel and have the code deployed seamlessly.

One of the exciting aspects of this re-implementation was the opportunity to apply techniques I learned from the Next.js dashboard tutorial. Features like pagination and search have been integrated, enhancing the functionality and usability of the blog. These features not only improve the user experience but also showcase the powerful capabilities of Next.js and the ease with which complex features can be implemented.

Conclusion

The evolution of the code and posts on my blog reflects part of my journey as a developer. I am very happy with the results and excited to learn more about TypeScript and frameworks like Next.js.

By the way, all these projects are available in my github profile.