Refactoring away Recompose

Written in July 2020

At my current employer, like many other software companies, we aim to prevent unnecessary code duplication (where it makes sense!) and to share functionality in our code base. In the earlier days of working with React, there was one relatively defacto solution for being able to share functionality between your components - Recompose: a library that aimed to make code reusability much simpler through the use of the highly lauded ‘Higher Order Component’ (or HOC) pattern that was extremely popular at the time.

As time moved on newer, more powerful, code sharing functionality was built directly into React in the form of hooks and the approach of using HOCs has been left by the wayside. Higher Order Components have proven to be cumbersome for developers as they’re more difficult to follow logically, are more difficult to debug, and require additional libraries to get the most out of. What’s more - recompose has discontinued active maintenance.

As a result, we’ve been working to move away from recompose, and instead leverage hooks or other patterns in its stead. This post contains some of our approaches as a front-end team for moving away from recompose, while still being able to leverage reusability that React provides.

This is the code we’re going to refactor away from compose:

import { withStateHandlers, compose, withHandlers, withProps } from "recompose";
import { PuppyWrapper } from "./puppy-wrapper.component";
import { selectPolicyBedTime } from "./puppies.selectors";
import { connect } from "react-redux";
import { REMOVE_PUPPY, ADJUST_PUPPY_END_DATE } from "./constants";

const withActive = withStateHandlers(({ active = false }) => ({ active }), {
  toggleActive: ({ active }) => () => ({ active: !active }),
});

const withConfirmAction = withStateHandlers(() => ({ confirmOpen: false }), {
  showConfirmation: () => (confirmType) => ({
    confirmOpen: true,
    confirmType,
  }),
  closeConfirmation: () => () => ({ confirmOpen: false }),
});

const withOnStatusChange = withHandlers({
  onStatusChange: ({ puppy, updatePuppyStatus }) => (status) => {
    updatePuppyStatus({ id: puppy.id, status });
  },
});

const withAddTreatsConfirmation = withStateHandlers(
  () => ({ addTreatsConfirmationVisible: false }),
  {
    showAddTreatsConfirmation: () => () => ({
      addTreatsConfirmationVisible: true,
    }),
    closeAddTreatsConfirmation: () => () => ({
      addTreatsConfirmationVisible: false,
    }),
  }
);

const mapStateToProps = withProps(({ puppy }) => ({
  userId: puppy.user_id,
  houseId: puppy.parent ? puppy.parent.provider_id : null,
  puppyType: puppy.parent ? puppy.parent.puppy_type : null,
  puppyStatus: puppy.status || puppy.puppy_status,
  puppyBedTime: puppy.puppy_end_date || null,
  userPuppiesView: !!puppy.parent,
}));

const withConfirmModalHandler = withHandlers({
  onConfirm: ({
    puppy,
    confirmType,
    removePuppy,
    onStatusChange,
    closeConfirmation,
    adjustPuppyBedTime,
    fullName,
    houseId,
    userId,
  }) => ({ newBedTime }) => {
    switch (confirmType) {
      case REMOVE_PUPPY:
        removePuppy(puppy);
        break;
      case ADJUST_PUPPY_END_DATE:
        adjustPuppyBedTime({
          puppyId: puppy.id,
          puppyName: fullName,
          newBedTime: newBedTime.toDate(),
          houseId,
          userId,
        });
        break;
      default:
        onStatusChange(confirmType);
    }
    closeConfirmation();
  },
});

const stateToProps = connect((state, { puppy }) => ({
  AdoptionDate: puppy.parent
    ? selectPolicyBedTime(puppy.parent.id)(state)
    : null,
}));

export const PuppyWrapperContainer = compose(
  stateToProps,
  mapStateToProps,
  withConfirmAction,
  withActive,
  withOnStatusChange,
  withAddTreatsConfirmation,
  withConfirmModalHandler
)(PuppyWrapper);

a lot.webp There’s a lot going on in that code block; don’t worry, we’ll tackle it bit by bit.

So with the above, we can see that there are a few compose functions that we need to get away from, but also we need to figure out how best to provide all that functionality from the PuppyWrapperContainer into the PuppyWrapperComponent. The methods that we need to get away from are: compose, withStateHandlers, withHandlers, and withProps. There is also some connect functionality here that provides redux interaction though a HOC, but we’re going to leave that there - we may refactor it to look a bit more like a standard connect method though.

Let’s look at the various methods that already exist, and roughly outline what we can do to get rid of them. But first maybe let’s look at the sort of end signature we are hoping to get to!

export const PuppyWrapperContainer = (props) => {
  // State handlers via hooks
  // Any other event handlers
  return <PuppyWrapper {...props} {...passingStateValues} {...eventHandlers} />;
};

This doesn’t account for the connect functionality, but that’s basically what we want to move towards. This takes us away from the confusing HOCs from heck, and becomes a lot more streamlined. Making it not only easier to grok when you’re reading through the code, but the debugging experience becomes significantly easier to trace through.

Step 1 - Fake it til you make it

fakeit.txt

We’re going to start by breaking everything and returning the code snippet above instead of the composed version. Yes this will break everything, but glory never came without a little adversity. That gives us:

export const PuppyWrapperContainerOld = compose(
  stateToProps,
  mapStateToProps,
  withConfirmAction,
  withActive,
  withOnStatusChange,
  withAddTreatsConfirmation,
  withConfirmModalHandler
)(PuppyWrapper);

export const PuppyWrapperContainer = (props) => {
  // State handlers via hooks
  // Any other event handlers
  return <PuppyWrapper {...props} />;
};

Step 2 - United States of Software Development (getting rid of withStateHandlers)

hax.gif

The first recompose function we’re going to get rid of is withStateHandlers. To do this, we’re going to leverage react hooks to provide us with with state functionality without needing to use React class components. That means that we’re going to have to figure out what to do with three methods: withActive, withConfirmAction, and withAddTreatsConfirmation. WithActive

As a reminder, this is the existing code:

const withActive = withStateHandlers(({ active = false }) => ({ active }), {
  toggleActive: ({ active }) => () => ({ active: !active }),
});

We’ve got two things going on here, we’ve got an active local state value that’s a boolean, false by default, and a state handler method toggleActive that sets the current value of active to the opposite value. If we’re looking at replacing this with a hook we’ve got something that looks like:

const [active, setActive] = useState(false);
const toggleActive = () => setActive(!active);

This should provide the same functionality, so let’s toss it into our PuppyWrapperContainer component. After that is in, we also want to ensure that we’re passing that toggleActive method into the PuppyWrapper component as a prop - which leaves us with…

export const PuppyWrapperContainer = (props) => {
  const [active, setActive] = useState(false);
  // other state hooks
  const toggleActive = () => setActive(!active);
  // other event handlers

  return (
    <PuppyWrapper {...props} active={active} toggleActive={toggleActive} />
  );
};

And with that, we can remove anything to do with the existing withActive code, as we’ve now replaced that functionality.

withConfirmAction

The next method to replace is withConfirmAction - let’s take a look:

const withConfirmAction = withStateHandlers(() => ({ confirmOpen: false }), {
  showConfirmation: () => (confirmType) => ({
    confirmOpen: true,
    confirmType,
  }),
  closeConfirmation: () => () => ({ confirmOpen: false }),
});

So we have a new state value confirmOpen as well as event handlers of showConfirmation and closeConfirmation which adjust this state, but also there is a confirmType snuck in there too. Sneaky hobbitses. Let’s look at what we can replace this with.

const [confirmOpen, setConfirmOpen] = useState(false);
const [confirmType, setConfirmType] = useState(null);
const showConfirmation = () => (confirmType) => {
  setConfirmOpen(true);
  setConfirmType(confirmType);
};
const closeConfirmation = () => setConfirmOpen(false);

Ezpz, right? First set the state hook, then set the handler to provide the same functionality the initial handler had. In a lot of cases the handler might not be necessary, though. So just play it by ear. After bringing these changes into our PuppyWrapperContainer we now have something like this:

export const PuppyWrapperContainer = (props) => {
  const [active, setActive] = useState(false);
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [confirmType, setConfirmType] = useState(null);

  // other state hooks
  const toggleActive = () => setActive(!active);
  const showConfirmation = () => (incomingConfirmType) => {
    setConfirmOpen(true);
    setConfirmType(incomingConfirmType);
  };

  const closeConfirmation = () => setConfirmOpen(false);
  // other event handlers
  return (
    <PuppyWrapper
      {...props}
      active={active}
      toggleActive={toggleActive}
      confirmOpen={confirmOpen}
      confirmType={confirmType}
      showConfirmation={showConfirmation}
      closeConfirmation={closeConfirmation}
    />
  );
};

And with that, we can remove anything to do with the existing withConfirmAction method! Even though we’ve only really just started moving stuff around, it’s already more readily understandable exactly what the PuppyWrapper component is going to be receiving as props and what is actually happening. Let’s keep going!

withAddTreatsConfirmation

Here’s what we want to get rid of:

const withAddTreatsConfirmation = withStateHandlers(
  () => ({ addTreatsConfirmationVisible: false }),
  {
    showAddTreatsConfirmation: () => () => ({
      addTreatsConfirmationVisible: true,
    }),
    closeAddTreatsConfirmation: () => () => ({
      addTreatsConfirmationVisible: false,
    }),
  }
);

This is super straight forward! We’ve got a new state value of addTreatsConfirmationVisible which defaults to false, and a couple of handlers showAddTreatsConfirmation and closeAddTreatsConfirmation. Building from our earlier examples, we can refactor withStateHandlers away with something like this:

const [
  addTreatsConfirmationVisible,
  setAddTreatsConfirmationVisible,
] = useState(false);

const showAddTreatsConfirmation = () => () =>
  setAddTreatsConfirmationVisible(true);

const closeAddTreatsConfirmation = () => () =>
  setAddTreatsConfirmationVisible(false);

And we can bring these into our component the same we have the others:

export const PuppyWrapperContainer = (props) => {
  const [active, setActive] = useState(false);
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [confirmType, setConfirmType] = useState(null);
  const [
    addTreatsConfirmationVisible,
    setAddTreatsConfirmationVisible,
  ] = useState(false);

  // other state hooks
  const toggleActive = () => setActive(!active);
  const showConfirmation = () => (incomingConfirmType) => {
    setConfirmOpen(true);
    setConfirmType(incomingConfirmType);
  };
  const closeConfirmation = () => setConfirmOpen(false);
  const showAddTreatsConfirmation = () => () =>
    setAddTreatsConfirmationVisible(true);
  const closeAddTreatsConfirmation = () => () =>
    setAddTreatsConfirmationVisible(false);

  // other event handlers
  return (
    <PuppyWrapper
      {...props}
      active={active}
      toggleActive={toggleActive}
      confirmOpen={confirmOpen}
      confirmType={confirmType}
      showConfirmation={showConfirmation}
      closeConfirmation={closeConfirmation}
      addTreatsConfirmationVisible={addTreatsConfirmationVisible}
      showAddTreatsConfirmation={showAddTreatsConfirmation}
      closeAddTreatsConfirmation={closeAddTreatsConfirmation}
    />
  );
};

And with that, we can not only get rid of everything to do with withAddTreatsConfirmation but also withStateHandlers - bringing us one step closer to our goal: world domination.

Step 3 - If you can’t handle me without compose, you don’t deserve to handle me at all (saying farewell to withHandlers)

pupp.gif

We have two methods that use withHandlers - withOnStatusChange and withConfirmModalHandler prepare to say goodbye:

withOnStatusChange

const withOnStatusChange = withHandlers({
  onStatusChange: ({ puppy, updatePuppyStatus }) => (status) => {
    updatePuppyStatus({ id: puppy.id, status });
  },
});

What’s this doing? It’s just a method that’s getting passed to the PuppyWrapper component that calls a method named updatePuppyStatus. But wait. . . where are puppy and updatePuppyStatus coming from? I don’t see them anywhere in this file outside of this function 😣! It turns out that they’re props being passed in to PuppyWrapperContainer. But we don’t have any props declarations on this component, so we don’t know that as easily. Luckily, as we move from compose, we’ll also get yelled at by our linters if we don’t declare our dependencies. So this turns into a double win! Let’s see what the new method would look like:

const onStatusChange = (status) => {
  updatePuppyStatus({ id: puppy.id, status });
};

Heck, that looks pretty similar, but as is, it’s only a part of the puzzle piece, since this method depends on puppy and updatePuppyStatus from our props. So let’s bring it into our component and make sure those props are surfaced too! (I’m gonna comment out some of the code that hasn’t changed, trust me, it’s still there tho.

export const PuppyWrapperContainer = ({
  puppy,
  updatePuppyStatus,
  ...props
}) => {
  // state hooks
  // other handlers
  const onStatusChange = (status) => {
    updatePuppyStatus({ id: puppy.id, status });
  };

  // other event handlers
  return (
    <PuppyWrapper
      {...props}
      puppy={puppy}
      //other props
    />
  );
};

So a couple things to note here: We now destructure puppy and updatePuppyStatus out of props so that we can use them in our new function. We also are now forced to define this function’s proptypes, so there’s no ambiguity about where this component is getting those values from.

We still pass the puppy into the PuppyWrapper component as it’s a requirement of that component onStatusChange isn’t actually being used yet! That’s because it’s only used in the next chunk of code we’re going to refactor!

withConfirmModalHandler

This method looks and sounds a lot scarier than anything else we’ve faced thus far. But that’s ok! Through our journey thus far, we have levelled up, gotten enough XPs and skill points to just absolutely devastate this component away and vanquish withHandlers from the land. Let’s do some scouting and see what we’re dealing with.

const withConfirmModalHandler = withHandlers({
  onConfirm: ({
    puppy,
    confirmType,
    removePuppy,
    onStatusChange,
    closeConfirmation,
    adjustPuppyBedTime,
    fullName,
    houseId,
    userId,
  }) => ({ newBedTime }) => {
    switch (confirmType) {
      case REMOVE_PUPPY:
        removePuppy(puppy);
        break;
      case ADJUST_PUPPY_END_DATE:
        adjustPuppyBedTime({
          puppyId: puppy.id,
          puppyName: fullName,
          newBedTime: newBedTime.toDate(),
          houseId,
          userId,
        });
        break;
      default:
        onStatusChange(confirmType);
    }
    closeConfirmation();
  },
});

Heck. That’s got some stuff goin on - but it’s actually not such a big deal! The only part we’re actually going to have to adjust is the dependencies part, because they’re not being destructured out of our props yet 😮 First, let’s create our new onConfirm method - we can just copy and the existing method’s body:

const onConfirm = ({ newBedTime }) => {
  switch (confirmType) {
    case REMOVE_PUPPY:
      removePuppy(puppy);
      break;
    case ADJUST_PUPPY_END_DATE:
      adjustPuppyBedTime({
        puppyId: puppy.id,
        puppyName: fullName,
        newBedTime: newBedTime.toDate(),
        houseId,
        userId,
      });
      break;
    default:
      onStatusChange(confirmType);
  }
  closeConfirmation();
};

So the only problem here is that we’ve got some currently undefined values in this method; namely removePuppy, fullName, and adjustPuppyBedTime. They come in as props, so after we bring this in and update our props our component now looks like this:

export const PuppyWrapperContainer = ({
  puppy,
  updatePuppyStatus,
  adjustPuppyBedTime,
  removePuppy,
  fullName,
  ...props
}) => {
  // state hooks
  // event handlers
  const onConfirm = ({ newBedTime }) => {
    switch (confirmType) {
      case REMOVE_PUPPY:
        removePuppy(puppy);
        break;
      case ADJUST_PUPPY_END_DATE:
        adjustPuppyBedTime({
          puppyId: puppy.id,
          puppyName: fullName,
          newBedTime: newBedTime.toDate(),
          houseId,
          userId,
        });
        break;
      default:
        onStatusChange(confirmType);
    }
    closeConfirmation();
  };
  // other event handlers
  return <PuppyWrapper {...props} 
    fullName={fullName} 
    ...allOtherProps
    />;
};

After making that change, we can now remove withHandlers from our imports; leaving us with only two recompose methods left. There are also some other undefined values in this code block: userId, and houseId. If we investigate (🕵️‍♂️) we can see that these are being defined in mapStateToProps so let’s deal with that next.

Step 4 - You gotta gimme props (toodles withProps)

props.gif

The next method we’re going to get rid of is withProps. There is only one method that uses it: mapStateToProps - let’s take a looksee

const mapStateToProps = withProps(({ puppy }) => ({
  userId: puppy.user_id,
  houseId: puppy.parent ? puppy.parent.provider_id : null,
  puppyType: puppy.parent ? puppy.parent.puppy_type : null,
  puppyStatus: puppy.status || puppy.puppy_status,
  puppyBedTime: puppy.puppy_end_date || null,
  userPuppiesView: !!puppy.parent,
}));

There are at least two confusing things here just looking at it: mapStateToProps is a redux connect convention. Does this have anything to do with that? no What is this function even doing - it’s taking the puppy value that gets passed in and it defining some variables based on that that then get passed along to the actual component’s props.

So now that we’ve kinda figured out what the function is doing, we can make some super similar functionality to bring into our refactor. In fact, we can really just use this functionality and put it in a new function that looks something like this:

const getDerivedPuppyData = (puppy) => ({
  userId: puppy.user_id,
  houseId: puppy.parent ? puppy.parent.provider_id : null,
  puppyType: puppy.parent ? puppy.parent.puppy_type : null,
  puppyStatus: puppy.status || puppy.puppy_status,
  puppyBedTime: puppy.puppy_end_date || null,
  userPuppiesView: !!puppy.parent,
});

This function could even live outside of our component and shared with other parts of our application now if we wanted to! But let’s not get ahead of ourselves. Let’s first incorporate this into our component:

const getDerivedPuppyData = (puppy) => ({
  userId: puppy.user_id,
  houseId: puppy.parent ? puppy.parent.provider_id : null,
  puppyType: puppy.parent ? puppy.parent.puppy_type : null,
  puppyStatus: puppy.status || puppy.puppy_status,
  puppyBedTime: puppy.puppy_end_date || null,
  userPuppiesView: !!puppy.parent,
});
export const PuppyWrapperContainer = ({
  puppy,
  updatePuppyStatus,
  adjustPuppyBedTime,
  removePuppy,
  ...props
}) => {
  // state hooks
  // event handlers
  const derivedPuppyData = getDerivedPuppyData(puppy);
  return (
    <PuppyWrapper
      {...props}
      {...derivedPuppyData}
      // all other props from state and handlers
    />
  );
};

Here we can see that the userId and houseId variables that were currently undefined are now bundled up in derivedPuppyData, so we can also update our onConfirm method to use those values.

const onConfirm = ({ newBedTime }) => {
  switch (confirmType) {
    case REMOVE_PUPPY:
      removePuppy(puppy);
      break;
    case ADJUST_PUPPY_END_DATE:
      adjustPuppyBedTime({
        puppyId: puppy.id,
        puppyName: fullName,
        newBedTime: newBedTime.toDate(),
        houseId: derivedPuppyData.houseId,
        userId: derivedPuppyData.userId,
      });
      break;
    default:
      onStatusChange(confirmType);
  }
  closeConfirmation();
};

All we changed here was used derivedPuppyData for those two values. And with that change, we have negated the need for with props, which really only leaves compose.

Step 5 - Don’t lose your composure, actually, please do (getting rid of compose)

byefelicia.gif

If we take a look at the original method that we wanted to get rid of:

export const PuppyWrapperContainerOld = compose(
  stateToProps,
  mapStateToProps,
  withOnStatusChange,
  withAddTreatsConfirmation,
  withConfirmModalHandler
)(PuppyWrapper);

We have dealt with everything that was being used in compose except for one method: stateToProps. Let’s remind ourselves what that looks like:

const stateToProps = connect((state, { puppy }) => ({
  AdoptionDate: puppy.parent
    ? selectPolicyBedTime(puppy.parent.id)(state)
    : null,
}));

Neat! This is basically just a connect invocation. It may look a little different than a plain old usage, but it’s still just a connect usage, so we should be able to just use this connect call and wrap it around out PuppyWrapperContainer like this:

export const PuppyWrapperContainer = connect((state, { puppy }) => ({
  AdoptionDate: puppy.parent
    ? selectPolicyBedTime(puppy.parent.id)(state)
    : null,
}))(
  ({
    puppy,
    updatePuppyStatus,
    adjustPuppyBedTime,
    removePuppy,
    fullName,
    ...props
  }) => {
    // nothing changed in here
  }
);

Following that, we can now get rid of compose from this file entirely 😍

Step 6 - Mission Complete (but not necessarily complete)

done.gif

Now our app is working as expected, and we’ve gotten rid of compose, that’s exactly what we wanted! let’s check out the code in it’s entirety.

import React, { useState } from "react";
import PropTypes from "prop-types";
import { PuppyWrapper } from "./puppy-wrapper.component";
import { selectPolicyBedTime } from "./puppys.selectors";
import { connect } from "react-redux";
import { REMOVE_PUPPY, ADJUST_PUPPY_END_DATE } from "./constants";
const getDerivedPuppyData = (puppy) => ({
  userId: puppy.user_id,
  houseId: puppy.parent ? puppy.parent.provider_id : null,
  puppyType: puppy.parent ? puppy.parent.puppy_type : null,
  puppyStatus: puppy.status || puppy.puppy_status,
  puppyBedTime: puppy.puppy_end_date || null,
  userPuppiesView: !!puppy.parent,
});
export const PuppyWrapperContainer = connect((state, { puppy }) => ({
  AdoptionDate: puppy.parent
    ? selectPolicyBedTime(puppy.parent.id)(state)
    : null,
}))(
  ({
    puppy,
    updatePuppyStatus,
    adjustPuppyBedTime,
    removePuppy,
    fullName,
    ...props
  }) => {
    const [active, setActive] = useState(false);
    const [confirmOpen, setConfirmOpen] = useState(false);
    const [confirmType, setConfirmType] = useState(null);
    const [
      addTreatsConfirmationVisible,
      setAddTreatsConfirmationVisible,
    ] = useState(false);
    const derivedPuppyData = getDerivedPuppyData(puppy);
    // other state hooks
    const toggleActive = () => setActive(!active);
    const showConfirmation = () => (incomingConfirmType) => {
      setConfirmOpen(true);
      setConfirmType(incomingConfirmType);
    };
    const closeConfirmation = () => setConfirmOpen(false);
    const showAddTreatsConfirmation = () => () =>
      setAddTreatsConfirmationVisible(true);
    const closeAddTreatsConfirmation = () => () =>
      setAddTreatsConfirmationVisible(false);
    const onStatusChange = (status) => {
      updatePuppyStatus({ id: puppy.id, status });
    };
    const onConfirm = ({ newBedTime }) => {
      switch (confirmType) {
        case REMOVE_PUPPY:
          removePuppy(puppy);
          break;
        case ADJUST_PUPPY_END_DATE:
          adjustPuppyBedTime({
            puppyId: puppy.id,
            puppyName: fullName,
            newBedTime: newBedTime.toDate(),
            houseId: derivedPuppyData.houseId,
            userId: derivedPuppyData.userId,
          });
          break;
        default:
          onStatusChange(confirmType);
      }
      closeConfirmation();
    };
    return (
      <PuppyWrapper
        {...props}
        {...derivedPuppyData}
        puppy={puppy}
        active={active}
        toggleActive={toggleActive}
        confirmOpen={confirmOpen}
        confirmType={confirmType}
        showConfirmation={showConfirmation}
        closeConfirmation={closeConfirmation}
        addTreatsConfirmationVisible={addTreatsConfirmationVisible}
        showAddTreatsConfirmation={showAddTreatsConfirmation}
        closeAddTreatsConfirmation={closeAddTreatsConfirmation}
        onConfirm={onConfirm}
        fullName={fullName}
      />
    );
  }
);

With a quick look, there are definitely some areas we can clean up, let’s try that.

Removing unnecessary event handlers

If we check out the code above, we can see that some of our event handlers are simple enough that we probably don’t need to define their own method, instead we can define them on the component if we want. Take a look at toggleActive, toggleActive, showAddTreatsConfirmation, and closeAddTreatsConfirmation.

const toggleActive = () => setActive(!active);

const closeConfirmation = () => setConfirmOpen(false);

const showAddTreatsConfirmation = () => () =>
  setAddTreatsConfirmationVisible(true);

const closeAddTreatsConfirmation = () => () =>
  setAddTreatsConfirmationVisible(false);

All that these handlers are doing is just calling one of our already existing setState methods that we get from our useState hooks. So we can just get rid of all these and call those setState methods where we want them - resulting in something like:

export const PuppyWrapperContainer = connect((state, { puppy }) => ({
  AdoptionDate: puppy.parent
    ? selectPolicyBedTime(puppy.parent.id)(state)
    : null,
}))(
  ({
    puppy,
    updatePuppyStatus,
    adjustPuppyBedTime,
    removePuppy,
    fullName,
    ...props
  }) => {
    const [active, setActive] = useState(false);
    const [confirmOpen, setConfirmOpen] = useState(false);
    const [confirmType, setConfirmType] = useState(null);
    const [
      addTreatsConfirmationVisible,
      setAddTreatsConfirmationVisible,
    ] = useState(false);

    const derivedPuppyData = getDerivedPuppyData(puppy);

    const showConfirmation = () => (incomingConfirmType) => {
      setConfirmOpen(true);
      setConfirmType(incomingConfirmType);
    };

    const onStatusChange = (status) => {
      updatePuppyStatus({ id: puppy.id, status });
    };

    const onConfirm = ({ newBedTime }) => {
      switch (confirmType) {
        case REMOVE_PUPPY:
          removePuppy(puppy);
          break;

        case ADJUST_PUPPY_END_DATE:
          adjustPuppyBedTime({
            puppyId: puppy.id,
            puppyName: fullName,
            newBedTime: newBedTime.toDate(),
            houseId: derivedPuppyData.houseId,
            userId: derivedPuppyData.userId,
          });
          break;

        default:
          onStatusChange(confirmType);
      }

      setConfirmOpen(false);
    };
    return (
      <PuppyWrapper
        {...props}
        {...derivedPuppyData}
        puppy={puppy}
        active={active}
        toggleActive={() => setActive(!active)}
        confirmOpen={confirmOpen}
        confirmType={confirmType}
        showConfirmation={showConfirmation}
        closeConfirmation={() => setConfirmOpen(false)}
        addTreatsConfirmationVisible={addTreatsConfirmationVisible}
        showAddTreatsConfirmation={() => () =>
          setAddTreatsConfirmationVisible(true)}
        closeAddTreatsConfirmation={() => () =>
          setAddTreatsConfirmationVisible(false)}
        onConfirm={onConfirm}
        fullName={fullName}
      />
    );
  }
);

Which is just a bit tidier and less repetitive. You don’t necessarily have to do this, but you can!

Preventing unnecessary calculations (memoizing)

The main other thing that sticks out to me in here is that the getDerivedPuppyData method is going to get called every time this component needs to render, even if the puppy hasn’t changed. We can remedy this pretty simply by bringing in react’s useMemo function. It’ll look something like this:

const derivedPuppyData = useMemo(() => getDerivedPuppyData(puppy), [puppy]);

And that’s it!

Step 7 - Celebrate 💃 dance.gif

Golly, I really hope that you took a break somewhere in the midst of all of this!

This post is already a billion pages long, so I’m just gonna leave it there 👋