I have a bit of a complicated application where I'm making a chart based on the user input values. They pick a start date, and end date, and another parameter. When that extra parameter is filled, it renders the chart. The problem is when the user needs to edit the dates, in react-datepicker
, the start date and end date chosen are updated individually, so it updates the start date and resets the end date to null before the user has chosen an end date, causing the app to error out. I need to figure out how to rework the state updating, so the user has the opportunity to pick an end date when editing the dates before the chart re-renders.
Parent component:
export const OutagePanel: FC<propTypes> = (props) => {
const [parameters, setParameters] = useState({
startDate: null,
endDate: null,
unit: 'None'
})
const handleDateChange = (range) => {
const [startDate, endDate] = range;
setParameters( (prevState) => ({
...prevState,
startDate: startDate,
endDate: endDate
}))
}
const handleUnitChange = (value) => {
setParameters( (prevState) => ({
...prevState,
unit: value
}))
}
return (
<Flex>
<Flex>
<Box>Date Range:</Box>
<Box>
<DatePicker
className={styles.datepicker}
placeholderText="Click to select"
selected={parameters.startDate}
startDate={parameters.startDate}
endDate={parameters.endDate}
onChange={handleDateChange}
showMonthDropdown
showYearDropdown
dropdownMode="select"
minDate={new Date(2000, 0, 1)}
maxDate={new Date(2021, 0, 5)}
todayButton="Today"
selectsRange
/>
</Box>
</Flex>
<GeneratorUnitSelect handleUnitChange={handleUnitChange} />
{parameters.unit != 'None' && <OutageHistoryChart parameters={parameters}></OutageHistoryChart>}
</Flex>
)
}
As seen above, when parameters.unit != 'None'
it will show the OutageHistoryChart
component. So after it first successfully creates and displays a chart, when a user goes back to edit the dates, upon first click in the date picker, it will update the state to something like this:
parameters = {
startDate: <user's new date>,
endDate: null,
unit: 'Unit 1'
}
Since the updated state still contains a valid value in parameters.unit
it passes my test in the return statement and tries to re-render the chart. I know I could add an additional test parameters.endDate != null
before showing the chart and that is likely to fix it, however, it seems like I should be able to use a useEffect
here. This is what I have tried, but the useEffect
gets skipped upon editing the date range and it again fails to render the chart due to the end date missing.
export const OutagePanel: FC<propTypes> = (props) => {
const [parameters, setParameters] = useState({
startDate: null,
endDate: null,
unit: 'None'
})
const [showChart, setShowChart] = useState(false)
const handleDateChange = (range) => {
const [startDate, endDate] = range;
setParameters( (prevState) => ({
...prevState,
startDate: startDate,
endDate: endDate
}))
}
useEffect(() => {
if (parameters.unit != 'None' && parameters.endDate != null) {
setShowChart(true)
} else (
setShowChart(false)
)
}, [parameters.startDate, parameters.endDate, parameters.unit])
//more stuff
Then I changed it to this in the return statement:
{showChart && <OutageHistoryChart parameters={parameters}></OutageHistoryChart>}
Is this a case where a useEffect
is not a valid solution or am I just implementing it wrong? It just seems messy to do checking like parameter.unit != 'None' && parameter.endDate != null
in my return statement.
The issue with using a useEffect
hook to set a showCart
state is that this leaves open at least one render cycle where showCart
is possibly still true from a previous render and the parameters.unit
isn't equal to "None"
and the parameters.endDate
has been updated to null
. To guard this you're still sort of left checking parameter !== "None"
and/or parameter.endDate !== null
to conditionally render the OutageHistoryChart
.
Because of this delay to update the showCart
state you're better off computing a showCart
value. It's considered a React anti-pattern to store derived "state" in React state anyway.
Example:
const showCart = parameters.unit !== "None" && !!parameters.endDate;
If computing the derived state was "expensive" then you should use the useMemo
hook to compute and memoize the result value. In fact... just about any time you find that you've coded a useState
|useEffect
combo to hold/update some "derived" state, you should probably be using the useMemo
hook.
const showCart = useMemo(() => {
return parameters.unit !== "None" && !!parameters.endDate;
}, [parameters.unit, parameters.endDate]);
Complete code example:
export const OutagePanel: FC<propTypes> = (props) => {
const [parameters, setParameters] = useState({
startDate: null,
endDate: null,
unit: 'None'
});
const handleDateChange = (range) => {
const [startDate, endDate] = range;
setParameters( (prevState) => ({
...prevState,
startDate,
endDate
}));
};
const handleUnitChange = (value) => {
setParameters( (prevState) => ({
...prevState,
unit: value
}));
};
const showCart = parameters.unit !== "None" && !!parameters.endDate;
return (
<Flex>
<Flex>
<Box>Date Range:</Box>
<Box>
<DatePicker
className={styles.datepicker}
placeholderText="Click to select"
selected={parameters.startDate}
startDate={parameters.startDate}
endDate={parameters.endDate}
onChange={handleDateChange}
showMonthDropdown
showYearDropdown
dropdownMode="select"
minDate={new Date(2000, 0, 1)}
maxDate={new Date(2021, 0, 5)}
todayButton="Today"
selectsRange
/>
</Box>
</Flex>
<GeneratorUnitSelect handleUnitChange={handleUnitChange} />
{showCart && <OutageHistoryChart parameters={parameters} />}
</Flex>
);
};
Given the above, I think a slightly simpler solution would be to "reset" the parameters.unit
state back to "None"
if/when the endDate
is going to be updated to null
, or falsey, value.
const handleDateChange = (range) => {
const [startDate, endDate] = range;
setParameters( (prevState) => ({
...prevState,
startDate,
endDate,
...!!endDate ? {} : { unit: "None" },
}));
};
This allows the current condition to still work as expected.
Of course, the parameters.unit
state should probably now also be passed to GeneratorUnitSelect
so it's a controlled input and will "react" and show the current unit value (there will need to be a small update to that component to accommodate).
<GeneratorUnitSelect
handleUnitChange={handleUnitChange}
value={parameters.unit}
/>
{parameters.unit !== 'None' && <OutageHistoryChart parameters={parameters} />}