Resetting the Small App
Our lesson planner currently stores three values in state.
const [query, setQuery] = useState("");
const [showCompleted, setShowCompleted] = useState(true);
const [weeklyGoal, setWeeklyGoal] = useState("3");Everything else we show on screen comes from these three values. The filtered lessons, how many lessons are visible, whether the weekly goal is valid, whatever labels we're showing... all of that can be calculated again during render.
So if we add a Reset button, what should it actually reset?
These three values. That's pretty much it.
We don't need to manually reset the filtered list, reset the count, reset some "is goal valid" variable and then somehow remember every other value which depends on these three. We just put the actual stored state back to its initial values and let the next render calculate everything again.
That's the reset we want.
Resetting State With the Setters We Already Have
First, let's stop repeating the initial values in random places.
const initialQuery = "";
const initialShowCompleted = true;
const initialWeeklyGoal = "3";Then use those same values when creating state.
const [query, setQuery] = useState(initialQuery);
const [showCompleted, setShowCompleted] = useState(initialShowCompleted);
const [weeklyGoal, setWeeklyGoal] = useState(initialWeeklyGoal);And now resetting is pretty boring code, which is good.
function handleReset() {
setQuery(initialQuery);
setShowCompleted(initialShowCompleted);
setWeeklyGoal(initialWeeklyGoal);
}Then connect it to the button.
<button type="button" onClick={handleReset}>
Reset
</button>When the user clicks it, React queues all three state updates from the same event and then renders the planner using the new values.
Notice what we did not reset manually. We didn't touch visibleLessons, some visible count, or any other value we can calculate from state. Those will already get recalculated when the component renders again.
Reset the state you actually store. If something can be calculated from that state, let the next render calculate it again.
Keep the Starting Value and Reset Value Together
There's a very easy little bug you can create if you start copying literal values around.
const [weeklyGoal, setWeeklyGoal] = useState("3");
function handleReset() {
setWeeklyGoal("5");
}Now the app starts with a weekly goal of three but Reset changes it to five.
Maybe that's actually what you wanted. Could be. But just looking at this code I have no idea why those two values are different.
If reset means "go back to the initial value", give that value a name and use the same thing in both places.
const initialWeeklyGoal = "3";Now initialization and reset are reading from the same value.
And if the product really does have two different concepts, say an initial goal and some recommended goal, don't hide that difference inside two random strings. Name them separately.
const initialWeeklyGoal = "3";
const recommendedWeeklyGoal = "5";Now somebody reading the code can at least tell that the difference was intentional and not just somebody changing one "3" and forgetting the other.
What If All the State Is in One Object?
You might also have a planner written this way:
const [planner, setPlanner] = useState({
query: "",
showCompleted: true,
weeklyGoal: "3",
});For object state, I usually prefer a small function that creates the starting value.
function createInitialPlanner() {
return {
query: "",
showCompleted: true,
weeklyGoal: "3",
};
}Then initialize with it:
const [planner, setPlanner] = useState(createInitialPlanner);And reset by creating another fresh object.
function handleReset() {
setPlanner(createInitialPlanner());
}Why call the function again instead of keeping one object around forever?
Because your initial state can contain arrays, objects, or other values which somebody might accidentally mutate later.
function createInitialPlanner() {
return {
query: "",
selectedIds: [],
};
}Every call gives us a new selectedIds array too.
What we don't want is something like this:
const initialPlanner = { query: "" };
initialPlanner.query = "state";Well... now your "initial" object isn't initial anymore.
If Reset later gives React that same object, you've already modified the value you were planning to reset to.
For our little three-field planner, separate state variables are still easier to read anyway. I'd only group them once they really belong together and usually change together.
Native Form Reset and React State Are Different Things
HTML already has a reset button.
<button type="reset">Reset</button>And with an uncontrolled input, the browser can restore its default value.
<input name="query" defaultValue="" />But our input is controlled by React:
<input value={query} onChange={handleQueryChange} />Now the text inside that input comes from query.
So if you only tell the browser to reset the DOM input but query still contains "react state", what do you think happens on the next React render?
React gives the input "react state" again.
The browser cannot reset your React state for you. It doesn't know what query even is.
If you really want to use the form's native reset event, handle it and reset the React state yourself.
function handleFormReset(event) {
event.preventDefault();
handleReset();
}<form onReset={handleFormReset}>
<button type="reset">Reset</button>
</form>For our planner though, if we're not using any useful native form reset behavior, I'd just keep the ordinary button:
<button type="button" onClick={handleReset}>
Reset
</button>Less ceremony.
Why Doesn't State Reset Every Time the Parent Renders?
This is another part of state which can feel a bit strange at first.
Take this:
function App() {
return (
<main>
<LessonPlanner />
</main>
);
}Suppose LessonPlanner has a query stored in state and the user types "hooks".
Then App renders again.
Does React call useState("") and put the query back to an empty string?
No.
React preserves state for the component instance it already has at that position.
So if App renders again and React still sees LessonPlanner in the same place, with the same component type and the same key if one exists, React keeps that component's state.
This still preserves the planner state:
function App({ theme }) {
return (
<main className={theme}>
<LessonPlanner />
</main>
);
}The theme changed and App rendered again, sure, but LessonPlanner is still there in the same position. React doesn't randomly throw its state away because the parent rendered.
React associates state with a component instance based on where that component appears in the rendered tree, its type, and its key.
Different JSX Branches Can Still Preserve the Same State
Now look at this:
function App({ compact }) {
return compact
? <LessonPlanner compact />
: <LessonPlanner compact={false} />;
}These look like two different branches in our JavaScript source code, but React still receives a LessonPlanner in the same returned position.
So when compact changes from true to false, React doesn't need to throw away the component and create another one. It's still LessonPlanner. The prop changed, but the component identity stayed the same, so its state stays too.
Now change the component type:
function App({ compact }) {
return compact
? <CompactPlanner />
: <LessonPlanner />;
}That's different.
If React previously had CompactPlanner there and now gets LessonPlanner, those are two different component types. React removes the old component instance and creates the new one, and state inside that replaced subtree starts again from its initializers.
You could abuse this behavior to reset state, but please don't start creating fake component types only because you want to clear an input. If it's still the same planner and you only want its fields back to defaults, use the state setters.
Don't Define Components Inside Components
This one causes some really confusing state resets.
function App() {
function LessonPlanner() {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
);
}
return <LessonPlanner />;
}Looks harmless, right?
But every time App runs, JavaScript creates a new LessonPlanner function.
The name is still LessonPlanner, yes, but it's a different function object from the one created during the previous render. React sees another component type, removes the previous one, creates the new one, and there goes your state.
So the input can keep getting reset.
Move the component outside:
function LessonPlanner() {
const [query, setQuery] = useState("");
// ...
}
function App() {
return <LessonPlanner />;
}Now the LessonPlanner function itself stays the same between App renders.
Two component functions can have the exact same function name and still be different function objects. Define components at module level instead of recreating them inside another component.
When Changing a Key Actually Makes Sense
Keys can also tell React that one component instance should be replaced with another one.
Suppose we have an editor:
<LessonEditor lesson={selectedLesson} />And inside LessonEditor, maybe we initialize a local draft from the selected lesson title.
Now the user selects another lesson.
The LessonEditor is still sitting in the same position, and it's still the same component type. So React normally preserves its local state.
Which can be a problem here.
You might now be editing lesson B while the draft input still contains whatever the user typed for lesson A.
If selecting another lesson really means "this is another editor instance now", use the lesson ID as the key.
<LessonEditor
key={selectedLesson.id}
lesson={selectedLesson}
/>When selectedLesson.id changes, React knows this isn't the same keyed instance anymore. The previous LessonEditor gets removed and a new one gets mounted with fresh local state.
That's a perfectly good use for a key reset because the thing being represented has changed identity.
But notice how much gets reset.
Not just one text field. The entire keyed component subtree starts again. Local state goes away, child state goes away, DOM nodes below it can be recreated, and focus can disappear too.
So if all you wanted was "clear these three fields", changing a key is way more than you needed.
Please Don't Use Random Keys to Reset Stuff
Sometimes you'll see this:
<LessonPlanner key={Math.random()} />This does reset the planner.
It resets it constantly.
Every parent render creates another random number. React sees a different key and decides the previous planner is gone and a new planner exists.
Now type one character into an input. State updates, something renders, another key gets generated, planner gets recreated, text disappears.
Excellent. We have invented an input you cannot type into.
This has the same problem:
<LessonPlanner key={crypto.randomUUID()} />A key doesn't need to be "very unique every millisecond". It needs to stay stable while you're representing the same thing, and change when that thing really becomes another identity.
So this makes sense:
<LessonPlanner key={reader.id} />If we're showing a planner belonging to one reader, the key stays the same. Switch to another reader and the ID changes, so React starts another planner instance.
That's predictable.
Random isn't.
Reset Other State When the Same User Action Requires It
Let's say the user submitted the search once and we keep this state:
const [submitted, setSubmitted] = useState(false);Then the user edits the query again.
Maybe the old "submitted" state doesn't make sense anymore, because they're now changing the input for another attempt.
Reset both from the same event:
function handleQueryChange(event) {
setQuery(event.target.value);
setSubmitted(false);
}One user action caused both state changes, so putting both updates in that handler is pretty straightforward.
And the full planner reset can clear it as well:
function handleReset() {
setQuery(initialQuery);
setShowCompleted(initialShowCompleted);
setWeeklyGoal(initialWeeklyGoal);
setSubmitted(false);
}But don't let handleReset() become some giant function which resets everything you've ever stored anywhere in the application.
If the user's theme preference lives above the planner, leave it alone. If some goal has already been saved to your server, don't delete it just because the local filter form got reset.
Reset the state owned by the thing the user asked to reset.
Also, once the page has more than one resettable section, names such as handleResetPlanner become nicer than having three unrelated functions all called handleReset.
What Happens to Focus?
Direct state reset usually keeps the existing input DOM nodes there.
Maybe their value changes from "state" back to "", but as long as React is still rendering the same input element in the same place, the DOM node can stay there. So if the user had focus in that input, focus normally stays there.
A key reset is different.
Changing the key tells React to remove the previous subtree and create another one. If the focused input lived inside that subtree, that input node just got removed.
So now focus can be lost unless you intentionally decide where it should go after the new instance mounts.
Yet another reason not to use keys just because you wanted to clear three fields.
For normal "put these controls back to their starting values" behavior, setters are usually what you want.
Putting the Planner Together
Our lesson data doesn't change while the component is running, so keep it outside the component.
const lessons = [
{ id: "events", title: "Click Handlers", complete: true },
{ id: "state", title: "First useState", complete: true },
{ id: "forms", title: "Form Inputs", complete: false },
];Then the planner stores only the values the user can change.
function LessonPlanner() {
const [query, setQuery] = useState("");
const [showCompleted, setShowCompleted] = useState(true);
const [weeklyGoal, setWeeklyGoal] = useState("3");
// ...
}The visible lessons are calculated from that current state.
const normalizedQuery = query.trim().toLowerCase();
const visibleLessons = lessons.filter((lesson) => {
const matchesText = lesson.title
.toLowerCase()
.includes(normalizedQuery);
const matchesStatus = showCompleted || !lesson.complete;
return matchesText && matchesStatus;
});And Reset changes the actual stored values.
function handleReset() {
setQuery("");
setShowCompleted(true);
setWeeklyGoal("3");
}That's enough.
We don't need setVisibleLessons(). We don't need setVisibleCount(). We don't need an Effect watching three state variables and then fixing some other state every time they change.
The component runs again and calculates the values from whatever state it has now.
What Actually Happens When the User Types?
Let's follow one query change.
The browser fires the input event, React calls our handler, and the handler requests a new value for query.
React then renders the component again using that new state. During that render, visibleLessons gets calculated again from the current query. React DOM then updates whatever part of the list actually changed.
input change
-> handler requests new query state
-> React renders with the new query
-> visibleLessons is calculated again
-> React DOM updates the required DOMReset is almost the same.
Reset click
-> handler queues the initial values
-> React batches those state updates
-> planner renders with the reset state
-> derived values are calculated again
-> React DOM updates what changedNo Effect needed. No random key. No separate state for the filtered list or count.
We've got three stored values, the rest comes from those values, and Reset just puts the stored values back where they started.
That's really all we're doing.
The key stuff becomes useful later when the whole component instance should represent somebody or something else. But for a Reset button inside the same planner? Just reset the state directly and let React render the result.