In the official React documentation, there’s an example where the setState
function is passed down to child components:
import { useState } from 'react';
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly} />
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly} />
</div>
);
}
This approach seems to go against the principle of encapsulation because it allows child components to directly manipulate the parent’s state.
Usually, I create a handler function and pass it down to the child instead:
const handleChangeFilterText = (val) => {
setFilterText(val);
};
<SearchBar onFilterTextChange={handleChangeFilterText} />;
I can also wrap the handler with useCallback
if the child is wrapped with React.memo
.
I’ve always thought that passing setState
directly to children is a bad practice, but seeing it in the React documentation has made me question this belief.
I’d appreciate insights from the community and React developers: Is this a recommended practice, or should it generally be avoided?
2
Answers
Component props are the same as function parameters in JS. Pass a callback function parameter is common pattern in JS. It does not go against the principle of encapsulation.
You can also think of component props as public APIs
setState
function is stable, you don’t have to useuseCallback
create another stable function.See Caveats
React component can be stateful or stateless.
Encapsulated components include: view, view logic, data (state, constants, enum), business logic.
Another component don’t know the implementation details of the encapsulated component. Can only use the encapsulated component via its props(public API).
When you pass a function to another unit, the receiver doesn’t know what function you are giving it. Generally the whole point of having a function parameter is to decouple the receiver from the particular function it calls, so that the receiver can (at least potentially) be called at different times with different functions; if the receiver is always calling a single statically known function through that parameter and knows it (i.e. its code is written with the assumption of which function it will be receiving), then it might as well be calling that function directly, rather than going through the bother of receiving it as an argument.
So if you were writing a component like the search bar and you give it a function parameter that you think of as "the parent component should pass its
setFilterText
function here so my search bar can update the parent’s text whenever it wants", that’s a bad design. It will likely lead to you coupling your child component strongly to the particular parent you’re thinking of it being embedded in, without even realising it. Things like not bothering to call thesetFilterText
function in some situations because you know that the parent product table is about to reset just after this; it seems easier and more efficient in this particular context, but decisions like that hinder you using the search bar as a general search bar component, rather than as the specific search bar for this one particular application1.But on the other hand you could write the search bar component with a function parameter where you think of it as "whenever the search bar’s text is changed I will call this function; my parents can use this to get notified but I will make very few assumptions about what they’re doing with that notification". Note that the technical details of the interface have come out almost exactly the same, but the way you think about it is different (which in real projects frequently leads to different code being written to implement the interface).
Basically the idea is not that you should never ever pass
setState
functions to child components, but child components should not be written with the assumption that that is what they’re getting. You should implement the child component as if the function parameter could be any function at all. Okay, "could be literally any function at all" is usually an impractical ideal. There will most likely be some assumptions about it: obviously things like what types it can be called with and what it should return, more subtly things like "it should return very quickly" or "it shouldn’t have any side effects that trigger this set of events", etc. These assumptions that the child implementation makes are effectively part of its interface, and would ideally be documented. But the child should be written to accept a very wide class of possible functions. If a particular parent happens to have asetState
function that meets the child component’s requirements for its callback parameter then it can *choose to pass that, but the child still shouldn’t know that that is what it is receiving.Wrapping the
setState
in a trivial function likehandleChangeFilterText
doesn’t change anything. If the child has been inappropriately coupled to this particular usage context, the trivial wrapper doesn’t fix the problem; you’ll still have all the same possibility of problems trying to reuse the component in a different context (or even just with updating the parent’s behaviour in future requiring synchronised changes in the child). And if the child component has been coded as a truly independent component, then passing itsetState
instead of a wrapper likehandleChangeFilterText
won’t make it any more coupled to this particular parent; it’s still going to be treating thesetState
function as a black box that it’s just calling whenever the search text is changed. (If you like, it doesn’t know that it can update its parent’s state, even though the parent has decided that’s what should happen when the child sends a notification about the text changing).1 It’s often even worse than this. Because you’ve gone through the motions of passing in knowledge of the parent as a parameter rather than hard-coding it, the search bar component can look like a general component and therefore be inappropriately reused elsewhere, causing bugs.
Soapbox appendix: This sort of issue is why I don’t like "best practice" rules all that much, though they have their uses. Almost always once you boil down what is actually "best practice" to some easy to teach and follow rules, focusing on the rules leads to things like making
handleChangeFilterText
wrappers. It conforms to the letter of a rule like "avoid passingsetState
functions to child components", but once you think about it in more depth you realise that it does nothing to achieve the goal of such a rule (which is to avoid inappropriate coupling between child components and their usage in particular parents). If you know the rules but lack a deep understanding of the principles behind them, it’s hard to notice when you’re doing this. Whereas if you understand the issues and the general principles you will tend to do reasonably right things without ever being taught the best practice rules.It is of course impractical to immediately learn all the principles and fiddly details that people try to boil down to "recommended practices", when you’re approaching a new skill or technology. So often it is a good idea to listen to those recommendations. But if they take the form of a simple rule like "don’t pass
setState
functions to child components" be wary that you probably need to understand why someone would state such a rule in order to apply the rule well, so over the long term you should aim to dig deeper rather than simply internalise it as a rule. (By asking questions such as this one!)