The answer isn't always simple, even when the problem seems to be. Figuring out answers and alternatives is an important skill. Let's look at a small exercise. Think about your potential solution, and how it stands up to the different problems.
Initial Value
Sometimes we need a value more unique than null
, or undefined
, or false
, or 0
. What do we do then?
Definitely Unique
There are scenarios where we need to ensure a value represents something unique that no other value can, like an initial value in a data store or a hook. Once set, we should never be able to get that value again. With a generic utility we may not know what values it will accept, so that value can't be something you could set. How can we create our own uniqueness? A few ways, thanks to JavaScript's design.
Objects & Strict Equality
You've probably seen simple demonstrations of strict equality showing that two different objects in JavaScript are not the same.
[] === []; // false
/*
* This uses parentheses because a line starting with a curly brace
* is assumed to be a code block, not an object. It's the same
* reason we can't use object destructuring without parentheses
* or a declaration like let or const.
*/
({}) === {}; // false
This means we can create unique objects and test for them. If they are later replaced with a different value, they will not match, even if the new value is the same type or shape of object.
So let's make a simple demo, starting with the initial value example.
Initial Value: Attempt 1 - Foreshadowing Title 🤔
Because of strict equality, all we really need is a value that hangs around to be the default that isn't used for anything else, right?
Code
const INITIAL_VALUE = {};
export const makeValueStore = (value = INITIAL_VALUE) => ({
get: () => value,
set: (newValue) => {
value = newValue;
return value;
},
isInitial: () => value === INITIAL_VALUE,
});
Testing
const data1 = makeValueStore();
const data2 = makeValueStore(false);
data1.isInitial(); // true
data2.isInitial(); // false
Now we have this very simple function that sets an initial value, and we can check for it. Once it's set to something else, isInitial()
is no longer true
.
Mistakes Were Made
Well, until we misuse the data. In the simple example, INITIAL_VALUE
is in the same file, so we could just access it directly.
data2.set(INITIAL_VALUE);
data2.isInitial(); // true
But even if that were in another file and not directly accessible, we still have an initial value shared across each value store.
// Access the shared initial value from another store
data2.set(data1.get());
data2.isInitial(); // true;
It turns out we didn't guarantee uniqueness across instances of the value store. So let's try again.
Initial Value: Attempt 2 – Probably Fine 🧐
We know that the initial value shouldn't be public, and it should be unique to each instance.
Requirements:
- A data store should default to a unique initial value.
- The initial value should be unique to each instance.
- The initial value should not be publicly accessible.
Code
export const makeValueStore = (value) => {
const INITIAL_VALUE = {};
let internalValue = value ?? INITIAL_VALUE
return {
get: () => internalValue,
set: (newValue) => {
internalValue = newValue;
return internalValue;
},
isInitial: () => internalValue === INITIAL_VALUE,
};
};
Testing
Let's prove that we can't "borrow" an initial value from another instance.
const data1 = makeValueStore();
const data2 = makeValueStore(false);
data1.isInitial(); // true
data2.isInitial(); // false
data2.set(data1.get());
data2.isInitial(); // false;
More Mistakes
Even though we now have isolation between instances, we can still access the initial value from a store and set it back later.
const myCopyOfInitial = data1.get();
data1.set('something else');
data1.isInitial(); // false
data1.set(myCopyOfInitial);
data1.isInitial(); // true;
So we have to be mindful that to guarantee uniqueness, we cannot return the private value either.
Initial Value: Attempt 3 – This Time For Sure 👍
I think we're getting close!
Requirements:
- A data store should default to a unique initial value.
- The initial value should be unique to each instance.
- The initial value should not be publicly accessible.
-
get()
should not return the actual initial value.
Code
export const makeValueStore = (value) => {
const INITIAL_VALUE = {};
let internalValue = value ?? INITIAL_VALUE
return {
get: () => internalValue === INITIAL_VALUE ? null : internalValue,
set: (newValue) => {
internalValue = newValue;
return internalValue;
},
isInitial: () => internalValue === INITIAL_VALUE,
};
};
Testing
const data1 = makeValueStore();
const initial = data1.get();
data1.isInitial(); // true
// Change it.
data1.set('later');
data1.isInitial(); // false
// Try to reset back to initial
data1.set(initial);
data1.isInitial(); // false
We now have guaranteed uniqueness for the initial value. We can't access initial, but we can guarantee it can never be reset back.
More Unique
We used a plain object, but it isn't the only thing that creates a unique instance. We can use objects, arrays, even functions. Any non-primitive type, including functions, can provide this for you.
However, there is a type specifically made for this: Symbol. Symbols are made to provide the uniqueness we have been squeezing out of other types. They handily work in this case, and in many others.
Code
We can just swap in Symbol for the object:
export const makeValueStore = (value) => {
const INITIAL_VALUE = Symbol('initial');
let internalValue = value ?? INITIAL_VALUE
return {
get: () => internalValue === INITIAL_VALUE ? null : internalValue,
set: (newValue) => {
internalValue = newValue;
return internalValue;
},
isInitial: () => internalValue === INITIAL_VALUE,
};
};
Tangent: Alternate Design
Once we decided we to not expose the INITIAL_VALUE
, a different possibility opened up. We can change our logic and allow any initial value to still indicate true
, and eliminate the need for the special instance check.
Code
export const makeValueStore = (value) => {
let isInitial = true;
let internalValue = value;
return {
get: () => internalValue,
set: (newValue) => {
isInitial = false;
internalValue = newValue;
return internalValue;
},
isInitial: () => isInitial,
};
};
This design keeps initial as a Boolean that is only ever set to false. Now there is no uniqueness concern, and regardless of what the value is set to, isInitial()
provides clarity.
const data1 = makeValueStore('one');
data1.get(); // 'one';
data1.isInitial(); // true
// Change the value, it is no longer initial
data1.set('two');
data1.isInitial(); // false
// Change it back? Still false
data1.set('one');
data1.isInitial(); // false
Conclusion
Sometimes your first idea works out. Sometimes it should be discarded if you find a better solution. But identifying the defects and documenting the new requirements as they become clear helps you better understand the problem and ensure the code does what you expect.
What were the different solutions you came up with for the problem? Did they suffer from any of the defects we found? Are there more defects we need to worry about? Let us know in the comments!