What if I pass a state setter as a prop?

Ismael Ramon
SEAT CODE
Published in
5 min readDec 18, 2023
Photo by Lautaro Andreani on Unsplash

While working with React it is possible that, under the ins and outs of some project, you have encountered components that receive some prop like this:

<SomeInputField setValue={setValue} /> // 🤔

If you’ve got enough experience in React you may avoid this without even thinking about it. Nevertheless, how good is it to be able to explain it so all of the team is on the same page.

If otherwise you’ve been there for a short time or you’ve already caught the habit, you may be thinking it’s not that bad.

Well, I’ve been there too. Let’s talk about this.

What’s the problem

It may be true there are small cases where it doesn’t seem like a big deal. But please, take a look at this other example:

<SomeComponent
setForm={setForm}
setCart={setCart}
setUser={setUser}
setMeFree={setMeFree}
/>

Now consider these basic questions:

  • When does each state change?
  • What or who is causing the change?
  • How do they change exactly?
  • Where’s the responsible piece of code?

As you can see, the code is not giving us enough information to answer any of the questions. Basically, we know nothing about these states.

But I only have to take a look to the code from SomeComponent, right? — Someone will say

Then let’s go in:

// Somewhere within SomeComponent.jsx
<YetAnotherComponent
setForm={setForm}
setCart={setCart}
setUser={setUser}
setMeFree={setMeFree}
/>

Oh my! Frustration starts. A loop in the purest Inception style, and we still have no answers.

What’s more, the setters will be passed on to other sibling components and will keep being spread through props to more and more children in the hierarchy, multiplying the number of code files to review.

You don’t want to go through 15 files to understand a state

Do you see what the problem is? The state is a key element within the app, and it’s hard to know what it does like this!

How to escape this

Let’s go back to the questions: when does state change? how? where?… Wouldn’t it be nice to be able to answer without having to review other files?

Pay attention, because there is a way and it is standard. And if a standard exists it is because it has advantages over other methods.

Following standards usually saves a lot of trouble.

Look at the native code

There is definitely no standard setter named prop.

An input does not receive setValue

Have you ever seen it in native components? UI kits? Popular libs? Nothing.

What you will have seen is this:

<input onChange={...} /> // 👀

That’s right, a native input does not offer a setValue prop, but rather an onChange prop.

This event prop expects a function, where typically the contents of the event argument are used to perform a state update:

<input onChange={
event => setValue(event.target.value)
} />

Think about all of the valuable information we’ve got here:

  • The name of the prop is giving us an idea of when it does happen
  • It is easy to imagine the responsible action for the change
  • How the state is changed can be clearly seen
  • All relevant code is visible there

Isn’t it awesome?

The key: division of responsibility

Let’s point out why this works so well.

There is division of responsibility

When there is division of responsibility, each piece of the system has a well-defined and delimited purpose. It can often be understood on its own without reviewing other pieces.

This is important in this profession, because the less time it takes you to understand, the sooner you can start making changes.

Continuing with the division of responsibility: the native input does not want to know anything about how your state is changed, that is your parent component’s problem.

The input only tells you about what it controls thanks to the onChange event prop:

  • When a change occurs
  • The details of that change

So, instead of sending a state setter as a prop, it’s time to imitate the native pattern: event props.

Do it in your components

The state and its values are the sole responsibility of the component that runs the useState. For example, if you had a component that manages a list:

// List.jsx
const [list, setList] = useState([])

Let’s say we are going to have a child Item with the ability to delete itself from the list.

The List parent is responsible for performing each setList in its own code. It will not delegate that responsibility to Item:

// Also in List.jsx
const handleRemoveItem = id => setList(list.filter(...))
return <Item onRemove={handleRemoveItem} />

Item should not know anything about the parent’s entire list or how it will change, but two things are its responsibility:

  • When to delete: because it will have a button to do so
  • Which one has to be deleted: because it will know its own id

As we have already seen, instead of receiving setList directly we are going to expose an event prop that allows the parent to be aware of these things:

function Item ({ onRemove }) {
const myId = 27
return <button onClick={() => onRemove(myId)}>❌</button>
}

But Item will never decide what to do with a state that is not its own, and in fact from its code it will not have explicit access to any foreign setter.

That’s it, each component has its responsibility and none invades that of the other. Let’s look at the full List code one last time:

// ✅
function List () {
const [list, setList] = useState([])
const handleRemoveItem = id => setList(list.filter(...))
return <Item onRemove={handleRemoveItem} />
}
  • When does the state change? — When the “delete” action happens
  • What or who makes it change? — The user who interacts with Item
  • How exactly does it change? — Through a .filter(), can be seen
  • Where is the responsible code for it? — Within the same file

But, just to taste the difference once again:

// ❌
function List () {
const [list, setList] = useState([])
return <Item setList={setList} />
}

In contrast, here we would no longer have any idea when, where, how or what is being done with the state.

Conclusion

If we imitate the native model we receive event props, not state setters

❌ Passing state setters as props obscures the state logic by fragmenting it into different files, resulting in difficulties to understand it.

const [value, setValue] = useState('')
<SomeInputField setValue={setValue} /> // ???

✅ The native event props standard helps to correctly delimit the responsibility of the state and its changes together with the code where the state is declared.

const [value, setValue] = useState('')
<AwesomeInputField onChange={next => setValue(next)} />

--

--