Frontend State
Handling State in React
Handling state in React is a problem with many solutions. There are several libraries that enable different paradigms. This booking flow will only have very limited state which means that the built-in Context
will be sufficient for our needs. We will combine it with React Query because it comes built in with tRPC, which we will use for API calls, but we'll get to that later.
Now, there are two distinct sets of state that we want to keep track of here: the "navigation", i.e. which page the user is on, and the actual data that the user is entering.
You're going to create a file that contains your Context
and a little helper hook that encapsulates the logic of keeping this state up to date. Create bookingFlowContext.tsx
in the src
folder and add the following code:
xxxxxxxxxx
import { FC, createContext, useMemo, useState } from 'react';
export type BookingFlowState = {};
type BookingFlowContext = {
onGoBack: () => void;
onProceed: () => void;
state: BookingFlowState;
updateState: (fields: Partial<BookingFlowState>) => void;
};
const ctxMissing = () => {
throw new Error('bookingContext not set');
};
const bookingFlowContext = createContext<BookingFlowContext>({
onGoBack: ctxMissing,
onProceed: ctxMissing,
state: {},
updateState: ctxMissing,
});
export function useBookingFlow(flow: FC[]): {
page: JSX.Element;
activePageIndex: number;
setActivePageIndex: (i: number) => void;
} {
const [activePageIndex, setActivePageIndex] = useState(0);
const [state, setState] = useState<BookingFlowState>({});
const value = useMemo(
() => ({
onGoBack: () => setActivePageIndex((i) => i - 1),
onProceed: () => setActivePageIndex((i) => i + 1),
state,
updateState: (fields: Partial<BookingFlowState>) =>
setState({ state, fields }),
}),
[state]
);
const Contents = flow[activePageIndex];
const page = (
<bookingFlowContext.Provider value={value}>
<Contents />
</bookingFlowContext.Provider>
);
return { page, activePageIndex, setActivePageIndex };
}
export default bookingFlowContext;
Let's break this down. The type BookingFlowState
is empty for now, but it will contain the fields that the user will fill in. The outer type, BookingFlowContext
, contains that state as well as some functions that relate to navigation. We will get access to these through the useContext
hook.
The context is created with a default value that throws an error if you try to access it before it has been set. There is no way for the type system to ensure that a component that relies on the context is actually wrapped in a provider. That's a shame but this approach will at least give you an easily understandable runtime error if you happen to do so by accident. An alternative would be to allow undefined
as a value for the context, but that would then require defensive undefined-checks everywhere you use the context. Ultimately, this is down to personal preference, but I am generally in favor of fail-fast over defensive programming.
The useBookingFlow
hook is the one that you will use to apply the context. It takes an array of components making up the pages of your booking flow. The hook returns the current page, the index of the current page, and a function to set the current page.
Use the hook like this in NewBooking.tsx
:
xxxxxxxxxx
import { FC } from 'react';
import { useBookingFlow } from './bookingFlowContext';
const ChooseTypePage: FC = () => <div>Choose type</div>;
const ChooseDatePage: FC = () => <div>Choose date</div>;
const EnterEmailPage: FC = () => <div>Enter email</div>;
const flow = [ChooseTypePage, ChooseDatePage, EnterEmailPage];
const NewBooking: FC = () => {
const { page, activePageIndex, setActivePageIndex } = useBookingFlow(flow);
return (
<div className="min-h-screen w-full bg-cover bg-fixed py-12">
<div className="mx-auto flex max-w-3xl flex-col justify-between rounded border border-gray-200 bg-gray-50 px-12 py-8 shadow-lg">
<div className="">{page}</div>
<div>
<button onClick={() => setActivePageIndex(activePageIndex - 1)}>
Back
</button>
<button onClick={() => setActivePageIndex(activePageIndex + 1)}>
Next
</button>
</div>
</div>
</div>
);
};
export default NewBooking;
Okay, so this gives you the same functionality as before, but now you have a nice way to keep track of the state of the booking flow. You can also easily add more pages to the flow by adding more functions to the array. You'll move the back/next buttons into the pages themselves so that you can disable and enable them depending on the current page. However, you can add a progress indicator at this level. We'll create a bunch of circles with numbers in them to show where in the flow the user is.
For this, you need to apply conditional classNames
. That can be easily done with string concatenation or interpolation, but a helper library quickly becomes nice to have. The one I tend to use is called clsx
. It's very simple and easy to use. Install it with npm i clsx
and update your NewBooking.tsx
file like this:
xxxxxxxxxx
import { FC } from 'react';
import clsx from 'clsx';
import { useBookingFlow } from './bookingFlowContext';
const ChooseTypePage: FC = () => <div>Choose type</div>;
const ChooseDatePage: FC = () => <div>Choose date</div>;
const EnterEmailPage: FC = () => <div>Enter email</div>;
const flow = [ChooseTypePage, ChooseDatePage, EnterEmailPage];
const NewBooking: FC = () => {
const { page, activePageIndex } = useBookingFlow(flow);
return (
<div className="min-h-screen w-full bg-cover bg-fixed py-12">
<div className="mx-auto flex max-w-3xl flex-col justify-between rounded border border-gray-200 bg-gray-50 px-12 py-8 shadow-lg">
<div>{page}</div>
<div className="mx-auto mt-8 flex flex-row space-x-3">
{flow.map((_, index) => (
<div
key={index}
className={clsx(
{
'bg-green-200 text-green-700': index === activePageIndex,
'bg-gray-400 text-white': index > activePageIndex,
'bg-gray-200 text-gray-400': index < activePageIndex,
},
'flex h-8 w-8 select-none items-center justify-center rounded-full'
)}
>
{index + 1}
</div>
))}
</div>
</div>
</div>
);
};
export default NewBooking;

This lesson preview is part of the Fullstack Typescript with TailwindCSS and tRPC Using Modern Features of PostgreSQL course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Fullstack Typescript with TailwindCSS and tRPC Using Modern Features of PostgreSQL, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
