Introduction
So I have written a simple service that, when loaded, reads all of the cookies in the document, reduces them to a simple object where the cookie name is the key, and the cookie value is the value, and then allows you to get a cookie by name by checking cookies[cookieName]
.
It works great, but I guess my biggest questions are:
Questions
- Where can I improve the code of course
- Is it worth optimizing the code to use a for loop instead of reduce, or is it better to leave the code more succinct by using reduce?
- Will most developers know what reduce does? Should I update the code to use for simply to make it easier for other team members to understand what the code does?
But of course, feel free to pick it apart however, I'm always up for learning better ways of coding.
Other Stuff
I'm sure this code could be enhanced by allowing the setting of cookies as well, but for now my needs are simply to read cookies as my application should never be adding or modifying cookies. Therefore set functionality was intentionally left out to discourage those implementing the class from doing so.
The Code
export class CookieService {
// Constructor
constructor(){
// Initialize the cookie service
this.init()
};
// Holds the resulting cookies object after init is run
cookies: any;
// Initializes the cookie service by getting all cookies on the document and creating an object that holds each cookie by name in a key/value pair
init: Function = () => {
// Get the document cookie
this.cookies = document.cookie
// Split it on the ";" character to get each individual cookie in an array
.split(";")
// Reduce the array into an object
.reduce(
// Pass in the cookies object and the current value to the reduce callback function
( cookies, currentValue ) => {
// Split the cookie into key and value
let cookie = currentValue.split("=");
// Sanity check to make sure we got two values from the split and each value has a length. Filters out malformed cookies.
if ( cookie.length === 2 && typeof cookie[0] === "string" && cookie[0].length && typeof cookie[1] === "string" && cookie[1].length ){
// Get the key. Trim removes any spaces from the resulting key to ensure proper matches.
let key = String(cookie[0]).trim();
// Get the value. Trim removes any spaces from the resulting value to ensure proper matches.
let value = String(cookie[1]).trim();
// Set the new cookie on the cookies object
cookies[key] = value;
// Return the new cookies object
return cookies;
}
}, {}
)
};
// Gets a cookie from the cookies object
getCookie: Function = cookieName => {
// If we have a cookies array, and the requested cookie exists
if ( typeof this.cookies !== "undefined" && this.cookies[cookieName]){
// Return the requested cookie
return this.cookies[cookieName];
} else {
// Otherwise return false to let the requester know the cookie was not found
return false;
}
}
}
1 Answer 1
Re using reduce()
: There are many Javascript programmers who like to find clever ways to use this method, and your pattern is probably popular among them. I'm not one of them. I would use forEach()
instead, and assign to this.cookies[key]
within it. But if you like reduce()
, that should be fine; it's a common enough pattern that you shouldn't have to worry about other programmers not understanding it.
However, there's a bug in the way you're using reduce()
. The callback function has to return a result every time, but your function only returns in the if
block. You should move return cookies;
outside the if
.
You have some unnecessary code when expanding currentValue
.
String.prototype.split
always returns an array of strings, so it's not necessary to test if typeof cookie[i] == "string"
. And once you have tested this, it's not necessary to use String(cookie[i])
. You don't have to go overboard on defensive coding.
When returning the cookie, you can simplify the code. It's not necessary to test the value and then return false
if it's not truthy; you can simply return the value, and if it doesn't exist you'll return undefined
, which is just as falsey as false
. And rather than test specifically for typeof this.cookie == "undefuned"
, you can just test if the property is truthy (if you want to be really defensive, you could go the other way and test whether it's actually an array).
Since you're using other EcmaScript 6 features, you could use destructuring of the split()
assignment, rather than referring to cookie[0]
and cookie[1]
repeatedly. Then you can simplify the validation checks -- all the invalid values are also falsey, so if (key && value)
will suffice.
export class CookieService {
// Constructor
constructor(){
// Initialize the cookie service
this.init()
};
// Holds the resulting cookies object after init is run
cookies: any;
// Initializes the cookie service by getting all cookies on the document and creating an object that holds each cookie by name in a key/value pair
init: Function = () => {
// Get the document cookie
this.cookies = document.cookie
// Split it on the ";" character to get each individual cookie in an array
.split(";")
// Reduce the array into an object
.reduce(
// Pass in the cookies object and the current value to the reduce callback function
( cookies, currentValue ) => {
// Split the cookie into key and value
let [key, value] = currentValue.split("=");
// Sanity check to make sure we got two values from the split and each value has a length. Filters out malformed cookies.
key = key && key.trim();
value = value && value.trim();
if ( key && value ){
// Set the new cookie on the cookies object
cookies[key] = value;
}
// Return the updated cookies object
return cookies;
}, {}
)
};
// Gets a cookie from the cookies object
getCookie: Function = cookieName => {
// If we have a cookies array, and the requested cookie exists, return it
return this.cookies && this.cookies[cookieName];
}
}
-
\$\begingroup\$ The reason I was breaking them up like I was is that I need to run
String().trim()
against each one to ensure proper functionality, which I can't do if I assign them the way you have them. Wrapping the values in theString()
function allows me to run trim on them, otherwise the trim function does not work. I don't suppose you have an equally clever way of getting this done more succinctly? \$\endgroup\$StephenRios– StephenRios2017年06月08日 18:08:48 +00:00Commented Jun 8, 2017 at 18:08 -
\$\begingroup\$ You don't need to call
String()
to be able to usetrim()
. As long as the values are strings, you can use any string method on them. I've added the missingtrim()
calls to my answer. \$\endgroup\$Barmar– Barmar2017年06月08日 18:13:36 +00:00Commented Jun 8, 2017 at 18:13 -
\$\begingroup\$ Shouldn't they technically be inside the
if
check to make sure the split produced both results before we run functions against them? \$\endgroup\$StephenRios– StephenRios2017年06月08日 18:47:01 +00:00Commented Jun 8, 2017 at 18:47 -
\$\begingroup\$ Notice that I protected them with
key &&
, so if the split didn't produce a result then we won't calltrim()
. \$\endgroup\$Barmar– Barmar2017年06月08日 19:52:37 +00:00Commented Jun 8, 2017 at 19:52