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>
);
};
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>
);
};
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>
);
};
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>
);
};
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
- 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 correct typos.
Top comments (2)
Minor helpful tip
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 likevisible
toBanner
? That seems more clear in that case.Anyways, this is good. Thanks for your post :)