Приложение для просмотра дерева реакции, использующее хуки (т. Е. useCallback)

#reactjs #react-hooks

#reactjs #реагирующие перехваты

Вопрос:

Я ищу мнения о моем подходе к созданию приложения для просмотра дерева реакции, использующего хуки.

Вот код, использующий useCallback, React.memo и useState. Пожалуйста, обратите внимание, что одновременно может быть открыт только один элемент 1-го уровня, на остальных уровнях может быть открыто сразу несколько элементов.

Branch.js:

 import React, { useState, useCallback} from 'react'
import Leaf from './Leaf'

const Branch = ({ items }) => {
  const [expanded, setExpanded] = useState([])

  const clickHandler = useCallback(
    ({ categoryId, level }) => {
      let result
      if (level === 1) {
        result = expanded.includes(categoryId) ? [] : [categoryId]
      } else {
        result = expanded.includes(categoryId) ? expanded.filter(item => item !== categoryId) : [ ...new Set([ categoryId, ...expanded])]
      }

      setExpanded(result)
    },[expanded])

  return (
    <ul>
      {items amp;amp; items.map(item => {
        const { categoryId, categoryName, level, eventsCount, children } = item
        return (
          <Leaf
            key={categoryId}
            categoryId={categoryId}
            name={categoryName}
            level={level}
            eventsCount={eventsCount}
            children={children}
            isOpen={expanded.includes(categoryId)}
            onClick={clickHandler}
          />
        )})}
    </ul>
  )
}

export default Branch
  

Leaf.js:

 import React from 'react'
import Branch from './Branch'

const Leaf = React.memo(({ name, categoryId, level, children, eventsCount, onClick, isOpen }) => {
  const _onClick = () => {
    onClick({ categoryId, level })
  }
  return (
    <li className={!isOpen amp;amp; 'hidden'}>
      <button onClick={_onClick}>
        <span>{name}</span>
      </button>
        {children.length ? <Branch items={children}/> : ''}
    </li>
  )
})

export default Leaf
  

Я бы хотел, чтобы кто-нибудь проверил код на предмет производительности (т. Е. Количества ненужных повторных отображений), которые могут произойти. Мне интересно ваше мнение о моем использовании React.memo и обработчика событий click (useCallback).

Вызывает ли способ, которым я передаю вниз clickHandler , а затем получаю и запускаю этот обработчик, или предотвращает дополнительные повторные рендеринги?

Ответ №1:

Это было бы более эффективно с функциональными обновлениями:

   const clickHandler = useCallback(
    ({ categoryId, level }) => {
      setExpanded(expanded => {
        let result
        if (level === 1) {
          result = expanded.includes(categoryId) ? [] : [categoryId]
        } else {
          result = expanded.includes(categoryId) ? expanded.filter(item => item !== categoryId) : [ ...new Set([ categoryId, ...expanded])]
        }

        return result
      }
    }, []
  )
  

Таким образом, обработчик вообще не меняется.

Ответ №2:

Единственное серьезное ограничение производительности в вашем коде заключается в том, что при изменении expanded создается новый clickHandler обратный вызов, который приведет к прерыванию запоминания всего Leaf компонента, что приведет к повторному отображению всех компонентов, а не только того конкретного компонента, isOpen реквизит которого изменился

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

Первое: первое решение включает в себя использование метода обратного вызова для setState и использование useCallback только при первоначальном рендеринге

 const Branch = ({ items }) => {
  const [expanded, setExpanded] = useState([])

  const clickHandler = useCallback(
    ({ categoryId, level }) => {
      setExpanded(prevExpanded => {
          let result
          if (level === 1) {
            result = expanded.includes(categoryId) ? [] : [categoryId]
          } else {
            result = expanded.includes(categoryId) ? expanded.filter(item => item !== categoryId) : [ ...new Set([ categoryId, ...expanded])]
          }

          return resu<
      })

    },[])

  return (
    <ul>
      {items amp;amp; items.map(item => {
        const { categoryId, categoryName, level, eventsCount, children } = item
        return (
          <Leaf
            key={categoryId}
            categoryId={categoryId}
            name={categoryName}
            level={level}
            eventsCount={eventsCount}
            children={children}
            isOpen={expanded.includes(categoryId)}
            onClick={clickHandler}
          />
        )})}
    </ul>
  )
}

export default Branch;
  

Второе: когда логика обновления состояния становится сложной, использование метода обратного вызова для обновления состояния может запутать и затруднить отладку. В таких случаях лучше использовать useReducer вместо useState и использовать dispatch действие для установки состояния

 const initialState = [];

const reducer = (state, action) => {
  switch(action) {
    case 'UPDATE_EXPANDED': {
      const { level, categoryId } = action;
      if (level === 1) {
            return state.includes(categoryId) ? [] : [categoryId]
      } else {
            return state.includes(categoryId) ? state.filter(item => item !== categoryId) : [ ...new Set([ categoryId, ...state])]
      }
    }
    default: return state;
  }
}

const Branch = ({ items }) => {
  const [expanded, dispatch] = useReducer(reducer, initialState);

  return (
    <ul>
      {items amp;amp; items.map(item => {
        const { categoryId, categoryName, level, eventsCount, children } = item
        return (
          <Leaf
            key={categoryId}
            categoryId={categoryId}
            name={categoryName}
            level={level}
            eventsCount={eventsCount}
            children={children}
            isOpen={expanded.includes(categoryId)}
            onClick={dispatch}
          />
        )})}
    </ul>
  )
}


const Leaf = React.memo(({ name, categoryId, level, children, eventsCount, onClick, isOpen }) => {
  const _onClick = () => {
    onClick({ type: 'UPDATE_EXPANDED', categoryId, level });
  }
  return (
    <li className={!isOpen amp;amp; 'hidden'}>
      <button onClick={_onClick}>
        <span>{name}</span>
      </button>
        {children.length ? <Branch items={children}/> : ''}
    </li>
  )
})

export default Leaf
export default Branch;