14 conseils pour écrire du code React propre. Partie 1

Écrire du code propre est une compétence qui devient obligatoire à un certain stade de la carrière d'un programmeur. Cette compétence est particulièrement importante lorsque le programmeur essaie de trouver son premier emploi. C'est essentiellement ce qui fait du développeur un joueur d'équipe et quelque chose qui peut soit «remplir» l'entrevue, soit l'aider à réussir. Les employeurs, lorsqu'ils prennent des décisions en matière de personnel, consultent le code rédigé par leurs employés potentiels. Le code que le programmeur écrit doit être compris non seulement par les machines, mais aussi par les personnes.



Le matériel, dont nous publions aujourd'hui la première partie de la traduction, présente des conseils pour écrire du code propre pour les applications React. La pertinence de ces conseils est d'autant plus élevée que la taille du projet dans lequel les principes qui y sont énoncés sont appliqués est grande. Dans les petits projets, vous pouvez probablement vous passer de l'application de ces principes. Lorsque vous décidez de ce qui est nécessaire dans chaque situation particulière, il vaut la peine d'être guidé par le bon sens.

1. Propriétés de destruction


Les propriétés destructrices (dans la terminologie React en anglais, elles sont appelées «accessoires») sont un bon moyen de rendre le code plus propre et d'améliorer ses capacités de support. Le fait est que cela vous permet d'exprimer ou de déclarer clairement ce qu'une entité utilise (comme le composant React). Cependant, cette approche n'oblige pas les développeurs à lire dans l'implémentation du composant afin de connaître la composition des propriétés qui lui sont associées.

Les propriétés de destruction permettent également au programmeur de définir leurs valeurs par défaut. Ceci est assez courant:

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 

L'une des conséquences les plus agréables de l'utilisation de la déstructuration en JavaScript, que j'ai pu trouver, est qu'elle vous permet de prendre en charge diverses options de paramètres.

Par exemple, nous avons une fonction authenticate , qui a pris comme paramètre le token utilisé pour authentifier les utilisateurs. Plus tard, il a fallu lui faire accepter l'entité jwt_token . Ce besoin était dû à un changement dans la structure de la réponse du serveur. Grâce à l'utilisation de la déstructuration, vous pouvez facilement organiser la prise en charge des deux paramètres sans avoir à gérer la nécessité de modifier la plupart du code de fonction:

 //   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  } } 

L' jwt_token sera évaluée lorsque le code atteindra le token . En conséquence, si jwt_token se révèle être un jeton valide et que l'entité de token s'avère undefined , la valeur de jwt_token tombera dans le token . Si dans le token il y avait déjà une valeur qui n'était pas fausse par les règles JS (c'est-à-dire un vrai jeton), alors dans le token aura simplement ce qui est déjà là.

2. Placer les fichiers de composants dans une structure de dossiers bien pensée


Jetez un œil à la structure de répertoires suivante:

  • src

    • composants
    • Breadcrumb.js
    • CollapsedSeparator.js
  • Entrée

    • index.js
    • Input.js
    • utils.js
    • focusManager.js
  • Carte

    • index.js
    • Card.js
    • CardDivider.js
  • Button.js
  • Typography.js

Le fil d'Ariane peut inclure des séparateurs. Le composant CollapsedSeparator est importé dans le fichier Breadcrumb.js . Cela nous permet de savoir que dans la mise en œuvre du projet en question, ils sont liés. Cependant, une personne qui ne possède pas ces informations peut suggérer que Breadcrumb et CollapsedSeparator sont une paire de composants complètement indépendants qui ne sont en aucun cas connectés les uns aux autres. Surtout - si CollapsedSeparator aucun signe clair que ce composant est associé au composant Breadcrumb . Parmi ces signes, par exemple, il peut y avoir un préfixe Breadcrumb utilisé dans le nom du composant, qui peut transformer le nom en quelque chose comme BreadcrumbCollapsedSeparator.js .

Comme nous savons que Breadcrumb et CollapsedSeparator sont liés l'un à l'autre, nous pouvons nous demander pourquoi ils ne sont pas placés dans un dossier séparé, comme Input et Card . Dans le même temps, nous pouvons commencer à faire diverses hypothèses sur la raison pour laquelle les matériaux du projet ont juste une telle structure. Disons, ici, vous pouvez penser à ce que ces composants ont été placés au niveau supérieur du projet afin de les aider à trouver rapidement ces composants, en prenant soin de ceux qui travailleront avec le projet. En conséquence, la relation entre les parties du projet semble plutôt vague pour le nouveau développeur. L'utilisation de techniques d'écriture de code propres devrait avoir exactement l'effet inverse. Le fait est que grâce à eux, le nouveau développeur a la possibilité de lire le code de quelqu'un d'autre et de saisir instantanément l'essence de la situation.

Si nous utilisons une structure de répertoires bien pensée dans notre exemple, nous obtenons quelque chose comme ceci:

  • src

    • composants
  • Fil d'Ariane

    • index.js
    • Breadcrumb.js
    • CollapsedSeparator.js
  • Entrée

    • index.js
    • Input.js
    • utils.js
    • focusManager.js
  • Carte

    • index.js
    • Card.js
    • CardDivider.js
  • Button.js
  • Typography.js

Maintenant, peu importe le nombre de composants associés au composant Breadcrumb qui seront créés. Tant que leurs fichiers se trouvent dans le même répertoire que Breadcrumb.js , nous saurons qu'ils sont associés au composant Breadcrumb :

  • src

    • composants
  • Fil d'Ariane

    • index.js
    • Breadcrumb.js
    • CollapsedSeparator.js
    • Expander.js
    • BreadcrumbText.js
    • Breadcrumbhothotog.js
    • Breadcrumbfishes.js
    • Breadcrumbleftft.js
    • Breadcrumbhead.js
    • Breadcrumbaddict.js
    • Breadcrumbdragon0814.js
    • Breadcrumbcontext.js
  • Entrée

    • index.js
    • Input.js
    • utils.js
    • focusManager.js
  • Carte

    • index.js
    • Card.js
    • CardDivider.js
  • Button.js
  • Typography.js

Voici à quoi ressemble le travail avec des structures similaires dans le 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. Nommer les composants à l'aide des conventions de dénomination standard


L'utilisation de certaines normes lors de la dénomination des composants permet à une personne qui n'est pas l'auteur du projet de lire facilement le code de ce projet.

Par exemple, les noms des composants d'ordre supérieur (HOC) sont généralement précédés du préfixe. De nombreux développeurs sont habitués à ces noms de composants:

 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 

Supposons que quelqu'un décide de prendre du recul par rapport à cette pratique et procédez comme suit:

 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 

Il s'agit d'un code JavaScript parfaitement fonctionnel. Les noms ici sont composés, d'un point de vue technique, à droite. Mais le préfixe use est habituel à utiliser dans d'autres situations, notamment lors du nommage des hooks React . Par conséquent, si quelqu'un écrit un programme qu'il envisage de montrer à quelqu'un d'autre, il doit faire attention aux noms des entités. Cela est particulièrement vrai dans les cas où quelqu'un demande à voir son code et à l'aider à résoudre un problème. Le fait est que quelqu'un qui lit le code de quelqu'un d'autre, très probablement, est déjà habitué à un certain schéma de dénomination d'entité.

Les écarts par rapport aux normes généralement acceptées rendent difficile la compréhension du code de quelqu'un d'autre.

4. Évitez les pièges booléens


Le programmeur doit faire preuve d'une extrême prudence dans le cas où certaines sorties dépendent de certaines valeurs logiques primitives, et certaines décisions sont prises en fonction de l'analyse de ces valeurs. Cela fait allusion à la mauvaise qualité du code. Cela oblige les développeurs à lire le code d'implémentation de composants ou d'autres mécanismes pour avoir une idée précise du résultat exact de ces mécanismes.

Supposons que nous avons créé un composant Typography qui peut accepter les options suivantes: 'h1' , 'h2' , 'h3' , 'h4' , 'h5 ', 'h6' , 'title' , 'subheading' .

Qu'est-ce qui affectera exactement la sortie d'un composant si les options lui sont transmises sous la forme suivante?

 const App = () => (  <Typography color="primary" align="center" subheading title>    Welcome to my bio  </Typography> ) 

Ceux qui ont une certaine expérience avec React (ou plutôt avec JavaScript) peuvent déjà supposer que l'option title subheading option de subheading raison du fonctionnement du système. La dernière option remplacera la première.

Mais le problème ici est que nous ne pouvons pas, sans regarder dans le code, dire exactement dans quelle mesure l'option de title ou l'option de subheading - subheading sera appliquée.

Par exemple:

 .title {  font-size: 1.2rem;  font-weight: 500;  text-transform: uppercase; } .subheading {  font-size: 1.1rem;  font-weight: 400;  text-transform: none !important; } 

Même si le title emporte, la règle CSS text-transform: uppercase ne s'applique pas. Cela est dû à la spécificité plus élevée de la text-transform: none !important Règle text-transform: none !important qui existe dans le subheading . Si vous ne faites pas preuve de prudence dans de telles situations, le débogage de telles erreurs dans les styles peut devenir extrêmement difficile. Surtout - dans les cas où le code n'affiche pas certains avertissements ou messages d'erreur dans la console. Cela peut compliquer la signature du composant.

Une solution possible à ce problème consiste à utiliser une version plus propre du composant Typography :

 const App = () => <Typography variant="title">Welcome to my bio</Typography> 

Voici le code du composant Typography :

 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>  ) } 

Maintenant, lorsque dans le composant App , nous passons au composant Typography variant="title" , nous pouvons être sûrs que seul le title affectera la sortie du composant. Cela nous évite d'avoir à analyser le code du composant afin de comprendre à quoi ressemblera ce composant.

Pour travailler avec des propriétés, vous pouvez utiliser la construction 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 

Mais la principale force de cette approche est que vous pouvez simplement utiliser le design simple ligne suivant et y mettre fin:

 const result = styles[variant] 

5. Utilisez les fonctions fléchées


Les fonctions fléchées représentent un mécanisme concis et clair pour déclarer des fonctions en JavaScript (dans ce cas, il serait plus correct de parler de l'avantage des fonctions fléchées par rapport aux expressions fonctionnelles).

Cependant, dans certains cas, les développeurs n'utilisent pas de fonctions fléchées au lieu d'expressions fonctionnelles. Par exemple, quand il est nécessaire d'organiser l'augmentation des fonctions.

React utilise ces concepts de manière similaire. Cependant, si un programmeur n'est pas intéressé à augmenter les fonctions, alors, à mon avis, il est logique d'utiliser la syntaxe des fonctions fléchées:

 //     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> ) 

Il convient de noter qu'en analysant cet exemple, il est difficile de voir les forces des fonctions fléchées. Leur beauté se manifeste pleinement lorsqu'il s'agit de conceptions simples à une seule ligne:

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

Je suis sûr que ces conceptions unifilaires plairont à tout le monde.

6. Placez des fonctions indépendantes en dehors de vos propres crochets


J'ai vu comment certains programmeurs déclarent des fonctions dans leurs propres hooks, mais ces hooks n'ont pas particulièrement besoin de telles fonctions. Ce type "gonfle" légèrement le code du crochet et complique sa lecture. Des difficultés de lecture du code surviennent du fait que ses lecteurs peuvent commencer à se demander si le crochet dépend vraiment de la fonction qui s'y trouve. Si ce n'est pas le cas, il vaut mieux déplacer la fonction hors du crochet. Cela donnera au lecteur de code une compréhension claire de ce dont dépend le crochet et de ce qu'il ne fait pas.

Voici un exemple:

 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 

Si nous analysons ce code, nous pouvons comprendre que les fonctions removeFalseyImages , en fait, n'ont pas besoin d'être présentes à l'intérieur du crochet; il n'interagit pas avec son état, ce qui signifie qu'il peut être placé à l'extérieur et peut être appelé à partir du crochet sans aucun problème.

7. Soyez cohérent lorsque vous écrivez du code


Une approche cohérente pour écrire du code est quelque chose qui est souvent recommandé pour ceux qui programment en JavaScript.

Dans le cas de React, il convient de prêter attention à une approche cohérente de l'utilisation des conceptions suivantes:

  1. Équipes d'importation et d'exportation.
  2. Dénomination des composants, des crochets, des composants d'ordre supérieur, des classes.

Lors de l'importation et de l'exportation de composants, j'utilise parfois quelque chose de similaire au suivant:

 import App from './App' export { default as Breadcrumb } from './Breadcrumb' export default App 

Mais j'aime aussi la syntaxe:

 export { default } from './App' export { default as Breadcrumb } from './Breadcrumb' 

Quoi que le programmeur choisisse, il doit l'utiliser de manière cohérente dans chaque projet qu'il crée. Cela simplifie le travail de ce programmeur et la lecture de son code par d'autres personnes.

Il est très important de respecter les conventions de dénomination des entités.

Par exemple, si quelqu'un a donné au hook le nom useApp , il est important que les noms des autres hooks soient construits de la même manière - en utilisant le préfixe use. Par exemple, le nom d'un autre hook avec cette approche peut ressembler à useController .

Si vous n'adhérez pas à cette règle, le code d'un projet peut finalement se révéler être quelque chose comme ceci:

 //  #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,  } } 

Voici à quoi ressemble l'importation de ces crochets:

 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 

À première vue, il est tout à fait évident que le basicController est un crochet, le même que useApp . Cela oblige le développeur à lire le code d'implémentation de ce qu'il importe. Cela n'est fait que pour comprendre exactement ce à quoi le développeur doit faire face. Si nous adhérons systématiquement à la même stratégie pour nommer les entités, une telle situation ne se produira pas. Tout sera clair en un coup d'œil:

 const app = useApp() const controller = useBasicController() 

À suivre ...

Chers lecteurs! Comment abordez-vous le nommage d'entités dans vos projets React?

Source: https://habr.com/ru/post/fr465811/


All Articles