Why Flow still complains about null values for document.getElementById - flowtype

Why even with an IF check, Flow still complains about a possibly null value
if(document && document.getElementById("myID") && document.getElementById("myID").offsetWidth){
console.log(document.getElementById("myID").offsetWidth);
}
Gives this error
^ property `offsetWidth`. Property cannot be accessed on possibly null value

Flow has no way to know that the success of first call to getElementById means that the later ones will also succeed. For all it knows, reading the offsetWidth property could cause getElementById to start returning null the next time it is called.
You'll need to store the value, e.g.
const myIdEl = document && document.getElementById("myID");
if(myIdEl && myIdEl.offsetWidth) {
console.log(myIdEl.offsetWidth);
}
this way there is no way for myIdEl to become null after it has been referenced.

For HTMLElement (and extensions of HTMLElement like VideoHTMLElement) in FlowType, I'd recommend using instanceof to validate the Type and to validate that it's not null.
Also, I don't believe you need to check if document exists, that is defined globally in flow (1)*
<HTMLElement> Example
const myIdEl: ?HTMLElement = document.getElementById('myID');
if (myIdEl instanceof HTMLElement) {
// continue
console.log(myIdEl.offsetWidth);
}
<HTMLSelectElement> Example
const selectEl: ?HTMLElement = document.getElementById('someSelectElement');
// Checks correct type (!null !undefined come for free)
if (selectEl instanceof HTMLSelectElement) {
const selectedVal = selectEl.options[selectEl.selectedIndex].value;
}
<HTMLVideoElement> Example using invariant
import invariant from 'invariant';
const videoContent = document.getElementById('video-player');
invariant(videoContent instanceof HTMLVideoElement, 'No video element');
// do stuff with video api
videoContent.volume = 0;
videoContent.plause();
https://github.com/facebook/flow/blob/f3f29f7fd8c5aa73ac5a8a546ccfbc29cd7505fe/lib/dom.js#L1288

Related

When do you use updater() and patchstate() in ngrx/component-store?

I'm using ngrx/component-store and loving it so far. Having prior store knowledge building my own simple ones, the only real headache I've had so far is when I've had to update an array and figured out I have to always create a new one for the internal compare() pipe to realize the array got updated.
Anyway, reading through the documentation it talks about updater methods and patchState. To me they do exactly the same thing, but their creation is slightly different. You would call patchState inside of a method while this.updater() returns a method giving you a function you can expose in your service. Anytime I'm updating my state it's always after a network call. I assume there are plenty of scenarios where you'd want to update your state without a network call so this is why you would want to have an updater available to your component to call. The question is if an updater and patchState are really doing the same thing then is it a better practice to call an updater in an effect or use patchState, or maybe am I putting too much logic in my effect?
On a side note, the docs say an updater method is supposed to be a pure function. If you're using it to your push an object onto an array then is it really pure?
// adding the selectors so people know what components are subscribing to
readonly approvals$ = this.select(state => state.requestApprovals);
readonly registration$ = this.select(state => state);
readonly updateAssessment = this.effect(($judgement: Observable<{id: string, isApproved: boolean}>) => {
return $judgement.pipe(
switchMap((evaluation) => {
const state = this.get();
return this.requestApproval.patch(state.id, state.companyName, evaluation.id, evaluation.isApproved).pipe(
tapResponse(
(result) => {
// is it better to call patchState()?
this.patchState((state) => {
for(let i = 0; i < state.requestApprovals.length; i++) {
if(state.requestApprovals[i].id == result.id) {
state.requestApprovals[i].isApproved = result.isApproved;
}
}
// the take away is you must assign a whole new array object when you update it.
state.requestApprovals = Object.assign([], state.requestApprovals);
return state;
});
// or this updater?
// this.applyDecisionPatch(evaluation);
},
// oh look! another updater reassigning my array to the state so
// it propagates to subscribers to reset the UI
() => { this.reverseDecision(); }
)
);
})
);
});
// this is private to make sure this can only be called after a network request
private readonly applyDecisionPatch = this.updater((state, value: {id: string, isApproved: boolean}) => {
for(let i = 0; i < state.requestApprovals.length; i++) {
if(state.requestApprovals[i].id == value.id) {
state.requestApprovals[i].isApproved = value.isApproved;
}
}
state.requestApprovals = Object.assign([], state.requestApprovals);
return state;
});
Note: There's no tag for ngrx-component-store so couldn't tag it.
An updater can be compared to a reducer.
All the options to modify the state should change it in an immutable way.
A library like ngrx-immer can be used to make this easier.
The main difference is that updater receives the current state, and you can change the state based on it. E.g. a conditional update, or can be used with #ngrx/entity
While with setState and patchState, you just set state properties.
setState updates the whole state object, whereas patchState only sets the given properties and doesn't touch the rest of the state object.
These two methods are also easier to use when you just want to set the state, because you don't have to create an updater function.
To answer the side question, push is not immutable. Instead of creating a new instance, it updates the array instance.

Flowtype constantly requiring null checks

I'm wondering how to avoid these numerous null checks or at least understand what the point is because it seems counter-productive.
Flowtype is giving me an error for this if I omit the null check:
var myEl = new MyElement()
if (document.body != null) { // error on next line if omitted
document.body.appendChild(myEl)
}
I have to do that null check for the document body in every single callback too, because who knows, maybe the body is null here right?!
I think this is total overkill. Not only that, but what's the point of such a simple nullcheck? It will just silently skip over a vital part of the program and exhibit undefined behavior somewhere else and make debugging the app that much harder.
I'd really prefer just having a null exception at this point if an error ever happens here, because to be really sure this tiny 2-line code segment that I'd write in javascript would have to be like this in flowtype:
var myEl = new MyElement()
if (document.body != null) {
document.body.appendChild(myEl)
} else {
console.error("null error")
}
So 4 additional code lines and some nesting just to trace something I'd get for free if I just let the app run into an error. And I need those 4 lines on every single querySelector. On every single document.body. On every single getElementByTagName. This alone probably increases my entire codebase by 10%.
What's the point of enforcing this so strictly?
In other languages I'd also be able to try-catch around these hotspots gradually as needed, flow doesn't let me do that either. It shows errors whether I add a try-catch or not.
By using a type checker, you are opting into the rules that it enforces. Accessing a property on a nullable type is one of those restrictions. So if you want to have exceptions for null values, you need to explicitly throw to prove to Flow that it is what you want. You could for instance make a module like
if (!document.body) throw new Error("Unexpectedly missing <body>.");
export const body: HTMLElement = document.body;
export function querySelector(el: HTMLElement, selector: string): HTMLElement {
const result = el.querySelector(selector);
if (!result) throw new Error(`Failed to match: ${selector}`);
return result;
}
By throwing, these functions explicitly say "I will return an element" in all cases, and in null cases, they will throw exceptions.
Then in your normal code, you are guaranteed you can use those
import {body, querySelector} from "./utils";
body.appendChild(document.createElement('div'));
querySelector(body, 'div').setAttribute('thing', 'value');
and it will typecheck property.
When I know for sure that my variable won't be null and Flow doesn't, I use an unwrap() function:
export default function unwrap<T>(value: T): $NonMaybeType<T> {
if (value !== null && value !== undefined) return value
throw new Error('Unwrapping not possible because the variable is null or undefined!')
}

Exception in template helper: Error: Match error

I'm trying to perform a custom sort using a comparator function from within a template helper in Meteor.
Here is my template helper:
Template.move_list.helpers({
assets() {
return Assets.find({}, { sort: sortFunction });
}
});
And here is the comparator function:
const sortFunction = function (doc1, doc2) {
const barcodes = Session.get('barcodesArray');
if (barcodes.indexOf(doc1.barcode) === -1 || barcodes.indexOf(doc2.barcode) === -1) {
return 0;
}
let last = null;
_.each(barcodes, function (barcode) {
if (barcode === doc1.barcode) last = doc1.barcode;
if (barcode === doc2.barcode) last = doc2.barcode;
});
return last === doc1.barcode ? 1 : -1;
}
Error
When the page loads, the following error is returned:
Exception in template helper: Error: Match error: Failed Match.OneOf, Match.Maybe or Match.Optional validation
I put a breakpoint in chrome into the sortFunction, however the function was never entered and the breakpoint never reached.
Of course, the error is not throw when I remove sort.
References
This feature is not very well documented, however here is the relevant part of the docs:
For local collections you can pass a comparator function which receives two document objects, and returns -1 if the first document comes first in order, 1 if the second document comes first, or 0 if neither document comes before the other. This is a Minimongo extension to MongoDB.
And the commit by mitar adding the functionality, with example code from the test:
var sortFunction = function (doc1, doc2) {
return doc2.a - doc1.a;
};
c.find({}, {sort: sortFunction})
Can anyone make sense of this error?
Edit:
This issue should be resolved in Meteor >= v1.3.3.1.
Local collections (i.e, client-side and in-memory server-side collections) will allow to pass a function as the sort clause.
The error comes from the mongo package, where the spec does not allow sort to be a function.
#mitar changed LocalCollection in the minimongo package. LocalCollection is part of the Mongo.Collection object on the client (its _collection attribute), but queries are still checked according to the original mongo spec. I believe this to be a bug, as the spec was not updated to reflect the change.
To overcome this (in the meantime), either have the function accept a sub-field, such that the sort value is an object:
var sortFunction = function (x, y) {
return x - y;
};
c.find({}, {sort: {a: sortFunction}});
or use the c._collection.find() instead, which will work (as far as I can tell), except it will not apply any transformations defined for the collection.
var sortFunction = function (doc1, doc2) {
return doc2.a - doc1.a;
};
c._collection.find({}, {sort: sortFunction});

Flow doesn't recognise refinement inside callback

This code passes the flow check:
/* #flow */
function test (list: ?Array<string>): Promise<number> {
if(list !== null && list !== undefined) {
return Promise.resolve(list.length)
} else {
return Promise.resolve(0)
}
}
console.log(test(null))
Whereas the following gets a null check error
/* #flow */
function test (list: ?Array<string>): Promise<number> {
if(list !== null && list !== undefined) {
return Promise.resolve().then(() => list.length)
} else {
return Promise.resolve(0)
}
}
console.log(test(null))
error:
property `length`. Property cannot be accessed on possibly null value
Clearly list cannot be null so there must be something about the code structure that makes flow unable to recognise this.
I would like to understand what limitation I am hitting and how I can work around it. Thanks!
Basically, Flow doesn't know that your type refinement (null check) will hold at the time when () => list.length is executed. Inside that callback Flow only looks at the type of list – which says it can be null.
The difference between first and second snippet is that in the second snippet list is crossing a function boundary – you're using it in a different function than where you refined its type.
One solution is to extract list.length into a variable, and use that variable in the callback.
var length = list.length;
return Promise.resolve().then(() => length)
This might also work:
var list2: Array<string> = list;
return Promise.resolve().then(() => list2.length)
Note that this problem exists even for immediately invoked callbacks, e.g. when using map or forEach. There is an issue on flow's github about this, but I couldn't find it after a quick search.

Checking for existence of $firebaseObject

For a normal Javascript object on $scope, you might use ngShow with a div to make the div's presence depend on the existence of the object.
$scope.myObj = null;
// And then later
$scope.myObj = {arbitrary:{number:{of:"objects"}}};
...
<div ng-show="myObj">{{myObj}}</div>
But if you tie an object to a $firebaseObject
var fbObjRef = new Firebase(FirebaseURL).child("fbObj")
$scope.myObj = $firebaseObject(fbObjRef);
Then your ngShow won't work because a reference that is empty in Firebase will still be represented client side by an object that looks like:
{
"$id":"fbObj",
"$priority":null,
"$value":null
}
And then later if non-primitive content is set at that reference, the $value key disappears and is replaced by the content:
{
"$id":"fbObj",
"$priority":null,
"someKey":{
"anotherKey":"someValue"
}
}
What's the best way to check against the existence of the $firebaseObject? Here's what I'm doing but it feels clunky. Maybe this is an opportunity to add an $exists key to the AngularFire API.
<div ng-show="myObjExists()">{{myObj}}</div>
...
$scope.myObjExists = function(){
return !(
$scope.myObj.hasOwnProperty("$value")
&& $scope.myObj.$value === null
);
};

Resources