-
Notifications
You must be signed in to change notification settings - Fork 3k
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
[Tracking] Expensify/App cleanup + style guide + README improvements #5984
Comments
Can this be aided with any linting rules? |
We should definitely try to use linting rules whenever necessary, but for a vast majority of these, there are no lint rules for them. |
I like the idea of looking into some custom lint rules for things like using an array method instead of |
There are existing eslint rules for
There are rules against using native array methods like foreach, filter, map, reduce and so on. Though then won't be able to suggest to use Here are some more rules that I think match Expensify current convention |
Regarding "Make sure that we are always using This is not just an Expensify/App problem, every future consumer of Onyx can have the same problem as mixing In that case IMO it would be best to expose only one method for updating Onyx: #5930 (comment) |
|
To prevent this the action should not return a promise |
Perhaps out-of-scope, but I've found that there's a lack of consistency in the order in which modules are imported. I would love to impose the Also we should nix Edit: eslint-plugin-import seems like maybe a better option than |
@kidroca Thanks for those insights! I think some of those eslint rules definitely make sense for us to add.
That's a good point. I guess we can keep discussing on that issue, but sounds like the race condition issue might not change whether we should prefer
This gets a little tricky sometimes since when actions are referenced by other actions we sometimes need them to return promises, but I like where that idea is going. Probably in those cases we are missing opportunities to move more logic to the API layer. |
@roryabraham that's come up a few times so I think we should just do it! |
I think this infographic is really helpful! Nice job! Questions:
|
@Luke9389 Good points thanks!
Good question. I think the guidance could maybe be clearer in the README. An optimistic response is a kind of UI enhancement which ensures that views update fast without waiting for a response back from the server. This is useful when we are creating, updating, or deleting something. So, if we fail to create or update data it should also be "rolled back" in some cases. But in others we will keep trying to make the change until it succeeds e.g. report comments with spotty network connection. |
I would love to see the sorted imports. |
If enough people want to sort the imports then we should do it! Personally, I find arbitrarily forcing things to be alphabetized leads to tedious manual style fixes and not sure if it's worth it. Auto-fixing might solve that concern for me, but auto-fixing isn't going to be a part of every contributors workflow. |
Ah bummer so _.get(someObject, 'property.nestedProperty', 'default'); It will do arrays, but not single string with |
Another suggestion for something we could standardize + add to the style guide:
// Bad
export const X = 'Hello';
export const Y = 'world!';
// Good
const X = 'Hello';
const Y = 'world!';
export {
X,
Y,
}; That way, it's easy to see all the named exports in a module. We are prettymuch standardized on that but might be good to document it anyways because I just saw it come up in a PR review. |
Unsure how others feel about this, but one thing that could be better enforced is having "actions" always use a specific syntax. Maybe even call these files import * as PolicyActions from './lib/actions/PolicyActions';
PolicyActions.create(); Compare this with a file where we are doing something like this import {create as createPolicy} from './lib/actions/Policy';
createPolicy(); I generally find it much harder to resolve where these thing happen in the second example. It's much easier to global search for something like Edit: Also seems like a place where there is a subtle lack of consistency
|
Great suggestion @marcaaron! Let's standardize on this syntax: import * as PolicyActions from './lib/actions/PolicyActions'; |
Been thinking about this one a bit...
I am proposing something like this for the docs
I might even go so far as to say that
If you have something simple I think there is virtually no reason to use |
This seems like one more reason to have a single method for updates - it can work with primitive and referential values and handle them appropriately instead of trying to explain that in the documentation where it could be missed or forgotten Another thing that seems odd is that |
RE: I think one of the biggest reasons we've always opted for importing specific things is to take advantage of "tree-shaking" technology that would keep from importing everything. However, in practice we don't write code that isn't used somewhere, so the tree-shaking isn't something we need. Just some things to consider! For the merge vs. set discussion, I haven't really weighed in because I was interested to see where it would go. I think that when there are two methods, we will always struggle with not only advising which to use, but also enforcing it. I think throwing an error based on value type is a good enforcement method, but I am not totally sold on value type being the reason to choose one over the other. I think that's a hard determination for someone to make because the same piece of data could be stored in multiple ways (ie I like @kidroca's proposal of a unified method because I think that makes the usages much more explicit (and also a bonus if it solves race conditions, but that's a separate discussion). It is easier for someone to pass ANYTHING as a value, and then they just need to decide the update strategy (which is really the thing that matters). |
That's a good point. I would also be fine with adopting the practice where all modules with "named exports only should" have a single import since it makes code more readable and we are not really benefiting from tree shaking when referencing our own code. Tree-shaking benefit seems to imply there is dead code (and we shouldn't have any in theory 🤞). |
I posted a PR to implement my above suggestion banning props/state destructuring. |
Another suggestion ... no ternaries in JSX: // Bad
const MyBadComponent = (props) => (
<>
{props.shouldRenderA
? <A />
: null}
{props.shouldRenderB
? <B />
: null}
</>
);
// Good
const MyGoodComponent = (props) => (
<>
{shouldRenderA && <A />}
{shouldRenderB && <B />}
</>
); |
I definitely agree with your bad example, but I think there is still a valid use case for ternaries, such as this:
|
Okay, so maybe "No ternaries in JSX where the second half of the ternary is |
Yeah, I like that. I wonder if there is ever a valid case for any ternary to have the second half be |
I guess I can find quite a few of them in our code, but maybe we just stick to JSX for now. |
Looks like there's a pre-existing eslint plugin/rule for this: https://github.com/divyagnan/eslint-plugin-ternaries/blob/master/docs/rules/no-null-ternary.md 🎉 |
I think yes, because React will try to render the left hand side in some cases e.g. if it is a {items.length && <List items={items} />} // prints 0 |
Definitely on board for 'no-null-ternaries'. By 'no ternaries in JSX' are we really saying no ternaries in the |
I think we are saying don't have them in JSX. But if you are returning const MyComponent = (props) => {
if (!props.someCondition) {
return null;
}
return (
<View>{...stuff}</View>
);
} vs. {someCondition && <MyComponent />} |
Ah OK, I think I should have been more clear. What I was wanting to clarify is that we can have ternaries in a |
@marcaaron I'd still say there is a valid case for your example pattern. I like using that pattern when data is being loaded and I don't want to display anything (including a spinner). However, outside of "data being loaded", I would agree that pattern isn't a good one. @Luke9389 I think what we're saying is to not allow ternary-nulls inside the part that is returned from render():
.jsx files are only used in web-e, but the same rule would apply in that repo. |
IMO early returns in Check this thread: https://expensify.slack.com/archives/C01GTK53T8Q/p1636393736077100 Let's say we have a code like this componentDidUpdate(prevProps) {
if (!prevProps.visible && this.props.visible) {
this.input.focus();
}
} we're now forced to change the signature to early return componentDidUpdate(prevProps) {
if (prevProps.visible || !this.props.visible) {
return;
}
this.input.focus();
} But if we need to include another side effect we'll have to get rid of the early return and revert the code componentDidUpdate(prevProps) {
if (!prevProps.visible && this.props.visible) {
this.input.focus();
}
if (prevProps.message.length < this.props.messageLength) {
this.doSomething();
}
} Early return can be handy for functions that return something (e.g. other than void/undefined) But for code that is causing side effects it can be confusing Sometimes more side effects would be added and they might need to run concurrently (like in my last example) in that case the early return code will be modified and a bug can be introduced if it's not inverted correctly |
I can definitely see how someone could make that mistake, but unsure if this is actually a problem we've run into apart from people having opinions about it. A few ideas for solving your concern might be...
e.g. componentDidUpdate(prevProps) {
if (!this.canFocusInput()) {
return;
}
this.input.focus();
}
canFocusInput() {
return !prevProps.visible && this.props.visible;
} Will just need to be changed to... componentDidUpdate(prevProps) {
if (this.canFocusInput()) {
this.input.focus();
}
// other side effects...
} |
An early return is suitable for functions that aren't supposed to trigger side effects like When we want to trigger a side effect we should strive for clarity for the exact conditions that trigger the side effect
If this code: componentDidUpdate(prevProps) {
if (!this.canFocusInput(prevProps)) {
return;
}
this.input.focus();
} Would have to turn into this code: componentDidUpdate(prevProps) {
if (this.canFocusInput(prevProps)) {
this.input.focus();
}
if (this.shouldDoSomething(prevProps)) {
this.doSomething();
}
} Then simply start with componentDidUpdate(prevProps) {
if (this.canFocusInput(prevProps)) {
this.input.focus();
}
} |
There are cases where we'd like to skip all side effects - an early return comes naturally and conveys that well componentDidUpdate(prevProps) {
if (this.props.disabled) {
return;
}
if (this.canFocusInput(prevProps)) {
this.input.focus();
}
} IMO that's the only case where an early return is preferable in a side effects producing code |
Let's wait and see if this causes any issues. It should not be hard to modify the custom rule to skip when inside a |
Do you see it when eslint runs in a GH action? Or just locally? |
Locally |
Ah hmm interesting. Just checked to be sure and doesn't complain for me. Perhaps there is something specific to your environment that causes the rule to show errors in your IDE. |
Looks like there is only one thing left to cover here which is:
Unsure if we have really landed on the exact guidance for that and the discussion can continue to happen here. But I think we accomplished a lot here and can close this out. Thanks for everyone's help! |
I've traced the problem to file path checks and path separators /**
* @param {String} filename
* @returns {Boolean}
*/
function isInLibs(filename) {
return filename.includes('/src/libs/');
} To make it platform agnostic it should be const path = require('path');
/**
* @param {String} filename
* @returns {Boolean}
*/
function isInLibs(filename) {
const prefix = path.normalize('/src/libs/');
return filename.includes(prefix);
} Alternatively all separators can be replaced with |
Problem
Expensify/App, Onyx, and our JS styles are inconsistently enforced in the
Expensify/App
repo. This can happen for many reasons, but one obvious one is that we just haven't written down in our style guide clear examples.Why is this important?
Incorrect patterns will create more incorrect patterns and general confusion about the "right way" to make a change or implement a feature. Every Expensify contributor should ideally have what they need to be able to champion our code styles and philosophies when reviewing any contribution (internal or external). But this can be difficult without a clear guide to refer to.
Solution
Let's do a quick audit of various things that need clarification and clean them up by creating an issue for each best practice.
The assignee will:
Expensify/App
codebase to eliminate any inconsistenciesHere's a list of things that need to be addressed and can be broken out into other issues:
Onyx
directly inside a view #5998compose()
with only a single method #6000displayName
properties so that functional components all have it and class components do not #6002propTypes
imports not usIng camelCase #6004_
#5989import * as Something
when modules only have named exports #6072Onyx.merge()
overOnxy.set()
except for when absolutely necessaryFeel free to suggest other things to this list!
The text was updated successfully, but these errors were encountered: