React основы

Aug 29, 2021

Реакт использует тоже самое апи, которое используете вы при создании DOM узлов: В исходном коде React (createElement)

Реакт абстрагирует от вас обязательный апи браузера и дает вам гораздо более декларативный апи для работы. Imperative vs Declarative программирование

Введение в реакт raw апи (создание и рендеринг)

    const rootElement = document.getElementById('root')
    // создание реакт эл-та
    const element = React.createElement('div', {
        className: 'container',
        children: 'text simple',
    })
    // рендеринг реакт эл-тов в DOM
    ReactDOM.render(element, rootElement)

С дочерними эл-ми:

    const rootElement = document.getElementById('root'),
          element = React.createElement('div', {
    className: 'myClass',
    children: [
            React.createElement('span', null, 'Привет'),
            ' ',
            React.createElement('span', null, 'кто-то'),
        ],
    })
    ReactDOM.render(element, rootElement)

Преобразуется в:

<div id="root">
    <div class="myClass">
        <span>Привет</span> <span>кто-то</span>
    </div>
</div>

Введение в jsx (создание и рендеринг)

jsx не js, поэтому необходимо использовать компилятор, например, Babel

Введение в jsx на сайте reactjs: https://reactjs.org/docs/introducing-jsx.html

    const element = <div className="container">simple text</div>
    ReactDOM.render(element, document.getElementById('root'))

Интерполяция className and children

    const   children = 'simple text',
            className = 'container',
            element = <div className={className}>{children}</div>;
    ReactDOM.render(element, document.getElementById('root'))

Spread оператор для св-в объекта

Атрибуты spread: https://reactjs.org/docs/jsx-in-depth.html#spread-attributes

    const children = 'текст',
          className = 'container',
          props = {children, className},
          element = <div {...props} />;
    ReactDOM.render(element, document.getElementById('root'))

Кастомные компоненты (создание)

Кастомные компоненты с React.createElement

    // Для примера, в реальности так не делается
    function text({children}) {
        return <div className="message">{children}</div>
    }

    const element = (
        <div className="container">
            {React.createElement(text, {children: 'текст комп. 1'})}
            {React.createElement(text, {children: 'текст комп. 2'})}
        </div>
    )

    ReactDOM.render(element, document.getElementById('root'))

Кастомные компоненты с JSX

    function Text({children}) {
        return <div className="Text">{children}</div>
    }

    const element = (
      <div className="container">
        <Text>Hello World</Text>
        <Text>Goodbye World</Text>
      </div>
    )

    ReactDOM.render(element, document.getElementById('root'))

Добавляем PropTypes (внешний пакет, чекаем пропсы)

Проверка типа с PropTypes: https://reactjs.org/docs/typechecking-with-proptypes.html

import PropTypes from 'prop-types';

function HelloWorldComponent({ name }) {
    return (
      <div>Hello, {name}</div>
  )
}

HelloWorldComponent.propTypes = {
    name: PropTypes.string,

    // кастомный вадидатор:
    customProp: function(props, propName, componentName) {
        if (!/matchme/.test(props[propName])) {
            return new Error(
                'Invalid prop `' + propName + '` supplied to' +
                ' `' + componentName + '`. Validation failed.'
            );
        }
    },

}

export default HelloWorldComponent;

React Fragments

React Fragments - позволяет использовать несколько элементов подряд в коде:

<>
    <HelloWorldComponent name="Vasya" />
    <HelloWorldComponent name="Kolya"  />
</>

Стили

  1. Инлайн стили - свойство style prop.
  2. className для класса.
<div style={{paddingTop: 10, backgroundColor: 'blue'}} />

const myStyles = {paddingTop: 20, backgroundColor: 'blue'}
<div style={myStyles} />

Объединяем св-ва style и className переданными значениями:

function MyComponent({style, className = '', ...otherProps}) {
    return (
      <div   className={`box ${className}`}
                style={{fontStyle: 'italic', ...style}}
                {...otherProps}
    />
  )
}

<MyComponent className="testClass" style={{backgroundColor: 'orange'}}>
    small lightblue box
</MyComponent>

Формы

Получаем значение формы через event.target.elements.usernameInput.value:

function UsernameForm({onSubmitUsername}) {
  function handleSubmit(event) {
    event.preventDefault();
    onSubmitUsername(event.target.elements.usernameInput.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input id="usernameInput" type="text" />
      </div>
      <button type="submit">Submit</button>
    </form>
  )
}

function App() {
  const onSubmitUsername = username => console.log(`You entered: ${username}`);
  return <UsernameForm onSubmitUsername={onSubmitUsername} />
}

Использование refs (useRef)

ref также поможет получить значение поля формы. ref - это мутабельный обьект, который будет сохраняться в течение всего времени жизни компонента. Он имеет свойство current.
В случае с узлами DOM вы можете передать ref React элемент и React будет ставить св-ву current отрендеренный DOM узел.

Таким образом если вы сохдадите inputRef объет посредством React.useRef, вы могли бы получить значение: inputRef.current.value: https://reactjs.org/docs/hooks-reference.html#useref

function UsernameForm({onSubmitUsernameӥ) {
    const usernameInputRef = React.useRef()

    function handleSubmit(event) {
        event.preventDefault()
        onSubmitUsername(usernameInputRef.current.value)
    }

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <input id="usernameInput" type="text" ref={usernameInputRef} />
            </div>
            <button type="submit">Submit</button>
        </form>
    )
}

function App() {
    const onSubmitUsername = username => alert(`You entered: ${username}`)
    return <UsernameForm onSubmitUsername={onSubmitUsername} />
}

Состяние при помощи хука useState

Использовать состояние в React можно при помощи хука useState, классический пример:

function Counter() {
    const [count, setCount] = React.useState(0)
    const increment = () => setCount(count + 1)
    return{count}
}

React.useState принимает значение по умолчанию и взвращает массив. Обычно вы будете деструктуризировать этот массив, чтобы получить state и ф-ю обновления state -
https://reactjs.org/docs/hooks-state.html.

Управляем значением поля

React позволяет программно установить значение инпута:

<input value={myInputValue} />

Код выше гарантирует, что значение этого поля никогда не будет отличаться от значения переменной myInputValue.

Значение поля может храниться в state переменной через React.useState, и обновляться посредством onChange обработчика, возвращенного от того же useState через деструктуризацию.

function Form({onSubmit}) {
    const [username, setUsername] = React.useState('')

    function handleSubmit(event) {
        event.preventDefault()
        onSubmit(username)
    }

  function handleChange(event) {
        setUsername(event.target.value.toLowerCase())
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="usernameInput">Username:</label>
        <input
          id="usernameInput"
          type="text"
          onChange={handleChange}
          value={username}
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  )
}

function App() {
    const onSubmit = username => alert(`You entered: ${username}`)
    return (
        <div style={{minWidth: 400}}>
            <Form onSubmit={onSubmit} />
        </div>
  )
}

Рендерим массивы

React не поддерживает корректного изменения состояния массивов, когда вы что-то добавляете или удаляете в массиве.

Каждый React-элемент принимает специальное ключевое свойство key, которе вы можете использовать, чтобы помочь React между обновлениями.

Понимаение key в React: Understanding React’s key prop или Why React needs a key prop

<li key={item.id}>

Хуки: useState

Приложение должно где-то хранить состояние. В React для этого используются специальные функции, называемые хуками. Общие встроенные хуки включают:

  • React.useState
  • React.useEffect
  • React.useContext
  • React.useRef
  • React.useReducer

Хук - это специальная функция, которую вы можете вызвать внутри своего компонента, чтобы хранить данные (например, state) или для выполнения action (или побочных эффектов).

Каждый из хуков имеет уникальный API. Некоторые возвращают значение (например, React.useRef и React.useContext), другие возвращают пару значений (например, React.useState и React.useReducer), а третьи вообще ничего не возвращают (например, React.useEffect).

Классический пример использвания хука useState:

function Counter() {
    const [count, setCount] = React.useState(0)
    const increment = () => setCount(count + 1)
    return <button onClick={increment}>{count}</button>
}

React.useState - это функция, которая принимает единственный аргумент. Этот аргумент является начальным state компонента. В нашем случае state будет равно 0.

React.useState возвращает пару значений, посредством массива с двумя элементами (воспользуемся деструктуризацией для присвоения каждого из этих значений отдельным переменным).

Первый элемент - значение состояния, второй - функция, которую мы можем вызвать для обновления состояния. Мы можем называть эти переменные как угодно. Обычное соглашение - выбрать имя для переменной состояния, а затем установить префикс перед этим именем для функции изменения стостояния.

Вызывая setCount, мы сообщаем React о необходимости повторного рендеринга нашего компонента. Этим мы повторно вызывем функцию-компонент Counter, далее, когда на этот раз вызывается React.useState, возвращаемое значение - это значение, с которым мы вызвали setCount. Все это повторяется до размонтирования компонента

хуки: useEffect

React.useEffect - это встроенный хук, который позволяет вам выполнять некоторый пользовательский код после того, как React отрендерит (и перерендерит) ваш компонент в DOM.

React.useEffect(() => {
    // здесь можно выполнять HTTP-запросы или взаимодействовать с API браузера.
}

Получаем значение name из localStorage (если доступно) и обновляем localStorage по мере обновления name:

function TestComponent({initName = ''}) {
    const [name, setName] = React.useState(
      window.localStorage.getItem('name') || initName,
    )

    React.useEffect(() => {
      window.localStorage.setItem('name', name)
    })

    function handleChange(event) {
      setName(event.target.value)
    }

    return (
      <input value={name} onChange={handleChange} id='name' />
    )
}

хук useState: инициализация состояния только на первом рендере

Хук useState в React позволяет передать функцию вместо фактического значения, а затем он вызовет эту функцию только для получения значения состояния при первом рендеринге компонента. То есть код выше можно переписать на (что производительнее):

  const [name, setName] = React.useState(
    () => window.localStorage.getItem('name') || initName,
  )

хук useEffect: зависимости

React.useEffect имеет второй аргумент, называемый 'массивом зависимостей', который говорит React о том, что колбэк вашего эффекта должна вызываться, лишь в том случае, когда эти зависимости изменяются:

  React.useEffect(() => {
    window.localStorage.setItem('name', name)
  }, [name])

  // Если вы хотите запустить эффект один раз, вы можете передать пустой массив []

Кастомный хук

Некоторый повторяющийся код можно вынести в отдельную функцию - кастомный хук. Реализуем хук, реализующий следующую логику (код выше) - забираем из localStorage значение key посредством useState и записываем при каждом изменении key в localStorage:

// кастомный хук useLocalStorageState:
function useLocalStorageState(key, defaultValue = '') {
    const [state, setState] = React.useState(
      () => window.localStorage.getItem(key) || defaultValue,
    )

    React.useEffect(() => {
      window.localStorage.setItem(key, state)
    }, [key, state])

    return [state, setState]
}

function Greeting({initialName = ''}) {
    // использование кастомного хука useLocalStorageState:
    const [name, setName] = useLocalStorageState('name', initialName)
   //...
}

хук: useReducer

useState не охватывает все кейсы, например, иногда нужно отделить логику состояния от компонента. Кроме того, если у вас есть несколько элементов состояния, которые обычно изменяются вместе, то наличие объекта, содержащего эти элементы состояния, может быть весьма полезным.
useReducer

Для данных случаев пригодится useReducer.

Обычно useReducer (reactjs.org - usereducer) работает с объектом, но для нашего первого примеры мы будем управлять простым числом (count).

reducer (countReducer) вызывается с двумя аргументами:

  1. текущий state
  2. 'action' или, другими словами, dispatch-функцию
const countReducer = (state, newState) => newState

function Counter({initCount = 0, step = 1}) {
    const [count, setCount] = React.useReducer(countReducer, initCount),
          increment = () => setCount(count + step);
    return <button onClick={increment}>{count}</button>
}

function App() {
    return <Counter />
}

Реализуем setState через объект

const countReducer = (state, action) => ({...state, ...action})

function Counter({initialCount = 0, step = 1}) {
    const [state, setState] = React.useReducer(countReducer, {
        count: initialCount,
    })
    const {count} = state
    const increment = () => setState({count: count + step})
    return <button onClick={increment}>{count}</button>
}

function App() {
    return <Counter />
}

Реализуем setState через объект или функцию

const countReducer = (state, action) => ({
    ...state,
    ...(typeof action === 'function' ? action(state) : action),
});

function Counter({initialCount = 0, step = 1}) {
    const [state, setState] = React.useReducer(countReducer, {
        count: initialCount,
    })
    const {count} = state,
    increment = () => setState(currentState => ({count: currentState.count + step}))
    return <button onClick={increment}>{count}</button>
}

function App() {
    return <Counter />
}

Традиционный dispatch объект и оператором switch

function countReducer(state, action) {
    const {type, step} = action;
    switch (type) {
        case 'increment': {
        return {
                ...state,
                count: state.count + step,
            }
        }

        default: {
            throw new Error(`Unsupported action type: ${type}`)
        }
    }
}

function Counter({initialCount = 0, step = 1}) {
    const [state, dispatch] = React.useReducer(countReducer, {
      count: initialCount,
    });
    const {count} = state;
    const increment = () => dispatch({type: 'increment', step});
    return <button onClick={increment}>{count}</button>
}

function App() {
    return <Counter />
}

хук: useCallback

useCallback возвращает мемоизированный версию колбэка, который изменяется только, если изменяются значение одной из зависимостей.

const updateLocalStorage = React.useCallback(
  () => window.localStorage.setItem('count', count),
  [count], // <-- yup! That's a dependency list!
)
React.useEffect(() => {
    updateLocalStorage();
}, [updateLocalStorage]);   // при каждом рендере она будет создавать поновой, но нам это не нужно,
                                 // нам нужен ее вызов лишь при изменении зависимостей

На полседующих рендерах если элементы в списке зависимостей не изменились (yup!), вместо того, чтобы вернуть ту же функцию, которую мы передали ему, React предоставит нам ту же функцию, что и в прошлый раз.

Итак, хотя мы по-прежнему создаем новую функцию при каждом рендеринге (для передачи в useCallback), React предоставляет нам новую только в случае изменения списка зависимостей.

Использование useCallback

function asyncReducer(state, action) {
    switch (action.type) {
        case 'pending': {
            return {status: 'pending', data: null, error: null}
        }
        case 'resolved': {
            return {status: 'resolved', data: action.data, error: null}
        }
        case 'rejected': {
            return {status: 'rejected', data: null, error: action.error}
        }
        default: {
            throw new Error(`Unhandled action type: ${action.type}`)
        }
    }
}

// хук с использованием useCallback
function useAsync(initialState) {
    const [state, dispatch] = React.useReducer(asyncReducer, {
        status: 'idle',
        data: null,
        error: null,
        ...initialState,
    });

    const {data, error, status} = state;

    const run = React.useCallback(promise => {
        dispatch({type: 'pending'})
        promise.then(
            data => {
                dispatch({type: 'resolved', data})
            },
            error => {
                dispatch({type: 'rejected', error})
            },
        )
    }, []) // 1 раз при загрузке

    return {
        error,
        status,
        data,
        run
    }
}

function PokemonInfo({pokemonName}) {
  const {data: pokemon, status, error, run} = useAsync({
    status: pokemonName ? 'pending' : 'idle',
  })

  React.useEffect(() => {
    if (!pokemonName) {
        return
    }
    run(fetchPokemon(pokemonName))
  }, [pokemonName, run])

  if (status === 'idle') {
    return 'Submit a pokemon'
  } else if (status === 'pending') {
    return <PokemonInfoFallback name={pokemonName} />
  } else if (status === 'rejected') {
    throw error
  } else if (status === 'resolved') {
    return <PokemonDataView pokemon={pokemon} />
  }

  throw new Error('This should be impossible')
}

function App() {
    const [pokemonName, setPokemonName] = React.useState('')

  function handleSubmit(newPokemonName) {
    setPokemonName(newPokemonName)
  }

  function handleReset() {
    setPokemonName('')
  }

  return (
    <div className="pokemon-info-app">
      <PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
      <hr />
      <div className="pokemon-info">
        <PokemonErrorBoundary onReset={handleReset} resetKeys={[pokemonName]}>
          <PokemonInfo pokemonName={pokemonName} />
        </PokemonErrorBoundary>
      </div>
    </div>
  )
}
// request
function fetchPokemon(name, delay = 1500) {
  const pokemonQuery = //....;

  return window
    .fetch('https://test.app/', {
        method: 'POST',
        headers: {
          'content-type': 'application/json;charset=UTF-8',
          delay: delay,
        },
        body: JSON.stringify({
          query: pokemonQuery,
          variables: {name: name.toLowerCase()},
        }),
    })
    .then(async response => {
      const {data} = await response.json()
      if (response.ok) {
        const pokemon = data?.pokemon
        if (pokemon) {
          pokemon.fetchedAt = formatDate(new Date())
          return pokemon
        } else {
          return Promise.reject(new Error(`No pokemon with the name "${name}"`))
        }
      } else {
        // handle errors
        const error = {
          message: data?.errors?.map(e => e.message).join('\n'),
        }
        return Promise.reject(error)
      }
    })
}

хук: useContext

Расшарить state между компонентами распространенная проблема. Лучшее решение - поднять state наверх. Но это требует пробрасывать пропсы, что иногда вызывает проблемы.

Чтобы избежать этой боли, мы можем вставить какой-либо state в раздел нашего React-дерева, а затем извлечь этот state в любом месте этого React-дерева, не передавая его явно повсюду. Эта особенность называется context. В некотором смысле он похож на глобальные переменные, но не страдает теми же проблемами благодаря тому, как работает API, чтобы сделать отношения явными.

Пример работы контекста:

const FooContext = React.createContext()

function FooDisplay() {
    const foo = React.useContext(FooContext)
    return <div>Foo is: {foo}</div>
}

ReactDOM.render(
  <FooContext.Provider value="I am foo">
    <FooDisplay />
  </FooContext.Provider>,
  document.getElementById('root'),
)
// renders <div>Foo is: I am foo</div>

Первым аргументом createContext, вы можете указать значение по умолчанию, если, например, не будет продоставлено значение посредством value:

ReactDOM.render(<FooDisplay />, document.getElementById('root'))

Иногда лучшим решением проброса пропсов является Using Composition in React to Avoid "Prop Drilling" (youtube)

const CountContext = React.createContext()

function CountProvider(props) {
    const [count, setCount] = React.useState(0)
    const value = [count, setCount]
    // как вариант:
    // const value = React.useState(0)
    return <CountContext.Provider value={value} {...props} />
}

function CountDisplay() {
    const [count] = React.useContext(CountContext)
    return <div>{`The current count is ${count}`}</div>
}

function Counter() {
    const [, setCount] = React.useContext(CountContext)
    const increment = () => setCount(c => c + 1)
    return <button onClick={increment}>Increment count</button>
}

function App() {
    return (
      <div>
        <CountProvider>
          <CountDisplay />
          <Counter />
        </CountProvider>
      </div>
    )
}

хук: useLayoutEffect

Есть два способа указать React запускать побочные эффекты после рендеринга:
useEffect useLayoutEffect

В 99% случаях useEffect это то что вам нужно, но иногда useLayoutEffect может улучшить пользовательский опыт. useEffect vs useLayoutEffect и hook flow diagram

Когда мы используем useEffect, между моментом визуального обновления DOM и запуском нашего кода проходит немного времени. Вот простое правило, когда вам следует использовать useLayoutEffect: если вы вносите изменения в DOM, то это должно происходить в useLayoutEffect, в противном случае - useEffect.

function MessagesDisplay({messages}) {
  const containerRef = React.useRef();
  React.useLayoutEffect(() => {
    containerRef.current.scrollTop = containerRef.current.scrollHeight
  })

  return (
    <div ref={containerRef} role="log">
      {messages.map((message, index, array) => (
        <div key={message.id}>
          <strong>{message.author}</strong>: <span>{message.content}</span>
        </div>
      ))}
    </div>
  )
}

хук: useDebugValue

Расширения для браузера React DevTools является обязательным для любого разработчика React.

Но когда вы начинаете писать собственные хуки бываем полезным присвоить им собственные метки. Это особенно полезно, чтобы различать разные варианты использования одного и того же хука в определенном компоненте. В этом случае на помощь приходит useDebugValue. Вы используете его в настраиваемом хуке и вызываете его следующим образом:

function useCount({initialCount = 0, step = 1} = {}) {
  React.useDebugValue({initialCount, step})
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + step)
  return [count, increment]
}

Итак, теперь, когда люди используют хук useCount, они видят значения initialCount и step для каждого конкретного хука с данными параметрами.

Пример работы кастомного хука, useDebugValue и обработчиков matchMedia

function useMedia(query, initialState = false) {
  const [state, setState] = React.useState(initialState);
  React.useDebugValue(`\`${query}\` => ${state}`); // каждый раз мы будем видеть
    // state соотв-го query

   React.useEffect(() => {
    let mounted = true
    const mql = window.matchMedia(query)
    function onChange() {
      if (!mounted) {
        return
      }
      setState(Boolean(mql.matches))
    }

    mql.addListener(onChange)
    setState(mql.matches)

    // размонтируем листенеры
    return () => {
     mounted = false
     mql.removeListener(onChange)
    }
  }, [query]); // ставим листенер только при изменении query, т.е. 3 раза

  return state
}

function Box() {
    const isBig = useMedia('(min-width: 1000px)'),
          isMedium = useMedia('(max-width: 999px) and (min-width: 700px)'),
          isSmall = useMedia('(max-width: 699px)'),
          color = isBig ? 'green' : isMedium ? 'yellow' : isSmall ? 'red' : null;

    return <div style={{width: 200, height: 200, backgroundColor: color}} />
}

function App() {
    return <Box />
}

Заметки по работе с контекстом (createContext, useReducer, Запрос к серверу)

// Контекст
const UserContext = React.createContext();
UserContext.displayName = 'UserContext';

// Редьюсер
function userReducer(state, action) {
  switch (action.type) {
    case 'start update': {
      return {
        ...state,
        user: {...state.user, ...action.updates},
        status: 'pending',
        storedUser: state.user,
      }
    }
    //...etc
}

// пробрасываем значение (верхний компонент)
function UserProvider({children}) {
    const {user} = useAuth()
    const [state, dispatch] = React.useReducer(userReducer, {
    status: null,
        error: null,
        storedUser: user,
        user,
    })
    const value = [state, dispatch]
    return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}

// хук для получения контекста
function useUser() {
    const context = React.useContext(UserContext)
    if (context === undefined) {
    throw new Error(`useUser must be used within a UserProvider`)
  }
  return context
}

// Получаем данные с сервера
// https://twitter.com/dan_abramov/status/1125773153584676864
async function updateUser(dispatch, user, updates) {
  dispatch({type: 'start update', updates})
  try {
    const updatedUser = await userClient.updateUser(user, updates)
    dispatch({type: 'finish update', updatedUser})
    return updatedUser
  } catch (error) {
    dispatch({type: 'fail update', error})
    return Promise.reject(error)
  }
}


// компонент
function UserSettings() {

  // получаем значение контекста и userDispatch
  const [{user, status, error}, userDispatch] = useUser()

  function handleSubmit(event) {
    event.preventDefault()
    updateUser(userDispatch, user, formState).catch(() => {
      /* ignore the error */
    })
  }

  // html формы...
}

Эмуляция запроса

const sleep = t => new Promise(resolve => setTimeout(resolve, t));

// TODO: сделать реальный запрос на fetch
async function updateUser(user, updates, signal) {
  await sleep(1500) // эмуляция задуржки
  if (`${updates.tagline} ${updates.bio}`.includes('fail')) {
    return Promise.reject({message: 'Something went wrong'})
  }
  return {...user, ...updates}
}

export {updateUser}

Составные компоненты

cloneElement

Составные компоненты - это компоненты, которые работают вместе, чтобы сформировать законченный пользовательский интерфейс. Классический пример - select и option в HTML:

Основная проблемап при разработке составных компонентов - состояние, разделяемое между компонентами, является неявным. В примере ниже мы решим эту проблему, предоставив составным компонентам необходимые им пропсы, неявно используя React.cloneElement.

function Toggle({children}) {
    const [on, setOn] = React.useState(false)
    const toggle = () => setOn(!on)
    return React.Children.map(children, child => {
    return typeof child.type === 'string' // фильтруем DOM-элементыапэрдпрэардаэпдрэхапдрэадпрэжпдар
      ? child
      : React.cloneElement(child, {on, toggle})
  })
}

function ToggleOn({on, children}) {
    return on ? children : null
}

function ToggleOff({on, children}) {
    return on ? null : children
}

function ToggleButton({on, toggle, ...props}) {
    return <Switch on={on} onClick={toggle} {...props} />
}

function App() {
    return (
      <div>
        <Toggle>
          <ToggleOn>The button is on</ToggleOn>
          <ToggleOff>The button is off</ToggleOff>
          <ToggleButton />
        </Toggle>
      </div>
    )
}

Контекст

Воспользуемся вместо cloneElement контекстом.

const ToggleContext = React.createContext()
ToggleContext.displayName = 'ToggleContext'; // for debug

function Toggle({children}) {
    const [on, setOn] = React.useState(false),
          toggle = () => setOn(!on);

    return (
      <ToggleContext.Provider value={{on, toggle}}>
      {children}
    </ToggleContext.Provider>
  )
}

// кастомный хук для использования контекста в составных компонентов
function useToggle() {
    return React.useContext(ToggleContext)
}

function ToggleOn({children}) {
    const {on} = useToggle()
    return on ? children : null
}

function ToggleOff({children}) {
    const {on} = useToggle()
    return on ? null : children
}

function ToggleButton({...props}) {
    const {on, toggle} = useToggle()
    return <Switch on={on} onClick={toggle} {...props} />
}

Испольльзуем spread оператор и объект с пропсами

Список пропсов, которые необходимо применить к компонентам может быть обширным, поэтому хорошей идеей может быть создание объекта с пропсами для последующего применения к компоненту или хуку совместно с оператором spread.

function useToggle() {
    const [on, setOn] = React.useState(false)
    const toggle = () => setOn(!on)

    return {
      on,
      toggle,
      togglerProps: {
        'aria-pressed': on,
        onClick: toggle,
      },
    }
}

function App() {
  const {on, togglerProps} = useToggle()
  return (
    <div>
      <Switch on={on} {...togglerProps} />
      <hr />
      <button aria-label="custom-button" {...togglerProps}>
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

Совместно с обработчиками событий (onClick)

const callAll = (...fns) => {
  return (...args) => {
    fns.forEach(fn => fn?.(...args))
  }
} // useful
// ...args - всегда объект Event соотв-го эл-та, где произошло событие
// передается во все ф-и fns
function useToggle() {
  const [on, setOn] = React.useState(false)
  const toggle = (e) => {
    console.log('e: ', e)
    setOn(!on)
  }

  function getTogglerProps({onClick, ...props} = {}) {
    return {
      'aria-pressed': on,
      onClick: callAll(onClick, toggle),
      ...props,
    }
  }

  return {
    on,
    toggle,
    getTogglerProps,
  }
}
function App() {
  const {on, getTogglerProps} = useToggle()

  const getInfo = (data, ev) =>  {
    console.log(data);
  }

  return (
    <div>
      <Switch {...getTogglerProps({on})} />
      <hr />
      <button
        {...getTogglerProps({
          'aria-label': 'custom-button',
           onClick: (e) => getInfo('hey', e),
           id: 'custom-button-id',
        })}
      >
        {on ? 'on' : 'off'}
      </button>
    </div>
  )
}

State Reducer

шаблон State Reducer с React хуками

Ваша задача - дать людям возможность предоставить настраиваемый Редьюсер, чтобы они могли полностью контролировать, как происходит обновление State в нашем компоненте.

const callAll = (...fns) => (...args) => fns.forEach(fn => fn?.(...args))

const actionTypes = {
    toggle: 'toggle',
    reset: 'reset',
}

// дефолтный редьюсер
function toggleReducer(state, {type, initialState}) {
    switch (type) {
        case actionTypes.toggle: {
            return {on: !state.on}
        }
        case actionTypes.reset: {
            return initialState
        }
        default: {
            throw new Error(`Unsupported type: ${type}`)
        }
    }
}

// хук настройки редьюсера
function useToggle({initialOn = false, reducer = toggleReducer} = {}) {
  const {current: initialState} = React.useRef({on: initialOn})
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const {on} = state

  const toggle = () => dispatch({type: actionTypes.toggle})
  const reset = () => dispatch({type: actionTypes.reset, initialState})

  // получаем нужные пропсы
  function getTogglerProps({onClick, ...props} = {}) {
    return {
      'aria-pressed': on,
      onClick: callAll(onClick, toggle),
      ...props,
    }
  }

  function getResetterProps({onClick, ...props} = {}) {
    return {
      onClick: callAll(onClick, reset),
      ...props,
    }
  }

  return {
    on,
    reset,
    toggle,
    getTogglerProps,
    getResetterProps,
  }
}
// export {useToggle, toggleReducer, actionTypes}

// import {useToggle, toggleReducer, actionTypes} from './use-toggle'

function App() {
  const [timesClicked, setTimesClicked] = React.useState(0)
  const clickedTooMuch = timesClicked >= 4

  function toggleStateReducer(state, action) {
    // добавляем наш кастомный action
    if (action.type === actionTypes.toggle && clickedTooMuch) {
      return {on: state.on}
    }
    return toggleReducer(state, action)
  }

  const {on, getTogglerProps, getResetterProps} = useToggle({
    reducer: toggleStateReducer,
  })

  return (
    <div>
      <Switch
        {...getTogglerProps({
          disabled: clickedTooMuch,
          on: on,
          onClick: () => setTimesClicked(count => count + 1),
        })}
      />
      {clickedTooMuch ? (
        <div data-testid="notice">
          Whoa, you clicked too much!
        </div>
      ) : timesClicked > 0 ? (
        <div data-testid="click-count">Click count: {timesClicked}</div>
      ) : null}
      <button {...getResetterProps({onClick: () => setTimesClicked(0)})}>
        Reset
      </button>
    </div>
  )
}

Производительность

Разделение кода (lazy loading, Coverage)

Разделение кода действует исходя из правила, что загрузка меньшего количества кода ускорит ваше приложение. Например, зададимся вопросом - действительно ли нам нужен код (например, для диаграммы), когда пользователь загружает страницу логина? Нет, мы можем загрузить это код по запросу.

К счастью для нас, есть встроенный способ сделать это с помощью стандартов JavaScript. Это называется динамическим импортом, и его синтаксис выглядит следующим образом:

import('/some-module.js').then(
  module => {
    // работаем с module
  },
  error => {
    // error при загрузке модуля
  },
)

Простой запуск ES-модулей в браузере

React имеет встроенную поддержку загрузки модулей как компонентов React. Модуль должен иметь компонент React в качестве экспорта по умолчанию, и вы должны использовать компонент <React.Suspense /> для отображения резервного значения, пока пользователь ожидает загрузки модуля.

// smiley-face.js
import * as React from 'react'

function SmileyFace() {
    return <div>😃</div>
}

export default SmileyFace

// app.js
import * as React from 'react'

const SmileyFace = React.lazy(() => import('./smiley-face'))

function App() {
    return (
      <div>
        <React.Suspense fallback={<div>loading...</div>}>
          <SmileyFace />
        </React.Suspense>
      </div>
  )
}

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

More tools (3 вертикальные точки) - Coverage.
Таблица на вкладке Coverage показывает, какие файлы были проанализированы, и сколько кода используется в каждом файле. Щелкните по строке, чтобы открыть нужный файл на панели Sources и просмотреть построчную разбивку используемого (красный фон) и неиспользуемого кода.

Не имеет значения, сколько раз вы вызываете import ('./ path-to-module'), webpack фактически загрузит модуль только один раз.

webpack - magic комментарии и prefetch import

Если вы используете webpack для вашего приложения, вы можете использовать магические комментарии webpack, чтобы webpack проинформировал браузер об prefetch импорте:

import(/* webpackPrefetch: true */ './some-module.js')

Когда webpack видит этот комментарий, он добавляет в head вашего документа:

<link rel="prefetch" as="script" href="/static/js/1.chunk.js">

prefetch сообщает браузеру загрузить и кэшировать ресурс (например, скрипт или таблицу стилей) в фоновом режиме. Само изменение минимально, но запустите DevTools, чтобы убедиться, что он загружается правильно (вам нужно снять флажок с кнопки Disable cache, чтобы видеть любые изменения).

const Globe = React.lazy(() => {
    return import(/* webpackPrefetch: true */ '../globe');
})

function App() {
    // showGlobe - флаг на чекбоксе для показа Globe
    return (
        <React.Suspense fallback={<div>загрузка...</div>}>
            {showGlobe ? <Globe /> : null}
        </React.Suspense>
    )
}

useMemo для дорогостоящих вычислений (вкладка Performance)

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

function Distance({x, y}) {
    const distance = calculateDistance(x, y)
    return (
      <div>
        The distance between {x} and {y} is {distance}.
    </div>
  )
}

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

useMemo от React исправляет вышеописанную проблему:

function Distance({x, y}) {
    const distance = React.useMemo(() => calculateDistance(x, y), [x, y])
    return (
      <div>
        The distance between {x} and {y} is {distance}.
    </div>
  )
}

Перерасчет функции будет происходить лишь при изменении зависимостей ([x, y]).

useMemo (и его родственник useCallback) имеет нюансы и не должен применяться повсеместно. Подробнее в когда использовать useMemo и useCallback

Для отладки тяжелых вычислений понадобится вкладка Performance:

Performance - Шестеренка - CPU - 6x slowdown (замедляем cpu) - Record - Выделяете промежуток на графике - Изучаете время выполняемых функций (и рефакторите, убирая вызов функций (useMemo), если они не нужны)

function App() {
    const [inputValue, setInputValue] = React.useState('')

    // получаем все  итемы отфильтр-е по inputValue
    const allItems = React.useMemo(() => {
        return getItems(inputValue)
    }, [inputValue]);

  // ..код компонента
}

useful: обратить внимание на пакет matchSorter

    // allItems - массив
    // filter - на что фильтруем
    //  keys: ['name'] - по какому ключу
  return matchSorter(allItems, filter, {
    keys: ['name'],
  })

production режим

Для того чтобы запустить приложение в production режиме нужно запустить:

npm run build
npm run serve

Поместить getItems в веб-воркер

Увеличиваем скорость вашего приложение при помощи web-workers

React.memo для уменьшения ненужных ре-рендеров

Жизненный цикл приложения React:

→  render → reconciliation → commit
         ↖                   ↙
              state change

Определим несколько терминов:

  • Этап «render»: создание элементов React React.createElement
  • Этап «reconciliation»: сравнение предыдущих элементов с новыми
  • Фаза «commit»: обновление DOM (при необходимости)

Компонент React может ре-рендерится по любой из следующих причин:

  • Его props изменились
  • Его внутреннее state изменилось
  • Он потребляет значения контекста (context), которые изменились
  • Его родитель ре-рендерится

React достаточно быстр, однако иногда может быть полезно подсказать React об определенных частях дерева при обновлении состояния. Например, можно отказаться от обновлений состояния для части дерева React, используя одну из встроенных утилит React: React.PureComponent, React.memo или shouldComponentUpdate.

Для лучшего понимания может пригодиться статья - фиксим медленный render before прежде чем фиксить re-render

React.PureComponent предназначен для компонентов класса, а React.memo - для компонентов функций, и по умолчанию они делают в основном одно и тоже. Они делают так, чтобы ваш компонент не ре-рендерился просто потому, что его родительский компонент перерисовал, что может улучшить производительность вашего приложения в целом.

function CountButton({count, onClick}) {
    return <button onClick={onClick}>{count}</button>
}

function NameInput({name, onNameChange}) {
    return (
      <label>
        Name: <input value={name} onChange={e => onNameChange(e.target.value)} />
    </label>
  )
}
NameInput = React.memo(NameInput)

function Example() {
    const [name, setName] = React.useState('')
    const [count, setCount] = React.useState(0)
    const increment = () => setCount(c => c + 1)
    return (
      <div>
        <div>
          <CountButton count={count} onClick={increment} />
      </div>
      <div>
        <NameInput name={name} onNameChange={setName} />
      </div>
      {name ? <div>{`${name}'s favorite number is ${count}`}</div> : null}
    </div>
  )
}

Теперь компонент NameInput не будет ре-рендериться при клике по кнопке счетчика компонента CountButton.

React.memo полагается на неменяющиеся пропсы, чтобы предотвратить ненужную визуализацию. Вот почему мы не мемоизировали CountButton, родительский компонент повторно инициализирует increment при каждом рендеринге, но React.memo полагается на неменяющиеся пропсы при каждом вызове, чтобы предотвратить ненужные рендеры. Это не сработало бы без обертывания increment в React.useCallback.

React.memo может принимать второй аргумент, который представляет собой настраиваемую функцию сравнения, которая позволяет нам сравнивать пропсы - ф-я возвращает true, если рендеринг не нужен, и false, если рендеринг необходим.

ListItem = React.memo(ListItem, (prevProps, nextProps) => {
    // true означает НЕ рендерить
    // false означает рендерить

    // these ones are easy if any of these changed, we should re-render
    if (prevProps.getItemProps !== nextProps.getItemProps) return false
    if (prevProps.item !== nextProps.item) return false
    if (prevProps.index !== nextProps.index) return false
    if (prevProps.selectedItem !== nextProps.selectedItem) return false

    // this is trickier. We should only re-render if this list item:
    // 1. was highlighted before and now it's not
    // 2. was not highlighted before and now it is
    if (prevProps.highlightedIndex !== nextProps.highlightedIndex) {
        const wasPrevHighlighted = prevProps.highlightedIndex === prevProps.index
        const isNowHighlighted = nextProps.highlightedIndex === nextProps.index
        return wasPrevHighlighted === isNowHighlighted
    }
    return true
})

Передаем только примитивные значения

Чтобы не писать функции сравнения (см. выше) можно передавать примитивные значения компоненту и React сделает всю остальную работу.

<ListItem
    key={item.id}

    isSelected={selectedItem?.id === item.id}
    isHighlighted={highlightedIndex === index}
    >
        {item.name}
</ListItem>

react-virtual

К сожалению, React мало что может сделать, если вам нужно внести огромные обновления в DOM. Нет более чучшего UI, который выявляет эти проблемы лучше, чем сетки, таблицы и списки с большим количеством данных.

Лучшим решением будет показывать лишь видимую часть списка, анпример, а остальную загружать при скроллинге - такую концепцию называют 'windowing'. В экосистеме React есть различные библиотеки для решения этой проблемы. Мой личный фаворит - react-virtual. Вот пример того, как вы можете адаптировать список с использованием хука useVirtual (react-virtual):

// before
function MyListOfData({items}) {
  return (
    <ul style={{height: 300}}>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}
// after
function MyListOfData({items}) {
    const listRef = React.useRef();
    const rowVirtualizer = useVirtual({
        size: items.length,
        parentRef: listRef,
        estimateSize: React.useCallback(() => 20, []),
        overscan: 10,
    });

    return (
        <ul ref={listRef} style={{position: 'relative', height: 300}}>
            <li style={{height: rowVirtualizer.totalSize}} />
            {rowVirtualizer.virtualItems.map(({index, size, start}) => {
                const item = items[index];
                return (
                <li
                    key={item.id}
                    style={{
                        position: 'absolute',
                        top: 0,
                        left: 0,
                        width: '100%',
                        height: size,
                        transform: `translateY(${start}px)`,
                    }}
                >
                    {item.name}
                </li>
            )
            })}
        </ul>
    )
}

Таким образом, вместо того, чтобы перебирать все элементы в вашем списке, вы просто задаете useVirtual:
сколько строк находится в вашем списке; передаете колбэк, для определения размера.
Затем он вернет вам virtualItems и totalSize, которые вы затем можете использовать для визуализации только тех элементов, которые пользователь должен иметь возможность видеть в окне.
react-virtual имеет несколько действительно потрясающих возможностей для всех видов списков (включая сетки).

Оптимизируем значение контекста

Принцип работы контекста заключается в том, что всякий раз, когда предоставленное значение изменяется от одного рендеринга к другому, он запускает повторный рендеринг всех компонентов-потребителий (которые будут отрендерины независимо от того, мемоизированы они или нет).

const CountContext = React.createContext()

function CountProvider(props) {
    const [count, setCount] = React.useState(0)
    const value = [count, setCount]
    return <CountContext.Provider value={value} {...props} />
}

Каждый раз при рендере CountProvider значение переменной count становится новым, поэтому, даже если само значениеcount останется неизменным, все компоненты-потребители будут отрендерены.

Быстрое и простое решение этой проблемы - мемоизировать значение, которое вы предоставляете provider контекста:

const CountContext = React.createContext()

function CountProvider(props) {
    const [count, setCount] = React.useState(0)
    const value = React.useMemo(() => [count, setCount], [count])
    // const value = [count, setCount];
    return <CountContext.Provider value={value} {...props} />
}

Разделяем контекст

Если state и dispatch не зависят друг от друга, мы можем разделить их на независимые контексты, и для тех компонентов, где не нужен state, мы будем получать только dispatch, таким образом компнент не будет рендериться при изменения state.

function AppProvider({children}) {
    const [state, dispatch] = React.useReducer(appReducer, {
        dogName: '',
        grid: initialGrid,
    })
    return ( // 2 провайдера:
        <AppStateContext.Provider value={state}>
            <AppDispatchContext.Provider value={dispatch}>
                {children}
            </AppDispatchContext.Provider>
        </AppStateContext.Provider>
    )
}

// хук для получания State
function useAppState() {
    const context = React.useContext(AppStateContext)
    if (!context) {
        throw new Error('useAppState must be used within the AppProvider')
    }
    return context
}
// хук для получания Dispatch
function useAppDispatch() {
    const context = React.useContext(AppDispatchContext)
    if (!context) {
        throw new Error('useAppDispatch must be used within the AppProvider')
    }
    return context
}


// компонент Grid
function Grid() {
    // было
    const [, dispatch] = useAppState()
    // стало (получаем только Dispatch)
    const dispatch = useAppDispatch()
    //...
}
// компонент Cell - получаем и state и dispatch
function Cell({row, column}) {
    const state = useAppState()
    const cell = state.grid[row][column]
    const dispatch = useAppDispatch()
    //...
}

State Colocation(разделение)

State Colocation сделает ваше React приложение быстрее; State Colocation - разделение контекста(см. выше), при этом мы можем получать значения лишь для нужного компонента( DogProvider для DogNameInput:

<div>
    <DogProvider>
      <DogNameInput />
    </DogProvider>
    <AppProvider>
      <Grid />
    </AppProvider>
</div>

или удаление определенного state (если он завязан, например, только на 1 компонент) из глобального Reducer непосредственно в компонент, тем самым мы локализуем обновление state и предотвращаем рендер сторонних компонентов:

function DogNameInput() {
    const [dogName, setDogName] = React.useState('')

    function handleChange(event) {
        const newDogName = event.target.value
        setDogName(newDogName)
    }
    //...
}

По материалам - github.com/kentcdodds

Добавить комментарий