DEV Community

Gustavo Santos
Gustavo Santos

Posted on

Refactoring React: Applying Tell Don't Ask

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:

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

  return (
    <article>
      {!settings.isEmailConfirmed && (
        <Banner settings={settings} />
      )}
    </article>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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

  return (
    <section>
      <p>Bla bla bla</p>
    </section>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

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

  return (
    <article>
      <Banner settings={settings} />
    </article>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

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

  return (
    <Banner>
      Bla bla bla
    </Banner>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

Updates

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

Top comments (2)

Collapse
 
buingoctai profile image
TaiBui

Minor helpful tip

Collapse
 
lico profile image
SeongKuk Han • Edited

I think when someone sees the usage of Banner, they don't know how the component works. If that's used just for visibility, how about adding a property like visible to Banner? That seems more clear in that case.


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.

Anyways, this is good. Thanks for your post :)