-
Notifications
You must be signed in to change notification settings - Fork 2
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
update logic & jsx rule, add refactors directory, with logic and jsx refactor #84
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1621,49 +1621,189 @@ In addition to `render()` being the _last_ method in a component (see [Method or | |
|
||
### Logic and JSX | ||
|
||
React and JSX supporting logic and markup in the same file allows for substantial complexity in markup generation over other traditional templating languages (like [handlebars](http://handlebarsjs.com)). But with that increased complexity can come a decrease in readability. | ||
> **Note:** This rule was changed from a previous version that said the | ||
> opposite, read the [refactoring](../refactors/logic-and-jsx) that explains why. | ||
|
||
In order to maximize both complexity and readability, we suggest keeping all logic out of JSX, except variable references and method calls. Expressions, particularly ternary expressions, should be stored in variables outside of JSX. | ||
A single component's returned JSX code can get quite complex with lots of | ||
expressions. But instead of pulling those expressions out into variables and | ||
statements, you should focus on pulling out parts of the JSX into seperate | ||
components, and some of the logic into utility functions. | ||
|
||
```js | ||
// good | ||
render() { | ||
let {includeHeader} = this.props; | ||
let buttons = [1, 2, 3, 4, 5].map((page) => ( | ||
<Button key={page} onClick={this._handlePageClick.bind(this, page)} /> | ||
)); | ||
let header; | ||
// good (inlining all JSX/simple logic and abstracting complex JSX into separate components) | ||
function Component1(props) { | ||
return <SomeComponent kind="gah" foo={props.foo} baz={props.baz} />; | ||
} | ||
|
||
function Component2(props) { | ||
return <SomeComponent kind="goop" bar={props.bar} baz={props.baz} />; | ||
} | ||
|
||
class MyComponent extends React.Component { | ||
render() { | ||
return ( | ||
<div> | ||
{cond && otherCond && ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this reads very well. |
||
<Component1 foo={this.props.foo} baz={this.state.baz} /> | ||
)} | ||
{cond && !otherCond && ( | ||
<Component2 bar={this.props.bar} baz={this.state.baz} /> | ||
)} | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
if (includeHeader) { | ||
header = (<h2>Pagination</h2>); | ||
// bad (hard to refactor into seperate components / moves JSX around) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this is what we agreed to. I think this is swinging the pendulum to the other side. It's exchanging one group's preferences for another IMO. I think the spirit of the rule is to limit a whole bunch of logic in JSX. If you wanna destructure with conditionals or access There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, which is why I included additional instructions on how to break down excessive logic in JSX.
I know, but I would rather push this right now. Considering it deeper, I don't think the middleground is actually okay. If people need further convincing, I would rather do that convincing and make this a hard rule. |
||
class MyComponent extends React.Component { | ||
render() { | ||
let { foo, bar } = this.props; | ||
let { baz } = this.state; | ||
|
||
let element1 = null; | ||
let element2 = null; | ||
|
||
if (cond) { | ||
if (otherCond) { | ||
element1 = <SomeComponent kind="gah" foo={foo} baz={baz} />; | ||
} else { | ||
element2 = <SomeComponent kind="goop" bar={bar} baz={baz} />; | ||
} | ||
} | ||
|
||
return ( | ||
<div> | ||
{element1} | ||
{element2} | ||
</div> | ||
); | ||
} | ||
} | ||
``` | ||
|
||
This might seem unnatural at first if you aren't familiar with writing React | ||
code this way. But if you start splitting JSX into logical statements, you end | ||
up with code that is hard to pull out into separate components, making your code | ||
less refactorable (and maintainable as a result). | ||
|
||
See [Helper components](#helper-components) for another way to help keep `render()` lean. | ||
|
||
#### Logic and JSX Tip #1: Complex Conditionals | ||
|
||
Sometimes inline JSX conditionals will get really complex. This can be hard to | ||
describe which logic is complex and which is not. But if it starts to become | ||
hard to read at a glance, you might want to pull that logic out into a small | ||
utility function. | ||
|
||
```js | ||
// bad (complex inline conditions) | ||
function MyComponent(props) { | ||
return ( | ||
<div> | ||
{header} | ||
{buttons} | ||
{(props.foo === CONSTANT_VALUE && props.bar !== CONSTANT_VALUE) || | ||
(props.foo !== CONSTANT_VALUE && | ||
props.bar === CONSTANT_VALUE && ( | ||
<div>Hello, I have a complex conditional render</div> | ||
))} | ||
{typeof props.baz === 'object' && | ||
props.baz !== null && | ||
!Array.isArray(props.baz) && | ||
Object.keys(props.baz).length > 1 && ( | ||
<div> | ||
Hello, I also have a complex conditional render | ||
</div> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
// bad (expressions in JSX) | ||
render() { | ||
let {includeHeader} = this.props; | ||
// good (small utils for the conditional logic that we pass info into) | ||
function xorEq(a, b, value) { | ||
if (a === value && b !== value) return true; | ||
if (a !== value && b === value) return true; | ||
return false; | ||
} | ||
|
||
function isPlainObject(val) { | ||
return typeof val === 'object' && val !== null && !Array.isArray(val); | ||
} | ||
|
||
function isEmptyObject(obj) { | ||
return Object.keys(obj).length === 0 | ||
} | ||
|
||
function MyComponent(props) { | ||
return ( | ||
<div> | ||
{includeHeader ? (<h2>Pagination</h2>) : null} | ||
{[1, 2, 3, 4, 5].map((page) => ( | ||
<Button key={page} onClick={this._handlePageClick.bind(this, page)} /> | ||
))} | ||
{xorEq(props.foo, props.bar, CONSTANT_VALUE) && ( | ||
<div>Hello, I have a complex conditional render</div> | ||
)} | ||
{isPlainObject(props.baz) && !isEmptyObject(props.baz) && ( | ||
<div>Hello, I also have a complex conditional render</div> | ||
)} | ||
</div> | ||
); | ||
} | ||
``` | ||
|
||
The above "bad" example doesn't seem so bad right? But as we know, code tends to grow over time. If we add more expressions, add more markup to the header, or the map gets more more logic, the code will become unwieldy. Setting up this guideline, even in the most simple examples, helps set the code along the right path. | ||
#### Logic and JSX Tip #2: Computed Data | ||
|
||
See [Helper components](#helper-components) for another way to help keep `render()` lean. | ||
Sometimes when you're writing JSX you'll start to compute data inline using | ||
functions like `array.map/reduce/filter/etc`. However, it's better to pull that | ||
logic out into separate functions, and store the data in variables before the | ||
JSX code. | ||
|
||
```js | ||
// bad (complex computed data written inline) | ||
function MyComponent(props) { | ||
return ( | ||
<ul> | ||
{Object.keys(props.items) | ||
.reduce((items, key) => { | ||
return [...items, props.items[key]]; | ||
}, []) | ||
.map((item, index) => { | ||
return { ...item, index }; | ||
}) | ||
.filter(item => { | ||
return matchFields(item.fields, props.searchQueryFields); | ||
}) | ||
.map(item => { | ||
return <div key={item.id}>...</div>; | ||
})} | ||
</ul> | ||
); | ||
} | ||
|
||
// good (computations pulled out into their own functions, stored in variables before JSX) | ||
function convertToItemsArray(items) { | ||
return Object.keys(items) | ||
.reduce((arr, key) => { | ||
return [...arr, items[key]]; | ||
}, []) | ||
.map((item, index) => { | ||
return { ...item, index }; | ||
}); | ||
} | ||
|
||
function filterItems(items, searchQueryFields) { | ||
return items.filter(item => { | ||
return matchFields(item.fields, searchQueryFields); | ||
}); | ||
} | ||
|
||
function MyComponent(props) { | ||
let items = convertToItemsArray(props.items); | ||
let filteredItems = filterItems(items, props.searchQueryFields); | ||
return ( | ||
<ul> | ||
{filteredItems.map(item => { | ||
return <div key={item.id}>...</div>; | ||
})} | ||
</ul> | ||
); | ||
} | ||
``` | ||
|
||
**[⬆ back to top](#table-of-contents)** | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Eventbrite JavaScript Refactors | ||
|
||
> Guided refactorings of real (but redacted) Eventbrite Frontend code. | ||
|
||
## [1. Logic and JSX](./logic-and-jsx) | ||
|
||
> Refactoring a large `render()` method with lots of long if-then conditional | ||
> logic into separate components and inline JSX logic. (Also: Reasons why | ||
> destructuring is bad for refactoring code). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a powerful. I once had a senior engineer admonish me that every new variable introduced is additional mental overhead and complexity for future devs. Using dot notation to reference object values provides clear context for where the value originates. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think these sorts of things are subjective and there are very good arguments on both sides. If you need to break up a render into multiple components then multiple class components then having However, I've also had cases where I migrated a chunk of And if you're just looking at markup and variables (instead of dot notation) some people find that easier to parse than others. I think we waste to pushing what we find easier to understand on the masses when there isn't an objective benefit to either way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True, I have seen both approaches cause hard-to-understand code. I'm loving hook-based state management in my personal projects which makes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I've been thinking about this all weekend because I've been considering removing destructuring from the programming language I'm designing. I can't come up with a case where I think destructuring improves code or a place where it is necessary.
It may be ever so slightly easier to read, but objectively speaking you do have less information when you write code this way. At this point, I think I group together destructuring with other language features of JavaScript I never recommend using, even if you stylistically prefer it. |
||
|
||
**Before:** | ||
|
||
```js | ||
class MyComponent extends React.Component { | ||
render() { | ||
let { foo, bar } = this.props; | ||
let { baz } = this.state; | ||
|
||
let element1 = null; | ||
let element2 = null; | ||
|
||
if (cond) { | ||
if (otherCond) { | ||
element1 = <SomeComponent kind="gah" foo={foo} baz={baz} />; | ||
} else { | ||
element2 = <SomeComponent kind="goop" bar={bar} baz={baz} />; | ||
} | ||
} | ||
|
||
return ( | ||
<div> | ||
{element1} | ||
{element2} | ||
</div> | ||
); | ||
} | ||
} | ||
``` | ||
|
||
**After:** | ||
|
||
```js | ||
function Component1(props) { | ||
return <SomeComponent kind="gah" foo={props.foo} baz={props.baz} />; | ||
} | ||
|
||
function Component2(props) { | ||
return <SomeComponent kind="goop" bar={bar} baz={baz} />; | ||
} | ||
|
||
class MyComponent extends React.Component { | ||
render() { | ||
return ( | ||
<div> | ||
{cond && otherCond && ( | ||
<Component1 foo={this.props.foo} baz={this.state.baz} /> | ||
)} | ||
{cond && !otherCond && ( | ||
<Component2 bar={this.props.bar} baz={this.state.baz} /> | ||
)} | ||
</div> | ||
); | ||
} | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was there supposed to be destructuring here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, no... I wanted it to be
<SomeComponent kind="goop" bar={props.bar} baz={props.baz} />