Single Source of Truth
Earlier in this course, when we talked about working with forms, we saw how a form input could either be controlled or uncontrolled.
A controlled input is bound to a piece of React state:
function App() { const [name, setName] = React.useState('');
return ( <> <label htmlFor="name-input"> Name: </label> <input id="name-input" value={name} onChange={(event) => { setName(event); }} /> </> );}
By contrast, an uncontrolled input is free to do its own thing. We might specify an initial value, but we won't control it with React state:
function App() { return ( <> <label htmlFor="name-input"> Name: </label> <input id="name-input" defaultValue="Enzo Matrix" /> </> );}
When we think about it, what we're really doing here is setting the “source of truth” for this input:
- Controlled inputs use React state as the source of truth.
- Uncontrolled inputs use the internal DOM state as the source of truth.
I'm not a browser engineer, so I don't know exactly how it works, but form controls like <input>
must have some sort of internal state, managed by the browser, to track what the user has typed in.
A few years ago, my mind was blown by a realization: we can apply this same controlled/uncontrolled idea to the components that we create!
Let's consider the Counter
component we saw earlier:
function Counter() { const [count, setCount] = React.useState(0);
return ( <button onClick={() => setCount(count + 1)}> {count} </button> );}
We then render this component without any props, like this:
function App() { return ( <Counter /> );}
From the consumer perspective, this element is totally self-contained. It has its own internal state, and I can't access it. It's like an uncontrolled input!
Now, suppose we re-write this code:
function App() { const [count, setCount] = React.useState(0);
return ( <Counter count={count} onIncrement={() => setCount(count + 1)} /> );}
function Counter({ count, onIncrement }) { return ( <button onClick={onIncrement}> {count} </button> );}
In this new version, Counter
is controlled by the consumer. The consumer owns the state, and we're effectively data-binding this Counter
element to our state variable. Like a controlled text input!
Now, it's not exactly the same, since either way, we're using a React state variable as the source of truth, not the DOM. But from the perspective of the consumer, pretending that Counter
is a black box, it's remarkably similar!
Which approach is better? Well, it depends on the circumstances! If the consumer needs to access / change the state, a controlled component makes that possible. Otherwise, it's more convenient to go with uncontrolled components.
The most important thing is that there should always be a single source of truth. We get into trouble when we start treating a component as both controlled and uncontrolled.
Spend a couple minutes considering this setup. Can you make sense of what's going on here?
Code Playground
Let's discuss:
Video Summary
This is the same Thermostat we saw earlier, with two differences:
- We've removed the Celsius/Fahrenheit toggle (it's not relevant to what we're talking about today)
- We've added a “Reset Temperature” control outside the
Thermostat
component
This code is not super well-structured. Let's talk about what the problems are, and how to fix them.
Also, let's suppose that we already had a working, self-contained Thermostat
component when the product/design team told us we needed to add an external button that can reset it to a pre-defined initial value, 25°.
The simplest thing might be to move the <button>
into the Thermostat
component, so that everything is managed internally, but let's suppose that we can't do that. The <button>
is supposed to represent some sort of external control; in a real app, it might be in an entirely separate part of the page (eg. the header, a settings modal, …).
The problem is that we have two sources of truth for the temperature. We have the value
state variable in App
, and the temperature
state variable in Thermostat
. They both track the same “thing”, the current temperature.
To keep everything working, we need to make sure that these two variables are kept in sync:
- We need an effect in the child
Thermostat
component that syncstemperature
tovalue
when it changes (when the “Reset Temperature” button is clicked) - When we increment/decrement the
temperature
, we pass the new value upwards, so thatvalue
is also updated
This adds significant complexity to our code, and it also makes it more bug-prone!
Suppose we comment out the onChange
call:
function incrementTemperature() { const nextTemperature = temperature + 1; setTemperature(nextTemperature);
// onChange(nextTemperature);}
This breaks the app in a very peculiar way: now, the “Reset Temperature” button doesn't work!
(Please see the video for a breakdown on why commenting out onChange
breaks the “Reset Temperature” button, it's easier to explain in video format.)
React doesn't expect us to “sync” multiple state variables together like this. And we run into lots of problems when we try and do this.
Alright, so what's the solution? We'll need to remove one of the state variables, either value
or temperature
. And because we need to know the temperature in App
, for the “Reset Temperature” button, we'll keep value
.
Here's how this affects the Temperature
component:
function Thermostat({ value, onChange }) {- const [temperature, setTemperature] = React.useState(value);- // Sync the `temperature` state variable- // with the `value` prop:- React.useEffect(() => {- setTemperature(value);- }, [value])
function incrementTemperature() {+ const nextTemperature = value + 1;- const nextTemperature = temperature + 1;- setTemperature(nextTemperature); onChange(nextTemperature); } function decrementTemperature() {+ const nextTemperature = value - 1;- const nextTemperature = temperature - 1;- setTemperature(nextTemperature); onChange(nextTemperature); } return ( <div className={styles.wrapper}> <div className={styles.logo}> Sugarfine </div> <div className={styles.digitalScreen}>+ {value}°- {temperature}° </div> <div className={styles.controls}> <div className={styles.tempAdjustButtons}> <button className={styles.iconButton} onClick={decrementTemperature} > <ChevronDown size={32} /> <VisuallyHidden> Decrease temperature </VisuallyHidden> </button> <button className={styles.iconButton} onClick={incrementTemperature} > <ChevronUp size={32} /> <VisuallyHidden> Increase temperature </VisuallyHidden> </button> </div> </div> </div> );}
export default Thermostat;
Quite a lot less code!
Essentially, the decision here is whether Temperature
should be a controlled or uncontrolled component. Controlled components get their source of truth from the parent, while uncontrolled components manage their own internal state.
The components we write should always be either controlled or uncontrolled, and we should decide based on whether it's possible for them to be entirely self-contained or not.
Here's the final code from the video:
Further reading
The official React docs have a blog post, You Probably Don’t Need Derived State. It touches on a lot of these ideas!
Unfortunately, this post is from 2018, and so all of the examples use class-based components. But I think the core ideas are still just as relevant today, and hopefully it's relatively clear!