Principle of Least Privilege
Video Summary
Let's talk about one more thing about the “shopping list” example from Module 2:
In my solution, I created a handleAddItem
function, and passed it down to the child component:
function App() { const [items, setItems] = React.useState([]);
function handleAddItem(label) { const newItem = { label, id: Math.random(), };
const nextItems = [...items, newItem]; setItems(nextItems); }
return ( <div className="wrapper"> <div className="list-wrapper"> ... </div> <AddNewItemForm handleAddItem={handleAddItem} /> </div> );}
Many students have wondered why I've structured things this way. Wouldn't it be simpler to pass the state-setter function directly?
function App() { const [items, setItems] = React.useState([]);
return ( <div className="wrapper"> <div className="list-wrapper"> ... </div> <AddNewItemForm setItems={setItems} /> </div> );}
This approach works, but there's a reason that I structured things the way I did. It has to do with one of the most important mental models I've learned, when it comes to working with React: the principle of least privilege.
This term comes from the security field, and it has to do with reducing the amount of access/authorization every member of an organization has.
Let's suppose I work as a bank teller. For me to do my job, I need the authorization to do things like open accounts for new customers, or handle deposits. But I probably shouldn't be able to issue mortgages, or buy/sell stock options. Those things fall outside the scope of my role, and so the computer system shouldn't authorize me to perform those actions.
Now, let's imagine that every component in our app is a member of our organization.
The AddNewItemForm
component has one job: it needs to be able to push a new item to the end of the current list of items.
When we give it a state-setter function, we grant it so much more power than that. For example, it can erase all of the current items:
// Erases all previously-saved items:setItems([]);
Or, it can break the app altogether, by setting it to something other than an array:
setItems(5);
By contrast, in my original solution, the AddNewItemForm
component can't do any of that stuff. The only thing it can do is push a new item into the array:
function AddNewItemForm({ handleAddItem }) { const [label, setLabel] = React.useState('');
return ( <div className="new-list-item-form"> <form onSubmit={(event) => { event.preventDefault();
handleAddItem(label);
setLabel('') }} > ... </form> </div> );}
This component can still break things (eg. by calling handleAddItem
with a random number instead of label
), but it has far less power. It can't cause as many problems.
Now, you might think this whole concept sounds a bit strange. Components aren't sentient! Either way, it's us developers who are deciding what to pass to setItems
. We wouldn't maliciously pass it a number! Why does it matter where we call setItems
?
This is one of those things that really only makes sense at scale. This example has less than 150 lines of code in the entire application. But what if we were working on a codebase with hundreds of thousands of lines of code? If we were 1 of 50 developers on the project?
In real-world production settings like this, we're often tossed into corners of the codebase that we aren't familiar with at all. You might be tasked with tweaking the AddNewItemForm
component without knowing anything about how it works!
In those types of situations, it's so so easy to introduce subtle bugs. And so, the less privilege we give to our components, the less problems we'll have down the road.
Here's the original solution, with the handleAddItems
handler function:
My Original Solution
And here's the not-recommended alternative, where we pass the state-setter function directly:
Passing the setter function