14 tips for writing clean React code. Part 1

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:



 //   async function authenticate({ user_id, token }) {  try {    const response = await axios.post('https://someapi.com/v1/auth/', {      user_id,      token,    })    console.log(response)    return response.data  } catch (error) {    console.error(error)    throw error  } } //   async function authenticate({ user_id, jwt_token, token = jwt_token }) {  try {    const response = await axios.post('https://someapi.com/v1/auth/', {      user_id,      token,    })    console.log(response)    return response.data  } catch (error) {    console.error(error)    throw error  } }
      
      





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:





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:





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:





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:



 //     function Gallery({ title, images = [], ...otherProps }) {  return (    <CarouselContext.Provider>      <Carousel>        {images.map((src, index) => (          <img align="center" src={src} key={`img_${index}`} />        ))}      </Carousel>    </CarouselContext.Provider>  ) } //         const Gallery = ({ title, images = [], ...otherProps }) => (  <CarouselContext.Provider>    <Carousel>      {images.map((src, index) => (        <img align="center" src={src} key={`img_${index}`} />      ))}    </Carousel>  </CarouselContext.Provider> )
      
      





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:



 //     function GalleryPage(props) {  return <Gallery {...props} /> } //         const GalleryPage = (props) => <Gallery {...props} />
      
      





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:



  1. Import and export teams.
  2. 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:



 //  #1 const useApp = ({ data: dataProp = null }) => {  const [data, setData] = React.useState(dataProp)  React.useEffect(() => {    setData(data)  }, [])  return {    data,  } } //  #2 const basicController = ({ device: deviceProp }) => {  const [device, setDevice] = React.useState(deviceProp)  React.useEffect(() => {    if (!device && deviceProp) {      setDevice(deviceProp === 'mobile' ? 'mobile' : 'desktop')    }  }, [deviceProp])  return {    device,  } }
      
      





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?






All Articles