Если ваша задача — не просто научиться писать код, а понять, как стать тем, без кого поддержка и развитие проекта просто немыслимы, то этот текст для вас. Заодно поговорим о том, как помочь коллегам постичь дзен и досконально изучить структуру разрабатываемого приложения.

Всем привет, меня зовут Макс Кравец, я CEO IT-компании Holyweb, и сегодня хочу поделиться вредными советами о том, как стать незаменимым React-разработчиком. Поехали!

Волшебный спред-оператор 

Вам поставили задачу написать компонент, приветствующий пользователя. Нет ничего легче — обращаемся к объекту, который хранит имя и фамилию, и добавляем их к приветствию:

export function Welcome({name, surname}) {
  return (
    <div>
      Привет, {name} {surname}
    </div>
  )
}

Но на странице приветствие выводится в составе другого компонента, предположим, это хедер. А значит — нужно пробросить параметры. Как поступит новичок? Сядет и методично распишет все в коде родителя:

export function Header({ user }) {
  return (
    <div>
      <Welcome 
        name={user.name}
        surname={user.surname}
      />
      <div>
        Some other code...
      </div>
    </div>
  );
}

В целом — это работает. Но такой код разберет любой другой новичок, ведь движение пропсов по древу компонентов прозрачно! Кроме того, параметров может быть не два, а два десятка. Представляете, сколько времени придется потратить, сколько строк кода написать?

Ловите лайфхак:

export function Header({ user }) {
  return (
    <div>
      <Welcome {...user}/>
      <div>
        Some other code...
      </div>
    </div>
  );
}

Магия спред-оператора — код стал проще, короче, не нужно расписывать каждый параметр. А главное — никто, кроме вас, теперь не может при беглом взгляде на хедер понять, какие именно поля из объекта user на самом деле нужны в компоненте приветствия. 

Вот таким нехитрым способом можно и время сэкономить, и +100 в скилл «незаменимость» себе добавить — пусть только попробуют от вас избавиться, себе дороже выйдет! 

Техника Props Drilling 

Если писать код, беспокоясь только о реализации бизнес-логики, можно даже не заметить, как попадешь в ловушку. 

Простейшая задача: пишем составной компонент, который выводит в банковском приложении информацию о счете клиента. Тип счета задается на верхнем уровне, а отрисовывает его компонент в глубине иерархии. Создаем контекст, определяем провайдер, присваиваем значение…

export const AccountType = createContext()

export function TopComponent() {
  return (
    <AccountType.Provider value="credit">
      <MiddleComponent />
    </AccountType.Provider>

  );
}

export function MiddleComponent() {
  return (
    <LowComponent />
  );
}

export function LowComponent() {
  return (
    <DeepComponent />
  );
}

export function DeepComponent() {
  const accountType = useContext(AccountType);

  return (
    <span> {accountType} account</span>
  );
}

Код выше — воплощение цифрового эгоизма. Судите сами — вашему коллеге достаточно разобраться, где контекст провайдится, и где выводится. А вся остальная иерархия остается без внимания. Хорошо, когда все нужные компоненты выстроены в одном файле друг за дружкой, но где вы такое в реальном проекте видели? 

На помощь приходит техника Props Drilling — просто объявите нужное свойство на самом верху древа компонентов и методично прокиньте до места использования.

export function TopComponent() {
  const account = {
    accountType: "credit"
  }

  return (
      <MiddleComponent account={account} />
  );
}

export function MiddleComponent({account}) {
  return (
    <LowComponent account={account} />
  );
}

export function LowComponent({account}) {
  return (
    <DeepComponent account={account}/>
  );
}

export function DeepComponent({account}) {

  return (
    <span> {account.accountType} account</span>
  );
}

Бинго! Даже если компоненты разнесены по разным папкам проекта, ни один не останется без внимания ваших коллег в процессе поиска возможной ошибки. А чтобы не бросать и этот процесс на самотек и гарантированно заработать искреннюю благодарность соратников по работе, можно где-то в середине цепочки изменить или даже обнулить свойство:

…
export function MiddleComponent({account}) {

  account.accountType = null

  return (
    <LowComponent account={account} />
  );
}
…

Простыми компонентами никого не удивишь

Ну вот и пришло время для серьезной задачи: готовим компонент, который выводит данные карты пользователя приложения. При этом номер карты и CVV по умолчанию должны быть скрыты и отображаться при клике/тапе по соответствующему полю, ФИО — отображаться в виде «имя сокращено до одной буквы, фамилия полностью, все заглавными буквами». Кроме того, нужно предусмотреть возможность копирования номера карты и CVV в буфер обмена.

Сначала пройдем по простому пути, а потом разберем, в чем была ошибка. Напишем функцию, которая будет отправлять нужные данные в буфер обмена

const copyToClipBoard = data => {
  const textField = document.createElement('textarea')
  textField.innerText = data
  document.body.appendChild(textField)
  textField.select()
  document.execCommand('copy')
  textField.remove()
  alert('Скопировано в буфер обмена')
}

добавим разбиение номера карты на группы

const formatNumber = cardNumber => {
  let cNumToStr = ''
    let it = 0
  
  String(cardNumber).split('').forEach((s) => {
    if (it % 4 === 0) {
      cNumToStr = cNumToStr + ' '
    }
    cNumToStr = cNumToStr + s
    it++
  })
  return cNumToStr
}

напишем компонент, который выводит номер карты, предусмотрев вариант скрытия первых трех групп чисел и возможность копирования данных

export function CardNumberComponent({cardNumber}) {
  const [isShow, setIsShow] = useState(false)
  
  const toggle = () => {setIsShow(!isShow)} 
  
  const showNumber = isShow 
    ? formatNumber(cardNumber)
    : "•••• " + formatNumber(cardNumber).substring(formatNumber(cardNumber).length - 4)

  return (
    <div>
      <p>Номер карты</p>
      <div onClick={toggle}> {showNumber} </div>
      <button onClick={() => copyToClipBoard(cardNumber)}>копировать</button>
    </div>
  )
}

аналогичным образом разберемся с CVV

export function CardCVVComponent({cvv}) {
  const [isShow, setIsShow] = useState(false)
  
  const toggle = () => {setIsShow(!isShow)} 

  const showCVV = isShow
    ? cvv
    : "•••"
    
  return (
    <div>
      <p>CVV</p>
      <div onClick={toggle}> {showCVV} </div>
      <button onClick={() => copyToClipBoard(cvv)}>копировать</button>
    </div>
  )
}

Напишем компонент, который выводит срок действия карты

export function CardExpireComponent({expire}) {
  return (
    <div>
      <p>Срок действия</p>
      {expire.year}/{expire.month}
    </div>
  )
}

и данные владельца

export function CardUserNameComponent({user}) {
  const userInfo = (user.name[0] + '. ' + user.surname).toUpperCase()

  return (
    <div>
      <p>Владелец</p>
      <div>{userInfo}</div>
      <button onClick={() => copyToClipBoard(userInfo)}>копировать</button>
    </div>
  )
}

Ну и последним действием соберем все вместе

export function CardComponent() {

  return (
    <>
      <CardNumberComponent cardNumber={cardInfo.cardNumber}></CardNumberComponent>
      <CardExpireComponent expire={cardInfo.expire}></CardExpireComponent>
      <CardCVVComponent cvv={cardInfo.cvv}></CardCVVComponent>
      <CardUserNameComponent user={user}></CardUserNameComponent>
    </>
  )
}

Источниками данных будут служить объект информации о пользователе и объект информации о карте

const user = {
  name: 'Ivan',
  surname: 'Ivanov',
}

const cardInfo = {
  cardNumber: "1234567812345678",
  expire: {
    year: 24,
    month: 11
  },
  cvv: 123
}

Как говорится — получите, распишитесь. Но присмотритесь внимательно, что мы накодили в результате? Пара простых функций, несколько небольших компонентов. Срамота, да и только! Этим не похвастаешься перед коллегами, да и начальству не объяснишь, что задача была трудна и неплохо бы подумать о премии. 

Выход — вспомнить о том, что нас просили сделать компонент, который выводит данные о карте. Один. Давайте исправим наш код.

export function CardComponent() {

  const copyToClipBoard = data => {
    const textField = document.createElement('textarea')
    textField.innerText = data
    document.body.appendChild(textField)
    textField.select()
    document.execCommand('copy')
    textField.remove()
    alert('Скопировано в буфер обмена')
  } 
  
  const [isShowNumber, setIsShowNumber] = useState(false)
  const [isShow, setIsShow] = useState(false)
  const toggleNumber = () => {setIsShowNumber(!isShowNumber)} 
  const toggleCVV = () => {setIsShow(!isShow)} 
  const userInfo = (user.name[0] + '. ' + user.surname).toUpperCase()
  let cNumToStr = ''
  let it = 0
  
  String(cardInfo.cardNumber).split('').forEach((s) => {
    if (it % 4 === 0) {
      cNumToStr = cNumToStr + ' '
    }
    cNumToStr = cNumToStr + s
    it++
    return cNumToStr
  })
  
  const showNumber = isShowNumber 
    ? cNumToStr
    : "•••• " + cNumToStr.substring(cNumToStr.length - 4)

  const showCVV = isShow
    ? cardInfo.cvv
    : "•••"
   
  return (
    <>
      <div>
        <p>Номер карты</p>
        <div onClick={toggleNumber}> {showNumber} </div>
        <button onClick={() => copyToClipBoard(cardInfo.cardNumber)}>копировать</button>
      </div>
      <div>
        <p>Срок действия</p>
        {cardInfo.expire.year}/{cardInfo.expire.month}
     </div>
     <div>
       <p>CVV</p>
       <div onClick={toggleCVV}> {showCVV} </div>
       <button onClick={() => copyToClipBoard(cardInfo.cvv)}>копировать</button>
     </div>
     <div>
        <p>Владелец</p>
        <div>{userInfo}</div>
        <button onClick={() => copyToClipBoard(userInfo)}>копировать</button>
    </div>

    </>
  )
}

Согласитесь, выглядит гораздо солиднее? Мало того, что весь функционал в одном месте, так еще и можно быть уверенным — проще будет позвать вас, как автора, чем разобраться, что в этом компоненте к чему относится. А значит — смело можно добавлять +500 в скилл «незаменимость».

Что в итоге?

Разумеется, секретов у настоящего мастера много, и выше приведены далеко не все. Ищите сами, добавляйте свои варианты в комментарии. Но помните, что при этом главное — не перегнуть палку. А то можно стать настолько незаменимым, что никто и на работу взять не рискнет.

Если хотите продолжения, пишите в комментарии или мне в Телеграм, и мы выпустим продолжение подборки незаменимых советов для реакт-мастеров :) 

Другие наши статьи: 

Комментарии (10)


  1. Frimko
    07.02.2022 14:08
    +1

    А чтобы не бросать и этот процесс на самотек и гарантированно заработать искреннюю благодарность соратников по работе, можно где-то в середине цепочки изменить или даже обнулить свойство:

    …
    export function MiddleComponent({account}) {
    account.accountType = null
    return (
    <LowComponent account={account} />
    );
    }
    …

    Заголовок не верный для такой статьи. Предлагаю "Вредные рецепты и как схватить ненависть своих коллег".

    з.ы. Смеялись с коллегами долго.


    1. JustDont
      07.02.2022 14:11
      +4

      Я так и не понял, то ли это "вредные советы", то ли еще что. Скажем первый совет про спред — весьма рабочий. При условии что проект на TS, и типы нормально описаны. А если на JS — то да, это уровень классического // happy debugging!


      1. ivadey
        07.02.2022 20:22

        Необязательно, вполне может хватить prop-types, в некоторых моментах результат может быть даже лучше чем на ts

        P.S. PropTypes не замена и не альтернатива, просто ещё один инструмент, который порой очень удобен.


      1. Dartess
        08.02.2022 10:09

        Типы не спасут от того что так можно покинуть несуществующие пропсы в дом элементы.


  1. aavezel
    07.02.2022 16:00
    +1

    Контексты можно тоже преопределять, притом хрен кто поймет в каком месте это поменяно. Особенно если использовать displayName по извращенному кодестайлу. И в отличии от props drilling, это точно хрен найдешь...

     export function MiddleComponent() { 
    	return (
    		<AccountType.Provider value="debet"> 
    			<LowComponent account={account} />
    		</AccountType.Provider>
    	);
     }

    а ещё можно провайдер завязать на функционал, который в зависимость берет туеву кучу параметров от родителя, который формируется на основе магии вуду индийского пошива.

    а ещё можно каждый компонент обвязать 5-6 различными контекстами (каждый компонент конечно своим списком контекстов), каждый контекст из которых инициализируется в разных родительских компонентах...

    я ещё знаю пару способов использования контекстов, которые заставят вашего разработчика сделать харакири, но их приводить не буду, так как запрещены Женевской конвенцией.


    1. Alexandroppolus
      08.02.2022 02:43

      Про способ с локальными контекстами было недавно - https://habr.com/ru/company/alfa/blog/647013/

      Имхо, вполне съедобная штука. Есть паттерн, есть теория к нему, есть, в конце концов, код-ревью для подстраховки соблюдения дисциплины при использовании - значит инструмент можно использовать.


  1. skeevy
    07.02.2022 16:46
    +1

    почему нет совета использовать querySelector вместо ref? :) такое же тоже бывает


    1. Maxim_from_HW Автор
      07.02.2022 20:22
      +1

      Поздравляем, вы приняты на работу, занесите в отдел кадров ведомость "Проклятия коллег 2010-2020"))


  1. Gibboustooth
    08.02.2022 19:37

    const formatNumber = cardNumber => {
      let cNumToStr = ''
      let it = 0
      
      String(cardNumber).split('').forEach((s) => {
        if (it % 4 === 0) {
          cNumToStr = cNumToStr + ' '
        }
        cNumToStr = cNumToStr + s
        it++
      })
      return cNumToStr
    }

    Отличная реализация форматирования! А то неопытный разработчик мог бы написать

    const formatNumber = (cardNumber: string): string => cardNumber.match(/\d{4}/g).join(' ')

    и немедленно вылететь с работы. Не за то ему платят, чтобы функции в одну строчку писать.

    Кстати, еще один отличный совет: ни в коем случае, ни при каких обстоятельствах не использовать Typescript


  1. insphoto
    08.02.2022 22:40
    +1

    Видимо у автора наболело)

    К сожалению правда жизни такова что очень многие разработчики пишут код как будто учились по вредным советам. Был у меня в работе один проект, который начинал один мастер пера, я один компонент дорабатывал три дня что бы отредактировать один алгоритм внутри. В итоге мне надоело, я его с нуля написал, оказалось проще код выкинуть