Creating a mini app to demonstrate the problem I'm having with React & Typescript
Here is a simple toggle component
import { MouseEvent, useState } from "react";
interface ToggleParams {
label?: string;
value?: boolean;
disabled?: boolean;
onToggleClick?: (
event: MouseEvent<HTMLButtonElement>,
value: boolean
) => void;
}
const Toggle = ({
label = "",
value = false,
disabled = false,
onToggleClick = () => {},
}: ToggleParams) => {
const [enabled, setEnabled] = useState(value);
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
if (disabled) return;
console.log("handleClick > enabled", enabled);
const newValue = !enabled;
console.log("handleClick > newValue", newValue);
setEnabled(value);
onToggleClick(event, newValue);
};
return (
<button
onClick={handleClick}
disabled={disabled}
className="focus:outline-none"
>
{`${enabled ? "true" : "false"} ${label}`}
</button>
);
};
export default Toggle;
here is the code that tests this component
import { useState } from "react";
import Toggle from "./Toggle";
export default function App() {
const [toggleAll, setToggleAll] = useState(false);
const [toggleItem, setToggleItem] = useState(false);
const [toggleDisabled, setToggleDisabled] = useState(false);
console.log("Toggle state", toggleAll, toggleItem, toggleDisabled);
const onToggleAllClick = (event: any, value: boolean) => {
setToggleAll(value);
setToggleItem(value);
setToggleDisabled(value);
};
const onToggleItemClick = (event: any, value: boolean) => {
setToggleItem(value);
if (!value) {
setToggleAll(value);
setToggleDisabled(value);
}
};
return (
<div className="App">
<div>
<Toggle
value={toggleAll}
label={"Toggle All"}
onToggleClick={onToggleAllClick}
/>
</div>
<div>
<Toggle
value={toggleItem}
label={"Toggle Item"}
onToggleClick={onToggleItemClick}
/>
</div>
<div>
<Toggle
value={toggleDisabled}
label={"Toggle Disabled"}
disabled={true}
/>
</div>
</div>
);
}
The toggle should change state when I click them, however they don't on the 1st click
When I click the Item
toggle, the console correctly shows
handleClick > enabled false
handleClick > newValue true
Toggle state false true false
indicating that the code works, however the display shows
false Toggle All
false Toggle Item
false Toggle Disabled
which means the toggle component itself didn't change
clicking it again, then changes the display correctly to this
false Toggle All
true Toggle Item
false Toggle Disabled
the console output doesn't change
I refresh the page to start again
When I click the All
toggle, all the items should be true, and the console correctly shows
handleClick > enabled false
handleClick > newValue true
Toggle state true true true
however the display incorrectly shows
false Toggle All
false Toggle Item
false Toggle Disabled
all these should have changed from false to true
when I click All
toggle again, the console is the same, but the display changes to
true Toggle All
false Toggle Item
false Toggle Disabled
toggle All
display changed correctly this time, but the others should have changed as well
Bug: toggles need to be clicked twice before they change their own value on the display
now click item
toggle (once), the item
toggle now displays true and only needed 1 click
now click item
toggle again (twice) so that it shows false
the display changes to
handleClick > enabled true
handleClick > newValue false
Toggle state false false false
the code correctly changed all the values to false here in the console according to onToggleItemClick
, however the display still incorrectly shows
true Toggle All
false Toggle Item
false Toggle Disabled
Bug: toggles need to be change their value when the underlying value from the code changes
Bug: the disabled toggle should also change its value when the underlying value from the code changes
what do I need to do to correctly show get the toggle to display the correct value?
Here is a sandbox https://codesandbox.io/p/sandbox/vibrant-jackson-44g6xt
You're unnecessarily complicating it by giving the Toggle
component it's own state. So there's actually two problems in the code:
The first is that in the handleClick
function, you are setting enabled
to the current value, not the new value, this is what's creating the "first-click" delay:
// ...
const newValue = !enabled;
setEnabled(value); // instead you should use `newValue`
onToggleClick(event, newValue);
// ...
So here, enabled
will always be one change behind the actual state from the parent. value
is not yet updated to the new value until the next render and that's what you are setting for enabled
. The next render comes along and value
is now updated but enabled
is not.
The second problem is your use of state in the Toggle
component in the first place, this creates a separate state for each Toggle
instance that is not connected to the parent state apart from providing an initial value (value
):
// ...
const [enabled, setEnabled] = useState(value);
// ...
{`${enabled ? "true" : "false"} ${label}`}
// any changes to `value` won't have any effect on `enabled`
I think what you mainly want though is for the parent state to be the single source of truth, to do this you should remove the separate state inside the Toggle
component and just use value
.