Often, we need to conditionally do something. In React land, this is mostly related to rendering stuff, like rendering a component based on some state or prop.
When faced with those kinds of problems, we can use Tell Don’t Ask to improve code readability.
What is “Tell Don’t Ask”?
Related to the 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 an 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.
By 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 and needs to deal with state revalidation, form submissions, and other things related to the settings page of the application.
This is the code (cropped, and many things omitted) of the SettingsPage component:
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 they need to confirm their email.
The Tell Don’t Ask violation here is that SettingsPage is conditionally rendering the Banner component. But why is this a problem?
To be clear, in this toy example, it is easy to spot what is happening, but whether to render or not is a business rule owned by the warning banner, not by the settings page.
The role of the 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.
Imagine a larger application with lots and lots of pages, where each page needs to mount components and deal with the communication between them. It quickly becomes a mess that 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>
);
};
Now we can run our tests; if they 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>
);
};
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 could do that without the settings page asking anything.
We could proceed and remove the useSettings call and incorporate it into the Banner component, but personally, I see this move as adding too much complexity to 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 to your context. Even if the Banner component is using a shared banner element, perhaps from an external library, it is still the Banner component’s business to decide what to use to complete its work.
If our application already had a context-agnostic Banner component that is shared, we could create a SettingsBanner component.
Better yet, we can talk to our users and ask them about that banner. How do they talk about it? Which words do they use? Maybe they call it a “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>
);
};
Conclusion
By encapsulating business rules inside components and hooks, we can compose them based on contexts. A little coupling within a domain context is not a big deal; coupling between domains is a problem.
Tell Don’t Ask helps us keep the logic behind a door. We should not ask whether we can or cannot do something; we just try to do it. In React land, this applies to rendering components, using React hooks, and so on.
Learn more
- Steve Freeman and Nat Pryce, Growing Object-Oriented Software Guided by Tests
- David Thomas and Andrew Hunt, The Pragmatic Programmer
- Martin Fowler, Tell Don’t Ask
- Ben Orenstein, Tell Don’t Ask
Updates
- 2022, April 28 - Added more sources and corrected typos.