DEV Community

Cover image for Frontend Challenges: Front-end Engineer Assignment
TK
TK

Posted on • Originally published at iamtk.co

Frontend Challenges: Front-end Engineer Assignment

The original post was published at iamtk.co

This is part of the Frontend Challenges Series.

Today I finished a frontend challenge and I had a blast working on it. There were so many interesting concepts I debated with myself while designing and implementing the feature, so I was eager to document and share everything I learned through this entire journey.

Let's talk about the challenge:

Technology

  • The project is setup using React
  • They prefer using TypeScript (or Flow)
  • They use EmotionJS as a CSS tool

UI

The idea is to create a Guest and Room Overlay component. The user can open it, add different rooms, select any limited number of adults and children, and select the children's ages.

The findhotel home, the dialog opened, and the dialog with multiple rooms

Input rules

The component should be able to pass a string as the default data. These are the rules:

  • Rooms are separated by pipe |
  • Adults and children are separated by a colon :
  • Children ages are separated by a comma ,

Examples:

  • "1:4,6|3" → Two rooms, one with one adult and two children ages four and six and the other with three adults and no children
  • "3" → One room with three adults and no children
  • "2:4" → One room with two adults and one child aged four
  • "1:0,13,16" → One room with one adult and three children (aged zero, thirteen, and sixteen)

Functional Requirements

  • Up to eight rooms can be added
  • Each room has at least one adult and up to a maximum of five
  • Each room has zero or more children up to a maximum of three
  • Each child needs to have their age supplied, so we know what kind of bed or cot to provide and what to charge for the room
  • Each room has a maximum occupancy of five. This is, adults plus children per room
  • The Guest and Room selector should always yield a valid room occupancy, use button disablement to avoid invalid configurations
  • A user can either click Search to commit the output to the URL or click the x on top to reset the chosen room selection and revert the UI back to the original state.

Now, giving this introduction to the challenge, I want to share the topics I will cover in this piece. The main topics are:

  • Data Structure & State Management: in this part, we'll discuss how to design the UI’s state data structure and manage it throughout the entire component.
  • UI & Style: creating reusable components, handling responsive design with media queries and react-device-detect, and handling animation.
  • Unit & Integration tests: the implementation of the tests to make us confident about the feature. Unit tests will be handled by react-testing-library and Integration tests by Cypress.

Data Structure & State Management

I came up with a data structure to represent this UI and it looks like this:

{
  rooms: [
    {
      adultsCount: <number>,
      children: [
        {
          age: <number>,
        },
      ],
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

A TypeScript implementation looks like this:

type Child = {
  age: number;
};

type Room = {
  adultsCount: number;
  children: Child[];
};

type GuestRooms = {
  rooms: Room[];
};
Enter fullscreen mode Exit fullscreen mode

And an example would look like this:

const GuestRooms = {
  rooms: [
    {
      adultsCount: 2,
      children: [
        {
          age: 4,
        },
      ],
    },
    {
      adultsCount: 2,
      children: [
        {
          age: 4,
        },
        {
          age: 10,
        },
      ],
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

Now that we have defined the data structure and have a simple implementation of it, we can move to the next part that's how we use this data and which API we should provide to update this state in different parts of the component.

Listing all the behaviors makes it easier to understand how we should handle the data and what are the APIs we need to provide for each component to update our data structure.

I sketched a little drawing with all the behaviors:

Showing each component's behavior in the guest rooms dialog

Let's list them here:

  • Update adults count for room X: updateAdultsCount, which should receive the room index and the new count. The function's type contract should be:
  (roomIndex: number, count: number) => void
Enter fullscreen mode Exit fullscreen mode
  • Update children count for room X: addChild, which should receive the room index and it should add a new child to the children list with the age's value as 8 (the default age's value). The function's type contract should be:
  (roomIndex: number) => void
Enter fullscreen mode Exit fullscreen mode
  • Remove a child from room X: removeChild, which should receive the room index and the child index. The function's type contract should be:
  (roomIndex: number, childIndex: number) => void
Enter fullscreen mode Exit fullscreen mode
  • Remove room X: removeRoom, which should receive the room index. The function's type contract should be:
  (roomIndex: number) => void
Enter fullscreen mode Exit fullscreen mode
  • Select a child's age from room X: updateChild, which should receive the room index, the child index, and the new child age.
  (roomIndex: number, childIndex: number, childAge: number) => void
Enter fullscreen mode Exit fullscreen mode
  • Add new room section: addRoom, which should just add a new room to the rooms list.
  () => void
Enter fullscreen mode Exit fullscreen mode
  • Search with the selected rooms & guests: this function won't update our data structure, it'll only receive the data structure, transform it into a string representation and append the result as a query param in the url.

Nice, we have all the necessary APIs to handle the component's state. Now let's start implementing them all.

Context Provider

When I started implementing this solution, I didn't want to use any library or framework to handle the state. I wanted to keep it very simple. I start with a useState hook. But soon some interesting (and common) problems started to arise.

If we have centralized data, to be able to access it using a useState hook, we need to the state via props to all components. And prop drilling can be a big problem in terms of runtime performance and impacting user experience. Updating the state had the same issue. I needed to pass this update function as a prop for all components.

The second solution that I came across, as I still didn't want to use a library for it, was to use the Context API and provide the state's data structure and function APIs to every component that is wrapped by the context provider. This is the fundamental part of my solution to handle the state.

The provider will be pretty simple. It should just be a component that wraps our component and provide a value for it.

export const GuestRoomsContext = createContext<GuestRoomsValues>(undefined);

const GUEST_ROOMS_DEFAULT = {
  rooms: [
    {
      adultsCount: 2,
      children: [],
    },
  ],
};

type GuestRoomsProviderPropTypes = {
  guestRoomsString?: string;
};

export const GuestRoomsProvider: FC<GuestRoomsProviderPropTypes> = ({
  children,
  guestRoomsString,
}) => {
  const defaultGuestRooms = guestRoomsString
    ? toGuestRooms(guestRoomsString)
    : GUEST_ROOMS_DEFAULT;

  const [guestRooms, setGuestRooms] = useState<GuestRooms>(defaultGuestRooms);

  // ...

  return (
    <GuestRoomsContext.Provider value={providerValue}>
      {children}
    </GuestRoomsContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

So it will receive children and a guestRoomsString. Receiving the guestRoomsString enables us to pass a string as the initial state of our data structure. You can see in the GuestRoomsProviderPropTypes, that this prop is optional, so if we don't pass any string for the provider, it should use the default value GUEST_ROOMS_DEFAULT.

We also use a simple useState and it should be the source of truth of our data. guestRooms is the state and setGuestRooms is the function API to update the state.

The GuestRoomsContext is created and exported. We will use this context in our components to access the data and function APIs. And we also use it to create the provider. children is wrapped by this provider and we also see a providerValue that will talk about it soon.

Before moving to the other implementations, I want to talk a bit about the toGuestRooms function. This is just a transformer, specifically transforming the string format into a GuestRooms data structure.

Why did I decide to do this? My approach is to have an internal data structure for the component instead of using string as the state type. I think designing a better data structure to represent the state of this UI would help a lot when managing the state. This is how is looks like:

const ROOM_SEPARATOR = '|';
const ADULT_CHILDREN_SEPARATOR = ':';
const CHILDREN_SEPARATOR = ',';

function parseChildren(children: string) {
  return children
    .split(CHILDREN_SEPARATOR)
    .map((age: string) => ({ age: Number(age) }));
}

function parseGuestRooms(guestRooms: GuestRooms, room: string) {
  const [adultsCount, childrenString] = room.split(ADULT_CHILDREN_SEPARATOR);
  const children = childrenString ? parseChildren(childrenString) : [];

  guestRooms.rooms.push({
    adultsCount: Number(adultsCount),
    children,
  });

  return guestRooms;
}

export function toGuestRooms(guestRooms: string) {
  const rooms = guestRooms.split(ROOM_SEPARATOR);
  const guestRoomsInitialValue = { rooms: [] };

  return rooms.reduce<GuestRooms>(parseGuestRooms, guestRoomsInitialValue);
}
Enter fullscreen mode Exit fullscreen mode

Using the separators to get each meaningful data and returning the GuestRooms data structure.

As a pure function, we can easily test it.

describe('toGuestRooms', () => {
  it('generates GuestRooms based on "1:4,6|3"', () => {
    expect(toGuestRooms('1:4,6|3')).toEqual({
      rooms: [
        {
          adultsCount: 1,
          children: [
            {
              age: 4,
            },
            {
              age: 6,
            },
          ],
        },
        {
          adultsCount: 3,
          children: [],
        },
      ],
    });
  });

  it('generates GuestRooms based on "3"', () => {
    expect(toGuestRooms('3')).toEqual({
      rooms: [
        {
          adultsCount: 3,
          children: [],
        },
      ],
    });
  });

  it('generates GuestRooms based on "2:4"', () => {
    expect(toGuestRooms('2:4')).toEqual({
      rooms: [
        {
          adultsCount: 2,
          children: [
            {
              age: 4,
            },
          ],
        },
      ],
    });
  });

  it('generates GuestRooms based on "1:0,13,16"', () => {
    expect(toGuestRooms('1:0,13,16')).toEqual({
      rooms: [
        {
          adultsCount: 1,
          children: [
            {
              age: 0,
            },
            {
              age: 13,
            },
            {
              age: 16,
            },
          ],
        },
      ],
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

...to make sure it works and also gain confidence in the implementation.

Number input

Let's now create the NumberInput component as it will be the building block for the adults count input and children count input.

This component is a very simple one. It should only handle the UI and be able to receive data and functions to trigger when necessary.

The type contract (or prop types) should be like:

type NumberInputPropTypes = {
  value: number;
  increaseValue: () => void;
  decreaseValue: () => void;
  minValue: number;
  maxValue: number;
};
Enter fullscreen mode Exit fullscreen mode
  • value: the value we want to show to the user.
  • increaseValue: the function to increase the value (meaning adults or children count)
  • decreaseValue: the function to decrease the value (meaning adults or children count)
  • minValue: the minimum value accepted by the component. It will be useful to disable the decrease button
  • maxValue: the maximum value accepted by the component. It will be useful to disable the increase button

And that's it.

There's a simple logic that I wanted to do to disable (or not) the increase and the decrease buttons.

const isAbleToDecreaseValue = value > minValue;
const isAbleToIncreaseValue = value < maxValue;

const isDecreaseDisabled = value === minValue;
const isIncreaseDisabled = value === maxValue;

const decreaseNumber = () => isAbleToDecreaseValue && decreaseValue();
const increaseNumber = () => isAbleToIncreaseValue && increaseValue();

const decreaseButtonVariant = isDecreaseDisabled ? 'disabled' : 'secondary';
const increaseButtonVariant = isIncreaseDisabled ? 'disabled' : 'secondary';
Enter fullscreen mode Exit fullscreen mode

I not only wanted to add a disabled variant for the buttons and change the UI but also disable the state update because it's possible for the user to just disable it via devtools and be able to click the button. Having this second constraint is nice to block this behavior.

And here's the UI:

<div>
  <Button
    disabled={isDecreaseDisabled}
    onClick={decreaseNumber}
    variant={decreaseButtonVariant}
  >
    <MinusIcon />
  </Button>
  <span>{value}</span>
  <Button
    disabled={isIncreaseDisabled}
    onClick={increaseNumber}
    variant={increaseButtonVariant}
  >
    <PlusIcon />
  </Button>
</div>
Enter fullscreen mode Exit fullscreen mode

Adults count input

Now that we have this base component, we can build the AdultsCountInput and the ChildrenCountInput on top of that.

It should be very simple actually.

type AdultsCountInputPropTypes = {
  roomIndex: number;
};

export const AdultsCountInput: FC<AdultsCountInputPropTypes> = ({
  roomIndex,
}) => {
  const { guestRooms, updateAdultsCount } = useContext(GuestRoomsContext);
  const adultsCount = getAdultsCount(guestRooms, roomIndex);

  const increaseValue = () => updateAdultsCount(roomIndex, adultsCount + 1);
  const decreaseValue = () => updateAdultsCount(roomIndex, adultsCount - 1);

  return (
    <NumberInput
      value={adultsCount}
      increaseValue={increaseValue}
      decreaseValue={decreaseValue}
      minValue={1}
      maxValue={5}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

The AdultsCountInput component can receive the roomIndex as we need this value to be able to update the correct adults count for a given room.

We use the useContext passing the GuestRoomsContext to get the guestRooms and the updateAdultsCount (that will be implemented in a sec).

But I want to focus on the getAdultsCount first. The idea is to just implement a “getter” to, well, get the adults’ count.

export function getAdultsCount(guestRooms: GuestRooms, roomIndex: number) {
  return guestRooms.rooms[roomIndex].adultsCount;
}
Enter fullscreen mode Exit fullscreen mode

It's pretty straightforward. It receives the guestRooms and the roomIndex and it should get the adultsCount from a specific room.

With that, we can use this value to pass to the NumberInput.

We can also see the minValue and maxValue:

minValue={1}
maxValue={5}
Enter fullscreen mode Exit fullscreen mode

These are parts of the business rules. For adults, it should have this interval.

Now let's talk about updateAdultsCount. As we mentioned earlier, it should have this type definition:

updateAdultsCount: (roomIndex: number, count: number) => void;
Enter fullscreen mode Exit fullscreen mode

Within the provider, we can access the guestRooms state and the setGuestRooms function to update the state. Receiving the roomIndex and the new adults’ count should be enough to update the state.

function updateAdultsCount(roomIndex: number, count: number) {
  guestRooms.rooms[roomIndex] = {
    ...guestRooms.rooms[roomIndex],
    adultsCount: count,
  };

  setGuestRooms({
    rooms: guestRooms.rooms,
  });
}
Enter fullscreen mode Exit fullscreen mode

And that's it. We use the spread operator to just update the adultsCount and keep the children value. Pass the updated value to the setGuestRooms and it should update properly.

Back to the component, we can use this new function:

const increaseValue = () => updateAdultsCount(roomIndex, adultsCount + 1);
const decreaseValue = () => updateAdultsCount(roomIndex, adultsCount - 1);
Enter fullscreen mode Exit fullscreen mode

The increaseValue should add +1 to the adultsCount and the decreaseValue should add -1 to the adultsCount.

Children count input

The ChildrenCountInput has a similar behavior but the data structure is a bit different. For the adults, the data representation is a number. For children, it's a list of objects.

type ChildrenCountInputPropTypes = {
  roomIndex: number;
};

export const ChildrenCountInput: FC<ChildrenCountInputPropTypes> = ({
  roomIndex,
}) => {
  const { guestRooms, addChild, removeChild } = useContext(GuestRoomsContext);
  const childrenCount = getChildrenCount(guestRooms, roomIndex);

  const increaseValue = () => addChild(roomIndex);
  const decreaseValue = () => removeChild(roomIndex);

  return (
    <NumberInput
      value={childrenCount}
      increaseValue={increaseValue}
      decreaseValue={decreaseValue}
      minValue={0}
      maxValue={3}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

The ChildrenCountInput also has a roomIndex prop. It should receive a minValue and a maxValue. As the functional requirements say, the minimum should be 0 and the maximum children's count should be 3.

The getChildrenCount is also very similar.

export function getChildrenCount(guestRooms: GuestRooms, roomIndex: number) {
  return guestRooms.rooms[roomIndex].children.length;
}
Enter fullscreen mode Exit fullscreen mode

Get the children's length from a specific room.

To increase and decrease the children's count, we should add a new child or remove the child from the children's list. Let's implement the addChild and the removeChild functions.

function addChild(roomIndex: number) {
  const children = guestRooms.rooms[roomIndex].children;

  children.push({
    ...children,
    age: 8,
  });

  setGuestRooms({
    rooms: guestRooms.rooms,
  });
}
Enter fullscreen mode Exit fullscreen mode

It receives the roomIndex, gets the children’s list, and pushes a new child with the age of 8 (the default age). Then we should just update the guestRooms state.

The removeChild should work in a similar way but remove a specific child.

function removeChild(roomIndex: number, childIndex: number = -1) {
  const children = guestRooms.rooms[roomIndex].children;

  children.splice(childIndex, 1);

  setGuestRooms({
    rooms: guestRooms.rooms,
  });
}
Enter fullscreen mode Exit fullscreen mode

We use splice to remove the child by its index and then update the guestRooms state.

It receives a childIndex because, in the future, we should use it in our favor to remove a specific child. In this case, we just want to remove the last one. This is why we add a default value as -1, so when calling splice, it removes the last one.

Child select

The next part is about the ChildSelect. It should show all possible age options and handle the select on change.

About the options, I just created a ageOptions with a simple array.

const ageOptions = [...Array(18)];
Enter fullscreen mode Exit fullscreen mode

And we use it to create all options for the select. The entire the ChildSelect component will look like this:

type ChildSelectPropTypes = {
  child: Child;
  roomIndex: number;
  index: number;
};

export const ChildSelect: FC<ChildSelectPropTypes> = ({
  child,
  roomIndex,
  index,
}) => {
  const { updateChild } = useContext(GuestRoomsContext);

  const childAgeOnChange =
    (childIndex: number) => (event: ChangeEvent<HTMLSelectElement>) => {
      const childAge = Number(event.target.value);
      updateChild(roomIndex, childIndex, childAge);
    };

  return (
    <select onChange={childAgeOnChange(index)} value={child.age}>
      {ageOptions.map((_, age) => (
        <option
          value={age}
          key={`${roomIndex}-child-${index}-age-option-${age}`}
        >
          {age ? age : '<1'}
        </option>
      ))}
    </select>
  );
};
Enter fullscreen mode Exit fullscreen mode

The component receives the child (to get the current age), the roomIndex (to be able to find and update the child in a specific room), and the index (the child's index to update its age).

Now we need to implement the updateChild in the provider. This is the type definition:

updateChild: (
  roomIndex: number,
  childIndex: number,
  childAge: number
) => void;
Enter fullscreen mode Exit fullscreen mode

And the implementation looks like this:

function updateChild(roomIndex: number, childIndex: number, childAge: number) {
  const children = guestRooms.rooms[roomIndex].children;

  children[childIndex] = {
    age: childAge,
  };

  guestRooms.rooms[roomIndex] = {
    ...guestRooms.rooms[roomIndex],
    children,
  };

  setGuestRooms({
    rooms: guestRooms.rooms,
  });
}
Enter fullscreen mode Exit fullscreen mode

The idea here is to get a specific child from a given room, update this child's age, and update the guestRooms state.

This component is used by the ChildrenSelect, where we get all children from a room and iterate through it:

export const ChildrenSelect = ({ roomIndex }: ChildrenSelectPropTypes) => {
  const { guestRooms } = useContext(GuestRoomsContext);
  const chidren = getChildren(guestRooms, roomIndex);

  return (
    <div className={childrenSelectWrapper}>
      {chidren.map((child, index) => (
        <div
          className={childAgeSelectWrapper}
          key={`${roomIndex}-child-${index}`}
        >
          <span>Child {index + 1} age</span>
          <div className={selectWrapperStyle}>
            <ChildSelect child={child} roomIndex={roomIndex} index={index} />
            <CloseButton roomIndex={roomIndex} index={index} />
          </div>
        </div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Here are just iterating through the children. To get the children, we need to implement a simple getter.

export function getChildren(guestRooms: GuestRooms, roomIndex: number) {
  return guestRooms.rooms[roomIndex].children;
}
Enter fullscreen mode Exit fullscreen mode

Remove child

Now that we can add a new child and update its age, we need to be able to remove it with the close button.

type CloseButtonPropTypes = {
  roomIndex: number;
  index: number;
};

export const CloseButton: FC<CloseButtonPropTypes> = ({ roomIndex, index }) => {
  const { removeChild } = useContext(GuestRoomsContext);

  const removeOnClick = (childIndex: number) => () => {
    removeChild(roomIndex, childIndex);
  };

  return (
    <Button variant="danger" onClick={removeOnClick(index)}>
      <CloseIcon />
    </Button>
  );
};
Enter fullscreen mode Exit fullscreen mode

It's actually a very simple implementation. We need a button and a way to handle the button's onClick event. Remember when I said that we could use the removeChild in other places too? This is the case for this component. To remove it, we'll use the removeChild function we already implemented but now passing the childIndex for it, so we can remove a specific child from a room.

And that's it!

Add room

Adding a new room is also very simple. We need a button and the addRoom function that will update the rooms list by pushing a new room with the default value to it.

<Button variant="secondary" onClick={addRoom} fullWidth>
  + Add room
</Button>
Enter fullscreen mode Exit fullscreen mode

And the addRoom implementation looks like this:

function addRoom() {
  setGuestRooms({
    rooms: [
      ...guestRooms.rooms,
      {
        adultsCount: 2,
        children: [],
      },
    ],
  });
}
Enter fullscreen mode Exit fullscreen mode

We keep the current rooms and add a new room two adults and no children.

Remove room

To remove a room, we need a button and the room's index.

const { removeRoom } = useContext(GuestRoomsContext);
const removeRoomOnClick = (roomIndex: number) => () => {
  removeRoom(roomIndex);
};

<Button variant="danger" onClick={removeRoomOnClick(index)}>
  Remove room
</Button>;
Enter fullscreen mode Exit fullscreen mode

We have the button and the removeRoomOnClick. Now we should implement the removeRoom function:

function removeRoom(roomIndex: number) {
  guestRooms.rooms.splice(roomIndex, 1);

  setGuestRooms({
    rooms: guestRooms.rooms,
  });
}
Enter fullscreen mode Exit fullscreen mode

Here we use the same concept we used to remove children from the children's list. Using splice with a specific roomIndex and then updating the guestRooms state.

Search button

To handle the search button, I needed to enable users (engineers) to pass a callback function to the main component and pass it down to the search button component to call it when the user clicks the button. This way, we enable engineers to do whatever they want with the current state.

In the case of this challenge, we are required to get the state data structure, transform it into the string format and attach it to the url.

To do this transformation, we can create a simple function to handle this part:

const ROOM_SEPARATOR = '|';
const ADULT_CHILDREN_SEPARATOR = ':';
const CHILDREN_SEPARATOR = ',';

function toChildrenAgesString(children: Child[]) {
  return children.map(({ age }) => age).join(CHILDREN_SEPARATOR);
}

function toAdultsAndChildrenAgesString({ adultsCount, children }: Room) {
  const childrenAges = toChildrenAgesString(children);

  return childrenAges
    ? adultsCount + ADULT_CHILDREN_SEPARATOR + childrenAges
    : adultsCount;
}

export function toGuestRoomsString(guestRooms: GuestRooms) {
  return guestRooms.rooms
    .map(toAdultsAndChildrenAgesString)
    .join(ROOM_SEPARATOR);
}
Enter fullscreen mode Exit fullscreen mode

A toGuestRoomsString to transform the GuestRooms data structure into a string. We use the separators to construct it. To “prove” it works, we can add some tests and gain more confidence.

describe('toGuestRoomsString', () => {
  it('generates "1:4,6|3"', () => {
    expect(
      toGuestRoomsString({
        rooms: [
          {
            adultsCount: 1,
            children: [
              {
                age: 4,
              },
              {
                age: 6,
              },
            ],
          },
          {
            adultsCount: 3,
            children: [],
          },
        ],
      }),
    ).toEqual('1:4,6|3');
  });

  it('generates "3"', () => {
    expect(
      toGuestRoomsString({
        rooms: [
          {
            adultsCount: 3,
            children: [],
          },
        ],
      }),
    ).toEqual('3');
  });

  it('generates "2:4"', () => {
    expect(
      toGuestRoomsString({
        rooms: [
          {
            adultsCount: 2,
            children: [
              {
                age: 4,
              },
            ],
          },
        ],
      }),
    ).toEqual('2:4');
  });

  it('generates "1:0,13,16"', () => {
    expect(
      toGuestRoomsString({
        rooms: [
          {
            adultsCount: 1,
            children: [
              {
                age: 0,
              },
              {
                age: 13,
              },
              {
                age: 16,
              },
            ],
          },
        ],
      }),
    ).toEqual('1:0,13,16');
  });
});
Enter fullscreen mode Exit fullscreen mode

That's it! Now we are able to transform it into the string format before attaching it to the URL. To the function and call the callback with the result of it, I created a search function:

function search(guestRooms: GuestRooms, callback: OnSearchFunction) {
  const guestRoomsString = toGuestRoomsString(guestRooms);

  return () =>
    callback(
      { guestRooms: guestRoomsString },
      `?guestRooms=${guestRoomsString}`,
    );
}
Enter fullscreen mode Exit fullscreen mode

This way, we just need to implement a possible callback for it. As I'm not using any library or framework for this, we can use the History API.

type State = any;
type Url = string | null;

export type PushStateSignature = (state: State, url?: Url) => void;

export const pushState: PushStateSignature = (state, url) => {
  window.history.pushState(state, '', url);
};
Enter fullscreen mode Exit fullscreen mode

It expects the state and the URL. Passing the pushState as a callback for the search function and we are able to append the guest rooms string as a query param.

UI & Style

Building a lot of different React apps, ones with pure React others with NextJS, made me experience different approaches to CSS styles. Even though it's simple to use inline CSS on React components, I didn't really like the experience as it lacks a lot of “features” like pseudo-classes, selectors.

So in this challenge, I was open to learning and applying a new CSS tool. I’ve heard about emotion-js before but never really tried. It seemed very simple, just CSS styles that you could attach to components. It's like the old days when you just write pure CSS but now with the power of making it modular.

I didn't want to use styled components, so I just installed @emotion/css.

npm i @emotion/css
Enter fullscreen mode Exit fullscreen mode

Button

The first component I wanted to focus on was the <Button>. I wanted to make a component that I could reuse for the entire application. With a “type”, I could change the entire style of a component, so what I built was a variant and this it looks like:

type ButtonVariants = 'primary' | 'secondary' | 'disabled' | 'danger' | 'close';
Enter fullscreen mode Exit fullscreen mode

And now we are able to use this as the prop type:

type ButtonPropTypes = {
  variant?: ButtonVariants;
};
Enter fullscreen mode Exit fullscreen mode

If the users (engineers using this component) are also using TypeScript, it requires they use one of these variants in compile time. This is a beautiful use of TypeScript with React.

With this variant, we can style anything. I used the idea of an object that matches a variant with its style. The first is the cursor:

const Cursor = {
  primary: 'pointer',
  secondary: 'pointer',
  disabled: 'not-allowed',
  danger: 'pointer',
  close: 'pointer',
};
Enter fullscreen mode Exit fullscreen mode

And the usage is simple:

cursor: ${Cursor[variant]};
Enter fullscreen mode Exit fullscreen mode

We would do the same thing for all other styles:

const Colors = {
  primary: 'white',
  secondary: '#0071f3',
  disabled: '#6a7886',
  danger: '#d83b3b',
  close: '#6a7886',
};

const BackgroundColors = {
  primary: '#0071f3',
  secondary: '#f7fbff',
  disabled: '#eff2F6',
  danger: 'rgba(255, 255, 255, 0)',
  close: 'rgba(255, 255, 255, 0)',
};

const BackgroundColorsHover = {
  primary: '#0064d8',
  secondary: '#e4f0fe',
  disabled: '#eff2F6',
  danger: 'rgba(255, 255, 255, 0)',
  close: 'rgba(255, 255, 255, 0)',
};

const BoxShadow = {
  primary: 'none',
  secondary: '#bfdaf9 0px 0px 0px 1px inset',
  disabled: 'none',
  danger: 'none',
  close: 'none',
};
Enter fullscreen mode Exit fullscreen mode

The usage is similar to the cursor:

color: ${Colors[variant]};
background-color: ${BackgroundColors[variant]};
box-shadow: ${BoxShadow[variant]};

&:hover {
  background-color: ${BackgroundColorsHover[variant]};
}
Enter fullscreen mode Exit fullscreen mode

In this component, I also enable it to receive these props: disabled, onClick, dataTestid, and children.

<button
  disabled={disabled}
  onClick={onClick}
  data-testid={dataTestid}
    ...
>
    {children}
</button>
Enter fullscreen mode Exit fullscreen mode

I also saw the need to be able to customize the styles if the user wants. For example, the component has a default padding spacing. But the user could need a different spacing for padding, so we can add a className prop as well and add it to the css like this:

className={css`
    ...
    ${className}
`}
Enter fullscreen mode Exit fullscreen mode

It's actually a lot of power we give to the user. The option was to have an object for the padding and margin, and any other property we want to match with the button's variant.

The last part of this component is the prop fullWidth. The name says everything. If enabling this prop, the button will have a full width, if not it will have an auto width.

width: ${fullWidth ? '100%' : 'auto'};
Enter fullscreen mode Exit fullscreen mode

The prop types look like this:

type ButtonVariants = 'primary' | 'secondary' | 'disabled' | 'danger' | 'close';

type ButtonPropTypes = {
  disabled?: boolean;
  onClick: () => void;
  variant?: ButtonVariants;
  className?: string;
  fullWidth?: boolean;
  dataTestid?: string;
};
Enter fullscreen mode Exit fullscreen mode

And the whole component has these props, types, and styles.

export const Button: FC<ButtonPropTypes> = ({
  children,
  disabled = false,
  onClick,
  variant = 'primary',
  className,
  fullWidth = false,
  dataTestid,
}) => (
  <button
    disabled={disabled}
    onClick={onClick}
    data-testid={dataTestid}
    className={css`
      display: inline-flex;
      border: 0px;
      border-radius: 6px;
      margin: 0px;
      cursor: ${Cursor[variant]};
      align-items: center;
      justify-content: center;
      text-align: center;
      vertical-align: middle;
      position: relative;
      text-decoration: none;
      font-size: 16px;
      font-weight: 600;
      padding: 16px 32px;
      color: ${Colors[variant]};
      background-color: ${BackgroundColors[variant]};
      box-shadow: ${BoxShadow[variant]};

      width: ${fullWidth ? '100%' : 'auto'};

      &:hover {
        background-color: ${BackgroundColorsHover[variant]};
      }

      ${className}
    `}
  >
    {children}
  </button>
);
Enter fullscreen mode Exit fullscreen mode

Animation

To make sure we have an effect when opening the overlay component in the mobile view, we will use keyframes and animation.

The code looks very simple for this transition.

The keyframes is imported from the emotion library, we create an animation name to go from top 100% to top 0, and set the duration for this transition.

import { css, keyframes } from '@emotion/css';

const overlayFade = keyframes`
  from {
    top: 100%;
  }
  to {
    top: 0;
  }
`;

const modelStyle = css`
  // ...
  animation-name: ${overlayFade};
  animation-duration: 0.3s;
  // ...
`;
Enter fullscreen mode Exit fullscreen mode

Simple as that.

Responsive design

To handle responsive design, I focused on mobile-first, and additional adjustments for desktop.

To be able to change a style for specific screen size, we can use media queries. Using emotion-js looks like this:

const style = css`
  border-radius: 0;

  @media (min-width: 576px) {
    border-radius: 6px;
  }
`;
Enter fullscreen mode Exit fullscreen mode

For mobile view, it won't have a border-radius, but all screens with a minimum size of 576px will have a border-radius of 6px.

To make it more consistent across all components and remove the need to write the correct media query, I created a mediaQuery object with all the possibilities.

type Breakpoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
type MediaQuery = Record<Breakpoints, string>;

export const mediaQuery: MediaQuery = {
  xs: '@media (max-width: 576px)',
  sm: '@media (min-width: 576px)',
  md: '@media (min-width: 768px)',
  lg: '@media (min-width: 992px)',
  xl: '@media (min-width: 1200px)',
};
Enter fullscreen mode Exit fullscreen mode

Now we can use our object without the need to understand the implementation details about each query. Refactoring the style CSS code above, we get something like this:

const style = css`
  border-radius: 0;

  ${mediaQuery['sm']} {
    border-radius: 6px;
  }
`;
Enter fullscreen mode Exit fullscreen mode

Now we can reuse this mediaQuery code in all components that need to handle different views.

I also created a simple unit test for it:

describe('mediaQuery', () => {
  it('returns the correct media query for each breakpoint', () => {
    expect(mediaQuery['xs']).toEqual('@media (max-width: 576px)');
    expect(mediaQuery['sm']).toEqual('@media (min-width: 576px)');
    expect(mediaQuery['md']).toEqual('@media (min-width: 768px)');
    expect(mediaQuery['lg']).toEqual('@media (min-width: 992px)');
    expect(mediaQuery['xl']).toEqual('@media (min-width: 1200px)');
  });
});
Enter fullscreen mode Exit fullscreen mode

I also needed to handle different HTML elements and styles for desktop vs. mobile views. This is why I used a library called react-device-detect.

In this case, our desktop modal should not only have a modal component but also a backdrop-overlay side by side. If the user clicks the overlay, it should close the modal.

In the mobile view, it doesn't have this overlay component. It should just open a dialog.

The desktop dialog:

export const DialogBrowserView: FC<DialogBrowserViewPropTypes> = ({
  guestRoomsString,
  onClose,
  onSearch,
}) => (
  <BrowserView>
    <div className={dialogStyle}>
      <div onClick={onClose} className={backdropStyle} />
      <Dialog
        guestRoomsString={guestRoomsString}
        onClose={onClose}
        onSearch={onSearch}
      />
    </div>
  </BrowserView>
);
Enter fullscreen mode Exit fullscreen mode

And the mobile dialog:

export const DialogMobileView: FC<DialogMobileViewPropTypes> = ({
  guestRoomsString,
  onClose,
  onSearch,
}) => (
  <MobileView>
    <Dialog
      guestRoomsString={guestRoomsString}
      onClose={onClose}
      onSearch={onSearch}
    />
  </MobileView>
);
Enter fullscreen mode Exit fullscreen mode

And use them:

<DialogBrowserView
  guestRoomsString={guestRoomsString}
  onClose={onClose}
  onSearch={onSearch}
/>
<DialogMobileView
  guestRoomsString={guestRoomsString}
  onClose={onClose}
  onSearch={onSearch}
/>
Enter fullscreen mode Exit fullscreen mode

We could also replace react-device-detect with media queries.

Code splitting

An additional thing that I did was to code split the dialog. Create a chunk for the mobile dialog and another chunk for the desktop dialog.

That way the application won't need to load the mobile dialog code in the desktop view and vice versa.

To do this, I used a library called Loadable Components.

I just needed to do this for the mobile dialog

import loadable from '@loadable/component';

export default loadable(
  () => import(/* webpackChunkName: "DialogMobileView" */ './DialogMobileView'),
);
Enter fullscreen mode Exit fullscreen mode

And this for the desktop view

import loadable from '@loadable/component';

export default loadable(
  () =>
    import(/* webpackChunkName: "DialogBrowserView" */ './DialogBrowserView'),
);
Enter fullscreen mode Exit fullscreen mode

And now the application doesn't need to load unnecessary JavaScript code for each screen size.

Unit & Integration Tests

Unit tests

To verify that all variants are working with the correct styles, I created a test for each variant.

describe('Button', () => {
  describe('primary variant', () => {
    it('verifies correct styles for primary button', () => {
      render(<Button onClick={noop}>{text}</Button>);

      const buttonText = screen.getByText(/Text/i);

      expect(buttonText).toBeInTheDocument();
      expect(buttonText).toHaveStyle('cursor: pointer');
      expect(buttonText).toHaveStyle('color: white');
      expect(buttonText).toHaveStyle('background-color: #0071f3');
      expect(buttonText).toHaveStyle('box-shadow: none');
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

We can use the toHaveStyle API to verify each CSS property. I wanted to test if the button is rendered and these four properties: cursor, color, background-color, and box-shadow.

And I also have a similar test for all other variants: secondary, disabled, danger, and close.

For the header, I added a very simple unit test to verify the header text and if the close button triggers everything correctly.

const noop = jest.fn();

describe('Header', () => {
  it('renders the header text', () => {
    render(<Header onClose={noop} />);

    const headerText = screen.getByText(/Rooms & Guests/i);
    expect(headerText).toBeInTheDocument();
  });

  it('triggers the onClose after clicking the close button', () => {
    render(<Header onClose={noop} />);

    const onCloseButton = screen.getByRole('button');
    userEvent.click(onCloseButton);
    expect(noop).toBeCalled();
  });
});
Enter fullscreen mode Exit fullscreen mode

For the header text, it's a nice test, but mocking the onClose function is not ideal. I will test it properly in an integration test where we simulate how the user interacts with the dialog and close it.

The test for the AdultsCountInput is very interesting as we can test it the way an user would use it.

describe('AdultsCountInput', () => {
  it('increases and decreases count by clicking buttons', () => {
    render(
      <GuestRoomsProvider>
        <AdultsCountInput roomIndex={0} />
      </GuestRoomsProvider>,
    );

    const count = screen.getByText('2');
    expect(count).toBeInTheDocument();

    const minusButton = screen.getAllByRole('button')[0];
    userEvent.click(minusButton);
    const decreasedCount = screen.getByText('1');
    expect(decreasedCount).toBeInTheDocument();

    const plusButton = screen.getAllByRole('button')[1];
    userEvent.click(plusButton);
    userEvent.click(plusButton);
    const increasedCount = screen.getByText('3');
    expect(increasedCount).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode
  • We start with rendering the component
  • Verify if the current count's value is correct
  • Click the button to decrease the count and verify if it actually decreases it
  • Click the button to increase the count twice and verify the current count's value

We can have a lot of confidence in this test as it simulates how a user would use it.

The test for the ChildrenCountInput works the same way:

describe('ChildrenCountInput', () => {
  it('increases and decreases count by clicking buttons', () => {
    render(
      <GuestRoomsProvider>
        <ChildrenCountInput roomIndex={0} />
      </GuestRoomsProvider>,
    );

    const count = screen.getByText('0');
    expect(count).toBeInTheDocument();

    const plusButton = screen.getAllByRole('button')[1];
    userEvent.click(plusButton);
    userEvent.click(plusButton);
    const increasedCount = screen.getByText('2');

    expect(increasedCount).toBeInTheDocument();
    const minusButton = screen.getAllByRole('button')[0];
    userEvent.click(minusButton);
    const decreasedCount = screen.getByText('1');
    expect(decreasedCount).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

The select component is also very interesting. The experience using userEvent is smooth and does what it intends to do.

But first, let's just add a test to verify the ChildrenSelect doesn't render any select because the current state doesn't have any children.

describe('ChildrenSelect', () => {
  it("does not render a child selector when there's no child", () => {
    render(
      <GuestRoomsProvider>
        <ChildrenSelect roomIndex={0} />
      </GuestRoomsProvider>,
    );

    const selectLabel = screen.queryByText('Child 1 age');
    expect(selectLabel).not.toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

Now we can create a test to interact with the select and choose different age options.

First I created a helper function to get the first option from the select element.

function getFirstOption(name: string) {
  return screen.getAllByRole('option', {
    name,
  })[0] as HTMLOptionElement;
}
Enter fullscreen mode Exit fullscreen mode

And now I can use it to verify the rendered selects and interact with each one of them.

describe('ChildrenSelect', () => {
  it('selects new option and verify selected item', () => {
    render(
      <GuestRoomsProvider guestRoomsString="1:4,6">
        <ChildrenSelect roomIndex={0} />
      </GuestRoomsProvider>,
    );

    const selectLabel1 = screen.getByText('Child 1 age');
    expect(selectLabel1).toBeInTheDocument();

    const selectLabel2 = screen.getByText('Child 2 age');
    expect(selectLabel2).toBeInTheDocument();

    const selectLabel3 = screen.queryByText('Child 3 age');
    expect(selectLabel3).not.toBeInTheDocument();

    const select = screen.getAllByRole('combobox')[0];
    const selectedOption = getFirstOption('4');
    expect(selectedOption.selected).toBeTruthy();

    const newSelectedOption = getFirstOption('3');
    userEvent.selectOptions(select, newSelectedOption);
    expect(selectedOption.selected).toBeFalsy();
    expect(newSelectedOption.selected).toBeTruthy();
  });
});
Enter fullscreen mode Exit fullscreen mode

Context: “1:4,6” means

  • 1 adult
  • 2 children: one with age 4 and the other with age 6.

We test a lot of things here:

  • Verify that child 1 and child 2 are rendered
  • Make sure that child 3 is not rendered
  • Verify if the selected option is the age 4
  • Select a new option (age 3)
  • Verify that the option age 4 is no more the selected one and now the selected option is age 3

For the NumberInput component, the test is pretty straightforward. Just render it and make sure the correct number is rendered.

describe('NumberInput', () => {
  it('renders the value between buttons', () => {
    const noop = () => {};

    render(
      <GuestRoomsProvider>
        <NumberInput
          value={3}
          increaseValue={noop}
          decreaseValue={noop}
          minValue={1}
          maxValue={5}
        />
      </GuestRoomsProvider>,
    );

    expect(screen.getByText('3')).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

The test for the SearchButton is also similar to the test above as we just want to make sure we are rendering the correct component with the right value.

describe('SearchButton', () => {
  it('renders the button', () => {
    render(
      <GuestRoomsProvider>
        <SearchButton onSearch={() => {}} />
      </GuestRoomsProvider>,
    );

    const button = screen.getByRole('button', {
      name: /Search 1 room • 2 guests/i,
    });

    expect(button).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

I also created a test for the GuestRooms component but it is very similar to the integration test I created later on. I will cover this test in the next section.

Integration tests

To gain even more confidence in the feature, I created an integration test using Cypress.

First, created a function to test the query params in the URL:

function verifyQueryParams(queryParams) {
  cy.location().should((location) => {
    expect(location.search).to.eq(queryParams);
  });
}
Enter fullscreen mode Exit fullscreen mode

Also created a function to click the search button and provide the verifyQueryParams:

function clickSearchButtonWithText(text) {
  cy.get('button').contains(text).click();

  return {
    andVerifyQueryParams: verifyQueryParams,
  };
}
Enter fullscreen mode Exit fullscreen mode

This way we can use it like this:

clickSearchButtonWithText('Search 1 room • 2 guests').andVerifyQueryParams(
  '?guestRooms=2',
);
Enter fullscreen mode Exit fullscreen mode

Then I created a function to handle the test for the adults’ count selection:

function selectAdultsCount() {
  const adultsBlock = 'div[data-testid="adults-count-input-block"]';

  cy.get(adultsBlock).within(() => {
    cy.contains('2').should('exist');

    const adultsMinusButton = cy.get('button[data-testid="minus-button"]');

    adultsMinusButton.click();
    adultsMinusButton.should('be.disabled');
    cy.contains('1').should('exist');

    const adultsPlusButton = cy
      .get('button[data-testid="plus-button"]')
      .first();

    adultsPlusButton.click();
    adultsPlusButton.click();
    adultsPlusButton.click();
    cy.contains('4').should('exist');
  });
}
Enter fullscreen mode Exit fullscreen mode
  • Verify the count is 2
  • Click the decrease button and verify the button is now disabled because it's the minimum number of adults and verify that 1 is rendered as the count
  • Then click the increase button 3 times and verify the current's count is 4

Now we need to create a function to test the children's count selection and their ages.

function selectChildrenCountAndAges() {
  const childrenBlock = 'div[data-testid="children-count-input-block"]';

  cy.get(childrenBlock).within(() => {
    cy.contains('0').should('exist');

    const childrenMinusButton = cy.get('button[data-testid="minus-button"]');

    childrenMinusButton.should('be.disabled');
    cy.contains('0').should('exist');

    const childrenPlusButton = cy
      .get('button[data-testid="plus-button"]')
      .first();

    childrenPlusButton.click();
    childrenPlusButton.click();
    childrenPlusButton.click();
    cy.contains('3').should('exist');

    cy.contains('Child 1 age');
    cy.contains('Child 2 age');
    cy.contains('Child 3 age');

    cy.get('button[data-testid="close-button-1"]').click();
    cy.contains('Child 3 age').should('not.exist');

    cy.get('select').first().select('3');
  });
}
Enter fullscreen mode Exit fullscreen mode
  • Verify it starts with the count 0 and the decrease button should be disabled
  • Click the increase button 3 times and it should add three age selects for each child's age
  • Click the close button for the third child and verify if it is not there anymore
  • Select the age for the first child

Now that we have all the building blocks, we can use them to create a complete test for the dialog.

function verifyGuestRoomsBehavior() {
  const openDialogButton = cy.get('button');
  openDialogButton.click();

  clickSearchButtonWithText('Search 1 room • 2 guests').andVerifyQueryParams(
    '?guestRooms=2',
  );

  const firstRoom = 'div[data-testid="room-key-0"]';

  cy.get(firstRoom).within(() => {
    selectAdultsCount();
    selectChildrenCountAndAges();
  });

  clickSearchButtonWithText('Search 1 room • 6 guests').andVerifyQueryParams(
    '?guestRooms=4:3,8',
  );

  cy.contains('Room 2').should('not.exist');
  cy.get('button').contains('+ Add room').click();
  cy.contains('Room 2').should('exist');

  const secondRoom = 'div[data-testid="room-key-1"]';

  cy.get(secondRoom).within(() => {
    selectAdultsCount();
    selectChildrenCountAndAges();
  });

  clickSearchButtonWithText('Search 2 rooms • 12 guests').andVerifyQueryParams(
    '?guestRooms=4:3,8|4:3,8',
  );

  cy.get('button').contains('Remove room').click();
  cy.contains('Room 2').should('not.exist');

  clickSearchButtonWithText('Search 1 room • 6 guests').andVerifyQueryParams(
    '?guestRooms=4:3,8',
  );
}
Enter fullscreen mode Exit fullscreen mode
  • Click the button to open the dialog
  • Click the search button and verify the expected query param in the URL
  • In the first room, select adults count and the children's count and ages
  • Click the search button again and verify the correct query param
  • Add a second room and add adults and children to it. Verify the query param again
  • Remove the second room, verify it is not there anymore, click the search button and verify the expected query param

I also created a function to handle the dialog close button and verify its behavior.

function verifyCloseButtonBehavior() {
  cy.contains('Rooms & Guests').should('exist');
  cy.get('button[data-testid="dialog-close-button"]').click();
  cy.contains('Rooms & Guests').should('not.exist');
}
Enter fullscreen mode Exit fullscreen mode

Everything together looks like this:

it('verifies guest rooms dialog behavior', () => {
  verifyGuestRoomsBehavior();
  verifyCloseButtonBehavior();
});
Enter fullscreen mode Exit fullscreen mode

But this is a test for the desktop. I also wanted to test it works fine for the mobile view. The idea is very similar, but add a different viewport and then test it.

describe('on iPhone X', () => {
  it('verifies guest rooms dialog behavior', () => {
    cy.viewport('iphone-x');
    verifyGuestRoomsBehavior();
    verifyCloseButtonBehavior();
  });
});
Enter fullscreen mode Exit fullscreen mode

Everything together looks like this:

describe('GuestRoomsDialog', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  describe('on iPhone X', () => {
    it('verifies guest rooms dialog behavior', () => {
      cy.viewport('iphone-x');
      verifyGuestRoomsBehavior();
      verifyCloseButtonBehavior();
    });
  });

  describe('on desktop', () => {
    it('verifies guest rooms dialog behavior', () => {
      verifyGuestRoomsBehavior();
      verifyCloseButtonBehavior();
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Let's see a preview of the integration test in action?

That's it!

This is the first article from this series I'm working on: Frontend Challenges. I want to challenge myself in different challenges in the frontend domain and see what I can learn from it. Every challenge will be documented and shared with you all.

I hope you liked this post and feel free to steal some ideas for the projects and products you are building.

See ya!

Resources

Discussion (15)

Collapse
guillep2k profile image
Guillermo Prandi

Impressive end-to-end walkthrough. Thank you!

Collapse
teekay profile image
TK Author

Thanks! glad you liked. Any feedback is always welcome!

Collapse
ayomiku222 profile image
ayomiku olatunji

Wow..I did something like this in my tech company and I know how technical it's to build something like this. You really did well bro. I loved the way you arrange your code and your documentation is top-notch. Kudos!!!

Collapse
teekay profile image
TK Author

Thank you! I really appreciate that!

Collapse
free_one profile image
Michael

I can't read with this font

Collapse
z2lai profile image
z2lai

The body text font? Is it too large/wide?

Collapse
free_one profile image
Michael

oh nevermind

Thread Thread
teekay profile image
TK Author

I hope you can read without any problem. Let me know if I can do anything to solve this issue!

Collapse
marekdano profile image
Marek Dano

Where did you get this challenge from? Did you make it by yourself?
It's very interesting article!!! Thanks for sharing it!!!

Collapse
teekay profile image
TK Author

It was part of an interview I did in the past!
Thank you, really appreciate the comment!

Collapse
joset98 profile image
joset98

dude it´s awesome that challenge and i like your approach, good job

Collapse
teekay profile image
TK Author

Thank you, really appreciate the comment! I put a lot of work on the challenge and the post.

Collapse
dmitrysobolevsky profile image
Dmitry Sobolevsky • Edited on

Great job!
Thanks for sharing!

Does Context better in your case in terms of performance ?

Collapse
teekay profile image
TK Author

Thank you! Really appreciate! :)
The Context API can be a performance bottleneck if used in the wrong way.
If you wrap your component and use a consumer only it really need to be used, you "solve" prop drilling and re-render only the necessary parts of the component (components that directly use the provider value).

You need to always be careful when implementing a context api driven component and keep track of the re-renders in your component. Other state management libraries could be a better choice for more complex cases.

Collapse
dmitrysobolevsky profile image
Dmitry Sobolevsky

Thanks for explanation. Appreciate it