Refactoring React: Applying Tell Don't Ask
Tags: react, refactoring, codequality, programming
Published: 2022-04-28 01:42:39

Often times we need to conditionally do something. In React land, it is mostly related to rendering stuff. Like conditionally render a component based on some state or prop.

When faced with those kinds of problems, we can use Tell Don't Ask to improve the code readability.

## What is "Tell Don't Ask"?

Related with Law of Demeter (but not the same thing), Tell Don't Ask is an object-oriented programming technique (or design principle) where we avoid asking the object about its internal state to tell that object to do something. Instead, we just tell the object and let it rely on its internal state to decide what to do.

Applying Tell Don't Ask, we avoid querying and depending on the internal state of a collaborator object. Instead, the owner of that state -- or behavior, should decide what to do.

## Working example

We have a settings page, represented by the `SettingsPage` component. This page uses many components, need to deal with state revalidation, form submissions and other things related with the settings page of the application.

This is the code (cropped, and many things omitted) of `SettingsPage` component:

```jsx
const SettingsPage = () => {
  const settings = useSettings();

  return (
    <article>
      {!settings.isEmailConfirmed && (
        <Banner settings={settings} />
      )}
    </article>
  );
};
```

The `Banner` component should display a meaningful message based on the current settings state, alerting the user that it needs to confirm the email.

The Tell Don't Ask violation here is that `SettingsPage` is conditionally rendering the `Banner` component. But why this is a problem?

To be clear, in this toy example it is easy to spot what is happening, but rendering or not is a business rule own by the warning banner, not by the settings page.

The role that settings page here is to bring all of its parts together. Each part should have its own role and work together with other components mounted in the same context.

But imagine in a larger application with lots and lots of pages, where each page need to mount components and deal with the communication between them. Quickly become a mess where no one wants to maintain.

### Applying the refactoring

The first step is to incorporate the business rule into the banner component, like so:

```jsx
const Banner = ({ settings }) => {
  if (!settings.isEmailConfirmed)
    return null;

  return (
    <section>
      <p>Bla bla bla</p>
    </section>
  );
};
```

Now we can run our tests, if we are green, we can proceed and then remove the conditional rendering at the parent component -- the settings page.

```jsx
const SettingsPage = () => {
  const settings = useSettings();

  return (
    <article>
      <Banner settings={settings} />
    </article>
  );
};
```

Now, the `SettingsPage` component doesn't know how the banner will deal with the settings. If the banner needed to show a different message based on a different settings property, it can do that without the settings page asking something.

We can proceed and remove the `useSettings` call and incorporate it to the `Banner` component, but personally see this movement as adding too much complexity into the banner component.

## I'm using a shared component! I cannot apply this rule

Yes, you are right. You cannot.

But you can create an abstraction layer bounded into your context. If `Banner` component is using a shared banner element, maybe from an external library. Either way, it's from `Banner` component business to decide what to use to complete its work.

If our application already had a `Banner` component that is shared and agnostic by context, we can create an `SettingsBanner` component.

Better than that, we can talk to our users and ask them about that banner. How do they talk about this banner? Which words they use? Maybe they call by "confirmation email warning". If so, we can create a component bounded inside the settings context called `ConfirmationEmailWarning` and then implement the business rules owned by this component.

```jsx
const ConfirmationEmailWarning = ({ settings }) => {
  if (!settings.isEmailConfirmed) return null;

  return (
    <Banner>
      Bla bla bla
    </Banner>
  );
};
```

## Conclusion

By encapsulating business rules inside components and hooks, we can compose them based on contexts. A little coupling behind a domain context is not a big deal, coupling between domains is a problem.

Tell Don't Ask help us to keep the logic behind a door. We should not ask whether we can do or not do something, we just try to do that. In React land, it applies to rendering components, using React hooks and so on.

## Learn more

- [Steve Freeman and Nat Pryce, Growing Object-Oriented Software Guided by Tests](http://www.growing-object-oriented-software.com/)
- [David Thomas and Andrew Hunt, The Pragmatic Programmer](https://pragprog.com/titles/tpp20/the-pragmatic-programmer-20th-anniversary-edition/)
- [Martin Fowler, Tell Don't Ask](https://martinfowler.com/bliki/TellDontAsk.html)
- [Ben Orenstein, Tell Don't Ask](https://thoughtbot.com/blog/tell-dont-ask)

## Updates

- *2022, April 28 - Added more sources and correct typos*.