Building features for your Next.js Makerkit application
Learn the flow and patterns that you can use to build new features
As we've mentioned before, the Starter Kit uses various conventions around how the code is split.
While you're free to use your own conventions, in this post I want to describe how to build new features using the existing conventions of the Next.js and Firebase kit.
Let's take a high-level overview of how we can go about adding new features. In this example, we want to add "events" to our application:
1) Adding the business logic
First, we create a new folder within lib
. For example, such as lib/events
. This folder will contain all the code related to events.
In this folder, we will add types, hooks, utilities, and anything regarding the domain of this feature.
For example, let's assume we want to write a feature that allows users to fetch and list events
. We will create a lib/events/hooks/use-fetch-events.ts
file that will contain the hook for fetching a list of events from Firestore.
export default function useFetchEvents() { const firestore = useFirestore(); const eventsCollection = 'events'; const collectionRef = collection( firestore, eventsCollection ) as CollectionReference<WithId<Task>>; const path = `organizationId`; const operator = '=='; const constraint = where(path, operator, organizationId); const organizationsQuery = query(collectionRef, constraint); return useFirestoreCollectionData(organizationsQuery, { idField: 'id', });}
We will now use the hook above within our React components to fetch the events.
2) Adding the "events" components
We will add a new folder within src/components
called events
. This folder will contain all the components related to events.
For example, EventsContainer.tsx
, EventDetail.tsx
and so on. These components will be imported within our Next.js pages components.
export default function EventsContainer() { const { data, status } = useFetchEvents(); if (status === `loading`) { return <Loading />; } if (status === `error`) { return <Error />; } return ( <div> {data.map((event) => ( <EventDetail key={event.id} event={event} /> ))} </div> );}
3) Finally, we add the events Next.js Pages
Finally, we need to add the pages to our Next.js application. We can do this by adding the events pages to pages/events/**.tsx
.
The page components will then import the components as building blocks from components/events
.
export default function EventsPage() { return ( <RouteShell> <EventsContainer /> </RouteShell> );}export function getServerSideProps(ctx: GetServerSidePropsContext) { return withAppProps(ctx);}
4) Using API Requests
Let's now assume you need to use an API request to fetch your data; since this can be due to various factors:
- You need to fetch data from an external API
- You need to fetch data from a different database
- You need to fetch from Firestore that requires elevated permissions
- You need to fetch from particularly complex Firestore queries
These are all good reasons! The flow explained above still makes a lot of sense, but the convention used by the kit is slightly different.
In fact, we store the server queries within ~lib/server/events
. Of course, you can also add these below your domain ~/lib/events/server
. The choice is yours.
export default function fetchEvents() { return makeExternalDatabaseRequest();}
Now that we have a function to retrieve data from the external service, we can finally create an API handler to call this function.
export default async function eventsHandler( req: NextApiRequest, res: NextApiResponse) { const events = await fetchEvents(); res.status(200).json(events);}
From the client side, we can use the hook useApiRequest
to call the API handler.
import useQuery from 'swr';export default function useFetchEventsFromApi() { const fetcher = useApiRequest(); return useQuery([`events`], () => fetcher(`/api/events`));}
Finally, we can use the hook within our React components.
export default function EventsContainer() { const { data, loading, error } = useFetchEventsFromApi(); if (loading) { return <Loading />; } if (error) { return <Error />; } return ( <div> {data.map((event) => ( <EventDetail key={event.id} event={event} /> ))} </div> );}