Skip to content

Instantly share code, notes, and snippets.

@DavidLazic
Created April 13, 2022 09:36
Show Gist options
  • Save DavidLazic/f6a0249507934e26991ded7598aa0ff3 to your computer and use it in GitHub Desktop.
Save DavidLazic/f6a0249507934e26991ded7598aa0ff3 to your computer and use it in GitHub Desktop.
Challenge of implementing checkbox component with no conditional statements
# Tinkering
## Checkbox -- `no conditional` statements
#### Challenge
Create a checkbox component and handle boolean logic without using any `if` or `ternary` statements. Checkbox component should support `onChange` callback property, with string value as an argument result, i.e. `Unchecked` and `Checked` states.
**Advanced**:
- Upgrade the component to support _transitional_ state `Maybe`
- Upgrade the component to support _any_ number of states.
**Important**: If you'd like to try the implementation yourself, no further reading as you'll get the spoilers below =)
## Table of Contents
1. [Setup](#setup)
2. [Dual state implementation](#dual_state)
3. [Triple state implementation](#triple_state)
4. [Any state implementation](#any_state)
### Setup
<a name="setup"></a>
Starting with the basic structure:
```js
import React from 'react';
enum CheckboxStates {
UNCHECKED = 'Unchecked',
CHECKED = 'Checked',
}
interface Props {
onChange: (value: string) => void;
}
const CustomCheckbox: React.FC<Props> = ({ onChange }) => {
const handleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
// ...logic
};
return (
<input type="checkbox" onChange={handleChange} />
);
};
export default CustomCheckbox;
```
We create a `CustomCheckbox` component which renders native `input` checkbox with `handleChange` bind. This callback receives `ChangeEvent` argument, from which we can read the `target.checked` property and determine its boolean state.
From now on, we'll mostly focus on tinkering with the `handleChange` callback, since it's the primary logic handler.
### Dual state
<a name="dual_state"></a>
Let's start with the most basic version of the `handleChange` callback:
```js
const handleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
if (target.checked) {
return onChange(CheckboxStates.CHECKED);
} else {
return onChange(CheckboxStates.UNCHECKED);
}
};
```
The `else` statement is not really necessary here, so we can just rewrite this with `return` statement:
```js
const handleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
if (target.checked) {
return onChange(CheckboxStates.CHECKED);
}
return onChange(CheckboxStates.UNCHECKED);
};
```
Now, we definitely have two `onChange` calls under the same function context, so we can merge these with ternary operator:
```js
const handleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const value = target.checked ? CheckboxStates.CHECKED : CheckboxStates.UNCHECKED;
return onChange(value);
};
```
Okay, we "removed" the `if` statement by replacing it with a ternary operator. But it's esentially still the same `if` logic written a bit differently. So, in order to rewrite this flow without conditionals, we'll need to introduce some more changes.
First of, we could start using a `map object` in order to discern which string value we need to pass to the `onChange` callback.
```js
enum CheckboxStates {
UNCHECKED = 'Unchecked',
CHECKED = 'Checked',
}
const handleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const states = {
true: CheckboxStates.CHECKED,
false: CheckboxStates.UNCHECKED,
};
const value = states[String(target.checked)];
return onChange(value);
};
```
This is looking a bit better, but now we have a `map object` literal **and** an `enum`, which are basically two maps. So maybe we could merge these as well:
```js
enum CheckboxStates {
false = 'Unchecked',
true = 'Checked',
}
const handleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const value = CheckboxStates[String(target.checked)];
return onChange(value);
};
```
Still not great, the `enum` looks weird with these `true` and `false` string keys. So, let's revert the enum and update the `handleChange` callback to read from it like so:
```js
enum CheckboxStates {
UNCHECKED = 'Unchecked',
CHECKED = 'Checked',
}
const handleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const states = Object.values(CheckboxStates);
const value = values[Number(target.checked)];
return onChange(value);
};
```
By creating a string array out of `enum` values, we can extract correct value by casting `target.checked` into a number and use it as array's index key. The end result (value) is always going to be the correct one without using any conditional statements.
With this, the end result of basic implementation would look like this:
```js
import React from 'react';
enum CheckboxStates {
UNCHECKED = 'Unchecked',
CHECKED = 'Checked',
}
interface Props {
onChange: (value: string) => void;
}
const CustomCheckbox: React.FC<Props> = ({ onChange }) => {
const handleChange = ({ target }: ChangeEvent<HTMLInputElement>) =>
onChange(
Object.values(CheckboxStates)[Number(target.checked)]
);
return (
<input type="checkbox" onChange={handleChange} />
);
};
export default CustomCheckbox;
```
### Triple state
<a name="triple_state"></a>
Moving on to the advanced part.
Our PM just mentioned that design team suggested that we add a _transitional_ state to our checkbox component, which would yield a `Maybe` string value. So, let's see how we can add a third state to our checkbox.
We'll add the `Maybe` enum field and the first thing we could probably do is to introduce internal `isChecked` and `isMaybe` component states. This would allow us to have a transitional "skip step" between `Unchecked` and `Checked` states.
You'll also notice that we don't need the `event.target` anymore as now we're dealing with three states, instead of a boolean `true/false`.
```js
import React, { useState } from 'react';
enum CheckboxStates {
UNCHECKED = 'Unchecked',
MAYBE = 'Maybe',
CHECKED = 'Checked',
}
interface Props {
onChange: (value: string) => void;
}
const CustomCheckbox: React.FC<Props> = ({ onChange }) => {
const [isChecked, setIsChecked] = useState(false);
const [isMaybe, setIsMaybe] = useState(false);
const handleChange = () => {
if (!isChecked && !isMaybe) {
setIsMaybe(true);
return onChange(CheckboxState.MAYBE);
}
if (!isChecked && isMaybe) {
setIsMaybe(false);
setIsChecked(true);
return onChange(CheckboxStates.CHECKED);
}
if (isChecked && !isMaybe) {
setIsChecked(false);
return onChange(CheckboxStates.UNCHECKED);
}
};
return (
<input type="checkbox" onChange={handleChange} />
);
};
export default CustomCheckbox;
```
Okay, now that the basic `if` implementation is done, let's see how we can improve it further and have a bit more functional logic flow. We could deduce current state value by leveraging an array index again.
Instead of having two separate component states, we can use a single one representing the current state index.
```js
import React, { useState } from 'react';
enum CheckboxStates {
UNCHECKED = 'Unchecked',
MAYBE = 'Maybe',
CHECKED = 'Checked',
}
interface Props {
onChange: (value: string) => void;
}
const CustomCheckbox: React.FC<Props> = ({ onChange }) => {
const [stateIndex, setStateIndex] = useState(0);
const handleChange = () => {
const possibleIndex = stateIndex + 1;
const index = CheckboxStates[possibleIndex] ? possibleIndex : 0;
onChange(CheckboxStates[index]);
return setStateIndex(index);
};
return (
<input type="checkbox" onChange={handleChange} />
);
};
```
With this change, we're simply incrementing the next possible state index on every checkbox change. If the next index is non existent within the states array, we'll simply reset it and go back from `0` again.
And again, we did manage to "remove" `if` statements, but we're still using conditionals. **If** only we could have an infinite loop of checkbox states that would reset itself when it reaches the end state.
Let's bring in generator functions to the rescue:
```js
const _loop = function* <T>(arr: T[]): Generator<number> {
let next = 1;
while (true) {
yield next;
next = arr.indexOf(arr[next + 1] || arr[0]);
}
};
```
This `_loop` generator takes an array as argument (string array in our case), and based on the existence of `next + 1` index within the array, either returns its current value or resets it back to `0` for a next iteration call.
When we apply this generator function to our checkbox component, it's going to look like this:
```js
import React, { useState } from 'react';
import { _loop } from 'src/utils';
enum CheckboxStates {
UNCHECKED = 'Unchecked',
MAYBE = 'Maybe',
CHECKED = 'Checked',
}
interface Props {
onChange: (value: string) => void;
}
const CustomCheckbox: React.FC<Props> = ({ onChange }) => {
const iterator = useRef(_loop(Object.values(CheckboxStates)));
const handleChange = () => {
const { value: index } = iterator.current.next();
return onChange(Object.values(CheckboxStates)[index]);
};
return (
<input type="checkbox" onChange={handleChange} />
);
};
```
By introducing this utility iterator, we're able to loop through all possible `CheckboxStates` indices `(0, 1, 2, 0, 1, 2, 0, ...)` infinitely, simply by calling `iterator.next()`. Notice that we've invoked this generator within `useRef` hook in order for the iterator to persist and preserve state between possible re-renders.
Now, in order for our UI to be updated, we'd need to add some UI flavor, such as a `label` wrapper, and an internal state for re-renders.
```js
import React, { useState } from 'react';
import cx from 'classnames';
import { _loop } from 'src/utils';
import styles from './CustomCheckbox.module.scss';
enum CheckboxStates {
UNCHECKED = 'Unchecked',
MAYBE = 'Maybe',
CHECKED = 'Checked',
}
interface Props {
onChange: (value: string) => void;
}
const CustomCheckbox: React.FC<Props> = ({ onChange }) => {
const states = Object.values(CheckboxStates);
const [stateIndex, setStateIndex] = useState(0);
const iterator = useRef(_loop(states));
const handleChange = () => {
const { value: index } = iterator.current.next();
onChange(states[index]);
return setStateIndex(index);
};
return (
<label className={cx(styles.label, styles[`label__${values[valueIndex]}`])}} htmlFor="input" >
<input id="input" type="checkbox" onChange={handleChange} />
</label>
);
};
```
Success, no conditionals within our `CustomCheckbox` component!
### Any state
<a name="any_state"></a>
Based on the newly added _transitional_ state to the checkbox component, client's inspiration soared and they suggested that we implement multiple states to our component.
No worries, we got everything covered already. Adding any number of additional states is as easy as changing our `enum`:
```js
enum CheckboxStates {
UNCHECKED = 'Unchecked',
MAYBE = 'Maybe',
CHECKED = 'Checked',
FOO = 'Foo',
BAR = 'Bar',
}
```
Done!
This was possible due to our component's setup and by re-arranging its logic flow in a way that its `CheckboxStates enum` is the single source of truth. Based on its values (any number of them), we create an iterator that's going to reset itself when it reaches the end state.
One thing to note here is that we could as well switch from using `enum` to a regular string array for component's states, but enums give us the ability to use them both as types and as values in our code (in case we need these elsewhere).
Lastly, and maybe you might've guessed this, but from adding the third state to our component, there's no more need for us to keep using native `<input type="checkbox">`. Native checkboxes, alas, have a finite number of states, representing only `true/false` values. Instead, we could switch to using a native `button`, and the component will behave in exactly the same way.
### Conclusion
<a name="conclusion"></a>
This was just one of the possible ways to achieve our goal, depending on the requirements and the context given. Goal of this post was to (hopefully) be a brain teaser and give you guys some inspiration for your future work!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment