Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selectors everywhere #2507

Merged
merged 4 commits into from Feb 8, 2018
Merged

Conversation

theengineear
Copy link
Contributor

@theengineear theengineear commented Feb 8, 2018

In response to some comments @rgbkrk left in #2500 I used selectors in mapStateToProps. I also couldn't help myself and added a commit in here that updates some usage of mapDispatchToProps. I think the latter change is an overall improvement. However, I don't know how cleverly/simply the mapDispatchToProps arg is handled in connect. It could potentially be a perf issue to re-create all the functions in mapDispatchToProps... just wanted to draw that out.

Also, I think I found a test that was yielding false positives. You'll see it skipped in the diff. Here's an issue I opened about it:

#2506

Just to reiterate, the goal of these refactors are to ensure that our code doesn't depend on the structure of our state 🌲. We want to reorganize the state and add useful things like app refs to our state (see the plan.md file for details on that).

test("handles a focus to next cell keypress on a sticky cell", () => {

// TODO: This test was silently broken. It was loudly found during a refact.
test.skip("handles a focus to next cell keypress on a sticky cell", () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's fine for now. We probably want to tear out the sticky cells at this point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not gonna tear them out in this PR, but I'm for doing it if we think that folks will not be terribly bothered.

Speaking of knowing if folks are terribly bothered, do we have any plans on trying to get anonymized usage data from folks? It'd be cool if we could see how many people are actually sticking cells. That's a whole can of worms, but I'm curious if it's been discussed?

Copy link
Member

@rgbkrk rgbkrk Feb 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! We ran an informal poll on twitter in October of 2017.


Do you use "pin cell" in nteract desktop?

# % Choice
5 26.3% Never, it's so not for me
5 26.3% Once for funsies
7 36.8% Hardly ever
2 10.5% All the time, it's my jam

19 respondents


I'm willing to say, based on those results, that it would be ok to remove it in favor of maintainability. We can always keep the component + styling in core, just rip it out of the standalone notebook app.


describe("codeMirrorMode", () => {
test("determines the right mode from the notebook metadata", () => {
// TODO: better way to get dummy state?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On some level, I think it's ok to only use the state we're actually using for the selectors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah, the one thing I got hung up on was that it's common for use to use fromJS in tests, but that causes selectors to fail if they expect a Record and use . (dot) accessors. I changed all the selectors to use get and getIn, which is a pretty easy compromise, but It is a little odd. I guess records all have defaults though, so there's no reason you couldn't instantiate one in a test with only the bits you need filled in?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhhhhh yeah. I suppose the records make it easier, which means using makeAppRecord and makeDocumentRecord should make building a dummy simpler than having to change dummy on the fly (or rely on its values).

Copy link
Member

@rgbkrk rgbkrk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so fantastic! I made a lot of comments that I think are more for future reference, so I don't intend to block this. I'll go back and note which comments we should address first (and as part of this PR).

filename: state.document.get("filename"),
notebook: state.document.get("notebook")
cellFocused: selectors.currentFocusedCellId(state),
cellMap: selectors.currentCellMap(state),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refresh my memory, why does the notebook-menu need the whole cellMap and cellOrder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs those because it needs to do runAllCells and such:

https://github.com/nteract/nteract/blob/master/packages/core/src/components/notebook-menu/extra-handlers.js#L19

BUT, if (when) we have an epic that does this for us... we can rip these out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooooooook

I'll get that into an epic since that would help both menu implementations

@@ -80,6 +80,12 @@ export function saveContentEpic(
const state = store.getState();
const currentNotebook = selectors.currentNotebook(state);

// TODO: this will likely make more sense when this becomes less
// notebook-centric.
if (!currentNotebook) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could / probably should emit an error rather than the empty.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saving I definitely want to make sure we're propagating enough information back up, both for trust and transparency for users, and debuggability for us.

// TODO: this will likely make more sense when this becomes less
// notebook-centric.
if (!currentNotebook) {
return of(empty());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't you able to return the empty() observable as is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😬 ah, yah, you're right, this is returning an observable of an observable, so mergeMap will only shell off the outer one, leaving empty() instead of just er... nothing. thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also i double-checked this (for sanity) and yes, just empty() should be returned.

if (!cellMap) {
return empty();
}

Copy link
Member

@rgbkrk rgbkrk Feb 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're only using cell type and cell source here, I think the selector used above could be:

const source = cellSourceById(store.getState(), { id });
const type = cellTypeById(store.getState(), { id });

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating the above comment to match the use of our cell ids (which of course are in fact refs).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gonna do this after, but 💯

@@ -21,14 +22,14 @@ function mapStateToProps(
state: Object,
ownProps: CodeMirrorEditorProps
): Object {
const kernel = selectors.currentKernel(state);
const { cursorBlinkRate } = selectors.userPreferences(state);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good old silly one configurable bit for codemirror here. 😝

if (notebook) {
return notebook.getIn(["metadata", "gist_id"]);
}
return "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's disparity here in that if ["metadata", "gist_id"] is not in the notebook, this will return undefined. If the notebook wasn't defined it returns "". I'd aim to return null in this one as:

notebook.getIn(["metadata", "gist_id"], null)

return toJS(notebook);
}
return null;
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean by if we should throw here... If it's not set we should fail and know where it occurred. Do people normally allow for throwing in exceptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure! I think flow should protect us in this case when we try to do anything with the selector, so i guess I'd rather return something and then have flow remind us that it could be null? I mostly say this because the error would likely originate somewhere in reselect code, which feels like it might be odd to debug?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think returning null and handling it elsewhere is perfect, unless there's some sensible default (like with codemirror mode, etc.)

if (notebookJS) {
return stringifyNotebook(notebookJS);
}
return "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ I'm a little afraid of us accidentally saving at some $PATH an empty notebook. I think I'm a fan of throwing here (or in stringifyNotebook). Perhaps the right choice though is to handle it in the epic -- if it returns null for instance, we should take a different path. I guess maybe that's the right choice though -- we do the handling in the epic where we have an ability to inform the UI and the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I think the moral here is to return null in these cases. I don't think I want to throw in the selectors...

export const transientCellMap = createSelector(
(state: AppState) =>
state.document.getIn(["transient", "cellMap"], Immutable.Map()),
identity
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like one we'll want to do as a byRef setup 🔜, since no component ever needs all the cells.

return notebook.get("cellMap");
}
return null;
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I look forward to this being replaced by a selector which is cellByRef

// TODO: this will likely make more sense when this becomes less
// notebook-centric.
if (!currentNotebook) {
return of(saveFailed());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do wish we provided more context here, so we know that it's because of a lack of notebook.

metadata.getIn(["language_info", "codemirror_mode"]) ||
metadata.getIn(["kernel_info", "language"]) ||
metadata.getIn(["kernelspec", "language"]) ||
CODE_MIRROR_MODE_DEFAULT
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic!

@rgbkrk rgbkrk merged commit 5fb299e into nteract:master Feb 8, 2018
@rgbkrk rgbkrk added this to the Jupyter Extension M1 milestone Feb 10, 2018
@rgbkrk rgbkrk mentioned this pull request Mar 7, 2018
7 tasks
@lock
Copy link

lock bot commented May 1, 2018

Howdy! I'm 🔓🤖!

In order to keep information timely (based on the most recent release), we want all activity to be added to either new issues or open issues and PRs. In service to that goal, I, the lock bot close inactive closed issues when they haven't had activity in 120 days.

Feel free to open a new issue for related bugs and link to relevant comments from this thread.

@lock lock bot locked and limited conversation to collaborators May 1, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants