Writing clean code is a skill that becomes mandatory at a certain stage in a programmer’s career. This skill is especially important when the programmer is trying to find his first job. This, in essence, is what makes the developer a team player, and something that can either “fill up” the interview, or help him successfully pass. Employers, when making personnel decisions, look at the code written by their potential employees. The code that the programmer writes should be understood not only by machines, but also by people.
The material, the first part of the translation of which we publish today, presents tips for writing clean code for React applications. The relevance of these tips is the higher, the larger the size of the project in which the principles set forth in them are applied. In small projects, you can probably do without applying these principles. When deciding what is needed in each particular situation, it is worth being guided by common sense.
1. Destructure properties
Destructuring properties (in React English terminology they are called “props”) is a good way to make the code cleaner and improve its support capabilities. The fact is that this allows you to clearly express or declare what an entity uses (like the React component). However, this approach does not force developers to read into the implementation of the component in order to find out the composition of the properties associated with it.
Destructuring properties also allows the programmer to set their default values. This is quite common:
import React from 'react' import Button from 'components/Button' const MyComponent = ({ placeholder = '', style, ...otherProps }) => { return ( <Button type="button" style={{ border: `1px solid ${placeholder ? 'salmon' : '#333'}`, ...style, }} {...otherProps} > Click Me </Button> ) } export default MyComponent
One of the most pleasant consequences of using destructuring in JavaScript, which I was able to find, is that it allows you to support various options for parameters.
For example, we have the
authenticate
function, which took as a parameter the
token
used to authenticate users. Later it was necessary to make it accept the
jwt_token
entity. This need was caused by a change in the structure of the server response. Thanks to the use of destructuring, it is possible to easily organize support for both parameters without the need to change most of the function code:
The
jwt_token
will be evaluated when the code reaches
token
. As a result, if
jwt_token
turns out to be a valid token, and the
token
entity turns out to be
undefined
, the
jwt_token
value will fall into the
token
. If in
token
there was already some value that was not false by JS rules (that is, some real token), then in
token
will simply be what is already there.
2. Place component files in a well-thought out folder structure
Take a look at the following directory structure:
- src
- components
- Breadcrumb.js
- CollapsedSeparator.js
- Input
- index.js
- Input.js
- utils.js
- focusManager.js
- Card
- index.js
- Card.js
- CardDivider.js
- Button.js
- Typography.js
Breadcrumbs may include separators. The
CollapsedSeparator
component is imported in the
Breadcrumb.js
file. This gives us knowledge that they are connected in the implementation of the project in question. However, someone who does not own this information may suggest that
Breadcrumb
and
CollapsedSeparator
are a pair of completely independent components that are not connected to each other in any way. Especially - if the
CollapsedSeparator
does not have any clear signs that this component is associated with the
Breadcrumb
component. Among such signs, for example, there may be a
Breadcrumb
prefix used in the component name, which can turn the name into something like
BreadcrumbCollapsedSeparator.js
.
Since we know that
Breadcrumb
and
CollapsedSeparator
are related to each other, we may wonder why they are not placed in a separate folder, such as
Input
and
Card
. At the same time, we can begin to make various assumptions about why the project materials have just such a structure. Let’s say, here you can think about what these components were placed on the top level of the project in order to help them quickly find these components, taking care of those who will work with the project. As a result, the relationship between the parts of the project looks rather vague for the new developer. The use of clean code writing techniques should have the exact opposite effect. The point is that thanks to them, the new developer gets the opportunity to read someone else's code and instantly grasp the essence of the situation.
If we use a well-thought out directory structure in our example, we get something like the following:
- src
- Breadcrumb
- index.js
- Breadcrumb.js
- CollapsedSeparator.js
- Input
- index.js
- Input.js
- utils.js
- focusManager.js
- Card
- index.js
- Card.js
- CardDivider.js
- Button.js
- Typography.js
Now it doesn’t matter how many components associated with the
Breadcrumb
component will be created. As long as their files are located in the same directory as
Breadcrumb.js
, we will know that they are associated with the
Breadcrumb
component:
- src
- Breadcrumb
- index.js
- Breadcrumb.js
- CollapsedSeparator.js
- Expander.js
- BreadcrumbText.js
- Breadcrumbhothotog.js
- Breadcrumbfishes.js
- Breadcrumbleftft.js
- Breadcrumbhead.js
- Breadcrumbaddict.js
- Breadcrumbdragon0814.js
- Breadcrumbcontext.js
- Input
- index.js
- Input.js
- utils.js
- focusManager.js
- Card
- index.js
- Card.js
- CardDivider.js
- Button.js
- Typography.js
This is how working with similar structures looks in code:
import React from 'react' import Breadcrumb, { CollapsedSeparator, Expander, BreadcrumbText, BreadcrumbHotdog, BreadcrumbFishes, BreadcrumbLeftOvers, BreadcrumbHead, BreadcrumbAddict, BreadcrumbDragon0814, } from '../../../../../../../../../../components/Breadcrumb' const withBreadcrumbHotdog = (WrappedComponent) => (props) => ( <WrappedComponent BreadcrumbHotdog={BreadcrumbHotdog} {...props} /> ) const WorldOfBreadcrumbs = ({ BreadcrumbHotdog: BreadcrumbHotdogComponent, }) => { const [hasFishes, setHasFishes] = React.useState(false) return ( <BreadcrumbDragon0814 hasFishes={hasFishes} render={(results) => ( <BreadcrumbFishes> {({ breadcrumbFishes }) => ( <BreadcrumbLeftOvers.Provider> <BreadcrumbHotdogComponent> <Expander> <BreadcrumbText> <BreadcrumbAddict> <pre> <code>{JSON.stringify(results, null, 2)}</code> </pre> </BreadcrumbAddict> </BreadcrumbText> </Expander> {hasFishes ? breadcrumbFishes.map((fish) => ( <> {fish} <CollapsedSeparator /> </> )) : null} </BreadcrumbHotdogComponent> </BreadcrumbLeftOvers.Provider> )} </BreadcrumbFishes> )} /> ) } export default withBreadcrumbHotdog(WorldOfBreadcrumbs)
3. Name components using standard naming conventions
Using certain standards when naming components makes it easy for someone who is not the author of the project to read the code for this project.
For example,
higher order component (HOC) names are usually prefixed
with
. Many developers are used to these component names:
import React from 'react' import hoistNonReactStatics from 'hoist-non-react-statics' import getDisplayName from 'utils/getDisplayName' const withFreeMoney = (WrappedComponent) => { class WithFreeMoney extends React.Component { giveFreeMoney() { return 50000 } render() { return ( <WrappedComponent additionalMoney={[ this.giveFreeMoney(), this.giveFreeMoney(), this.giveFreeMoney(), this.giveFreeMoney(), this.giveFreeMoney(), this.giveFreeMoney(), this.giveFreeMoney(), ]} {...this.props} /> ) } } WithFreeMoney.displayName = `withFreeMoney(${getDisplayName( WrappedComponent, )}$)` hoistNonReactStatics(WithFreeMoney, WrappedComponent) return WithFreeMoney } export default withFreeMoney
Suppose someone decides to step back from this practice and do this:
import React from 'react' import hoistNonReactStatics from 'hoist-non-react-statics' import getDisplayName from 'utils/getDisplayName' const useFreeMoney = (WrappedComponent) => { class WithFreeMoney extends React.Component { giveFreeMoney() { return 50000 } render() { return ( <WrappedComponent additionalMoney={[ this.giveFreeMoney(), this.giveFreeMoney(), this.giveFreeMoney(), this.giveFreeMoney(), this.giveFreeMoney(), this.giveFreeMoney(), this.giveFreeMoney(), ]} {...this.props} /> ) } } WithFreeMoney.displayName = `useFreeMoney(${getDisplayName( WrappedComponent, )}$)` hoistNonReactStatics(WithFreeMoney, WrappedComponent) return WithFreeMoney } export default useFreeMoney
This is perfectly functional JavaScript code. The names here are made up, from a technical point of view, right. But the
use
prefix is ​​customary to use in other situations, namely when naming
React hooks . As a result, if someone writes a program that they plan to show to someone else, he should be careful about the names of the entities. This is especially true for those cases when someone asks to see his code and help him solve a problem. The fact is that someone who will read someone else's code, quite possibly, is already used to a certain entity naming scheme.
Deviations from generally accepted standards make it difficult to understand someone else's code.
4. Avoid Boolean Traps
The programmer should exercise extreme caution if some output depends on some primitive logical values, and some decisions are made based on the analysis of these values. This hints at the poor quality of the code. This forces developers to read the code for implementing components or other mechanisms to get an accurate idea of ​​exactly what the result of these mechanisms is.
Suppose we created a
Typography
component that can take the following options:
'h1'
,
'h2'
,
'h3'
,
'h4'
,
'h5
',
'h6'
,
'title'
,
'subheading'
.
What exactly will affect the component output if the options are passed to it in the following form?
const App = () => ( <Typography color="primary" align="center" subheading title> Welcome to my bio </Typography> )
Those who have some experience with React (or, rather, with JavaScript) may already assume that the
title
option will
subheading
option due to the way the system works. The last option will overwrite the first.
But the problem here is that we cannot, without looking into the code, say exactly to what extent the
title
option or the
subheading
option will be applied.
For example:
.title { font-size: 1.2rem; font-weight: 500; text-transform: uppercase; } .subheading { font-size: 1.1rem; font-weight: 400; text-transform: none !important; }
Even though
title
wins, the CSS
text-transform: uppercase
rule will not apply. This is due to the higher specificity of the
text-transform: none !important
rule that exists in
subheading
. If you do not exercise caution in such situations, debugging such errors in styles can become extremely difficult. Especially - in cases where the code does not display some warnings or error messages to the console. This can complicate the signature of the component.
One possible solution to this problem is to use a cleaner version of the
Typography
component:
const App = () => <Typography variant="title">Welcome to my bio</Typography>
Here is the
Typography
component code:
import React from 'react' import cx from 'classnames' import styles from './styles.css' const Typography = ({ children, color = '#333', align = 'left', variant, ...otherProps }) => { return ( <div className={cx({ [styles.h1]: variant === 'h1', [styles.h2]: variant === 'h2', [styles.h3]: variant === 'h3', [styles.h4]: variant === 'h4', [styles.h5]: variant === 'h5', [styles.h6]: variant === 'h6', [styles.title]: variant === 'title', [styles.subheading]: variant === 'subheading', })} > {children} </div> ) }
Now, when in the
App
component we pass to the
Typography
component
variant="title"
, we can be sure that only the
title
will affect the output of the component. This saves us from having to analyze the component code in order to understand what this component will look like.
To work with properties, you can use the simple
if/else
:
let result if (variant === 'h1') result = styles.h1 else if (variant === 'h2') result = styles.h2 else if (variant === 'h3') result = styles.h3 else if (variant === 'h4') result = styles.h4 else if (variant === 'h5') result = styles.h5 else if (variant === 'h6') result = styles.h6 else if (variant === 'title') result = styles.title else if (variant === 'subheading') result = styles.subheading
But the main strength of this approach is that you can simply use the following clean single-line design and put an end to this:
const result = styles[variant]
5. Use arrow functions
Arrow functions represent a concise and clear mechanism for declaring functions in JavaScript (in this case, it would be more correct to talk about the advantage of arrow functions over functional expressions).
However, in some cases, developers do not use arrow functions instead of functional expressions. For example, when it is necessary to organize the raising of functions.
React uses these concepts in a similar way. However, if the programmer is not interested in raising functions, then, in my opinion, it makes sense to use the syntax of arrow functions:
It should be noted that, analyzing this example, it is difficult to see the strengths of arrow functions. Their beauty is fully manifested when it comes to simple single-line designs:
I am sure that such single-line designs will appeal to everyone.
6. Place independent functions outside your own hooks
I have seen how some programmers declare functions inside their own hooks, but at the same time, these hooks do not particularly need such functions. This slightly inflates the hook code and complicates its reading. Difficulties in reading the code arise due to the fact that its readers may start asking questions about whether the hook really depends on the function that is inside it. If this is not the case, it is better to move the function outside the hook. This will give the code reader a clear understanding of what the hook depends on and what it doesn't.
Here is an example:
import React from 'react' const initialState = { initiated: false, images: [], } const reducer = (state, action) => { switch (action.type) { case 'initiated': return { ...state, initiated: true } case 'set-images': return { ...state, images: action.images } default: return state } } const usePhotosList = ({ imagesList = [] }) => { const [state, dispatch] = React.useReducer(reducer, initialState) const removeFalseyImages = (images = []) => images.reduce((acc, img) => (img ? [...acc, img] : acc), []) React.useEffect(() => { const images = removeFalseyImages(imagesList) dispatch({ type: 'initiated' }) dispatch({ type: 'set-images', images }) }, []) return { ...state, } } export default usePhotosList
If we analyze this code, we can understand that the
removeFalseyImages
functions, in fact, do not have to be present inside the hook; it does not interact with its state, which means that it can be placed outside it and can be called from the hook without any problems.
7. Be consistent when writing code
A consistent approach to writing code is something that is often recommended for those who program in JavaScript.
In the case of React, you should pay attention to a consistent approach to the use of the following designs:
- Import and export teams.
- Naming of components, hooks, higher order components, classes.
When importing and exporting components, I sometimes use something similar to the following:
import App from './App' export { default as Breadcrumb } from './Breadcrumb' export default App
But I like the syntax too:
export { default } from './App' export { default as Breadcrumb } from './Breadcrumb'
Whatever the programmer chooses, he should consistently use this in every project he creates. This simplifies the work of this programmer and the reading of his code by other people.
It is very important to adhere to naming conventions of entities.
For example, if someone gave the hook the name
useApp
, it is important that the names of other hooks be constructed in a similar way - using the use prefix. For example, the name of another hook with this approach may look like
useController
.
If you do not adhere to this rule, then the code of a certain project, in the end, may turn out to be something like this:
Here's what the import of these hooks looks like:
import React from 'react' import useApp from './useApp' import basicController from './basicController' const App = () => { const app = useApp() const controller = basicController() return ( <div> {controller.errors.map((errorMsg) => ( <div>{errorMsg}</div> ))} </div> ) } export default App
At first glance, it’s completely unobvious that the
basicController
is a hook, the same as
useApp
. This forces the developer to read the implementation code of what he imports. This is done only in order to understand what exactly the developer is dealing with. If we consistently adhere to the same strategy for naming entities, then such a situation will not arise. Everything will be clear at a glance:
const app = useApp() const controller = useBasicController()
To be continued…
Dear readers! How do you approach entity naming in your React projects?