Nested Checkbox Coding Challenge
I had an interview challenge shared with me that's used at a large fintech company. This is my solution.
Requirements
Implement a nested checkbox component that satisfies the following interface:
type CheckboxOption = {
[key: string]: boolean | CheckboxOption;
};
const NestedCheckbox: ({
value,
onChange
}: {
value: CheckboxOption;
onChange: (value: CheckboxOption) => void;
}) => React.ReactNode;
The following input should produce the following output:
const App = () => {
const [value, onChange] = useState({
foo: true,
bar: false,
group1: {
level2: true,
level2option2: false,
nested: {
option: true
}
}
});
return <NestedCheckbox value={value} onChange={onChange} />;
};
The requirements for the implementation are:
- The non-leaf checkboxes (those corresponding to object values) are checked only if all of their child leaf checkboxes are checked
- There is no state maintained inside
NestedCheckbox
, as the component is fully controlled - [EXTRA] Clicking a non-leaf checkbox will toggle all of its children on or off, depending its the current checked status
Demo
{ "foo": true, "bar": false, "group1": { "level2": true, "level2option2": false, "nested": { "option": true } } }
Source
The problem I have with this problem is that the interface doesn’t allow for recursiveness with reference to a top-level root.
With a top-level onChange
, we could always mutate the initial value
object directly and call onChange(value)
to re-render the fully-controlled NestedCheckbox
.
However, this means not being able to call <NestedCheckbox value={childValue} onChange={onChange} />
because that would replace the top-level value.
So instead, I created a refresh
wrapper function that triggers the parent NestedCheckbox
’s onChange
, eventually reaching the parent:
const refresh = () => onChange(structuredClone(value))
- This is passed to child
<NestedCheckbox value={childValue} onChange={refresh} />
, since I don’t care about the childvalue
updates and only want to refresh. - I’m using
structuredClone
, which is a performant alternative toJSON.parse(JSON.stringify(value))
.
import type { JSX } from 'preact'
import { useState } from 'react'
type CheckboxOption = {
[key: string]: boolean | CheckboxOption
}
const isCheckboxOptionChecked = (value: boolean | CheckboxOption): boolean => {
if (typeof value === 'boolean') {
return value
}
return Object.keys(value).every((key: string) =>
isCheckboxOptionChecked(value[key] as CheckboxOption),
)
}
const toggleCheckboxOption = (
parent: CheckboxOption,
key: string,
checked: boolean,
): CheckboxOption => {
const currentValue = parent[key]
if (typeof currentValue === 'boolean') {
parent[key] = checked
} else {
Object.keys(currentValue).forEach((key) => {
toggleCheckboxOption(currentValue, key, checked)
})
}
return parent
}
const NestedCheckbox = ({
value,
onChange,
}: {
value: CheckboxOption
onChange: (value: CheckboxOption) => void
}) => {
const refresh = () => onChange(structuredClone(value))
return (
<ol className="list-none">
{Object.entries(value).map(([key, val]) => (
<li key={key}>
<label>
<input
type="checkbox"
name={key}
checked={isCheckboxOptionChecked(val)}
onChange={(event: JSX.TargetedEvent<HTMLInputElement>) => {
toggleCheckboxOption(
value,
key,
(event.target as HTMLInputElement).checked,
)
refresh()
}}
/>
{key}
</label>
{typeof val === 'object' && (
<NestedCheckbox value={val} onChange={refresh} />
)}
</li>
))}
</ol>
)
}
const App = () => {
const [value, onChange] = useState<CheckboxOption>({
foo: true,
bar: false,
group1: {
level2: true,
level2option2: false,
nested: {
option: true,
},
},
})
return (
<>
<NestedCheckbox value={value} onChange={onChange} />
<pre>{JSON.stringify(value, null, 2)}</pre>
</>
)
}
export default App