1. useMount

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

useEffect(() => {
// запрос fetch...
}, [])

Довольно просто, но есть один большой недостаток: семантика не достаточно ясна, даже если передается пустой массив.

Поэтому следует настроить хук под названием useMount. Тогда функция обратного вызова будет выполняться только при первом рендеринге компонента.

Исходный код

const useMount = (callback) => {
React.useEffect(callback, [])
}

Пример

const UseMountDemo = () => {
const [count, setCount] = React.useState(0)

useMount(() => {
console.log("useMount")
})

return (
<div>
count: { count }
<button onClick={() => setCount(count++)}>add</button>
</div>
)
}

Когда компонент рендерится повторно, useMount не выполняется снова.

2. useUnmount

Когда компонент “размонтирован” и нужно запустить некоторую логику, например очистить таймер, используется подобный код:

React.useEffect(() => {
return () => {
//Запускать, когда компонент размонтирован
}
}, [])

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

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

Исходный код

Действительно ли все так просто? Посмотрим, как правильно использовать этот хук.

const useUnmount = (callback) => {
React.useEffect(() => {
return callback
}, [])
}

Пример

const Child = () => {
const [ count, setCount ] = useState(0)

useUnmount(() => {
// Мы ожидаем вывод значения num, когда компонент будет размонтирован
console.log('useUnmount', count)
})
return (
<div>
count: {count}
<button onClick={() => setCount(count + 1)}>add</button>
</div>
)
}

const UseUnmountedDemo = () => {
const [showChild, setShowChild] = React.useState(true);
// Мы используем "showChild" для управления отображением и скрытием дочернего компонента
return (
<div>
{ !!showChild && <Child /> }
<button onClick={() => setShowChild(false)}>Destroy child</button>
</div>
)
}

Когда компонент Child удаляется, функция обратного вызова useUnmount выполняется, но обнаруживается, что count  —  это начальное значение 0, а не 10. Что вызывает такой неправильный результат?

Причина этого кроется в механизме закрытия в useEffect, потому что функция передается при выгрузке компонента для первого рендеринга. Чтобы получить состояние в реальном времени, нужно использовать useRef.

const useUnmount = (callback) => {
  const callbackRef = React.useRef(callback)
  
  callbackRef.current = callback
  
  React.useEffect(() => {
    return () => {
      callbackRef.current()
    }
  }, [])
}

Теперь при удалении компонента получаем правильное значение count.

3. useUpdateEffect

Иногда необходимо выполнять логический код только после изменения зависимости. Можно ли мы достичь этой цели, используя такой подход?

function UseUpdateEffectDemo() {
const [count, setCount] = React.useState(0)

React.useEffect(() => {
console.log('count changed', count)
}, [ count ])

return (
<div>
count: {count}
<button onClick={() => setCount(count + 1)}>change</button>
</div>
)
}

Однако count выведет 0, как только компонент будет смонтирован. Как можно выполнить функцию обратного вызова после изменения count?

const useUpdateEffect = function (effectCallback, deps = [])  {
const isFirstMount = React.useRef(false)

React.useEffect(() => {
return () => {
isFirstMount.current = false
}
}, [])
React.useEffect(() => {
// Не выполняйте effectcallback для первого раза
if (!isFirstMount.current) {
isFirstMount.current = true
} else {
return effectCallback()
}
}, deps)
}

function UseUpdateEffectDemo() {
const [count, setCount] = React.useState(0)

useUpdateEffect(() => {
console.log('Count changed', count)
}, [ count ])

return (
<div>
count: {count}
<button onClick={() => setCount(count + 1)}>change</button>
</div>
)
}

4. useSetState

При написании компонента class обычно используется this.setState для манипулирования данными компонента. Между тем setState зачастую более удобен при работе с объектными типами данных.

const [ person, setPerson ] = React.useState({
name: 'fatfish',
age: 100
})
// Изменяем имя
setPerson({
...person,
name: 'medium'
})
// Изменяем возраст
setPerson({
...person,
age: 1000
})
// Используем setState для изменения имени
setState({
name: 'medium'
})
// Используем setState для изменения возраста
setState({
age: 1000
})

Можно ли реализовать хук useSetState, чтобы упростить работу с setPerson? Это очень легко сделать: просто выполните обертку вокруг useState.

const useSetState = (initState) => {
const [ state, setState ] = React.useState(initState)

const setMergeState = (value) => {
setState((prevValue) => {
const newValue = typeof value === 'function' ? value(prevValue) : value
return newValue ? { ...prevValue, ...newValue } : prevValue
})
}

return [ state, setMergeState ]
}

Пример

function useSetStateDemo() {
const [ person, setPerson ] = useSetState({
name: 'fatfish',
age: 100
})
// Изменение значения person в обычном режиме настройки
const onSetName = () => {
setPerson({ name: 'medium' })
}
// Используем функцию обратного вызова для изменения значения person
const onSetAge = () => {
setPerson(() => {
return {
age: 1000
}
})
}
return (
<div>
<p>name: { person.name }</p>
<p>age: { person.age }</p>
<button onClick={onSetName}>change name</button>
<button onClick={onSetAge}>change age</button>
</div>
)
}

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи fatfish: 4 Custom React Hooks Every Developer Should Know

Предыдущая статья12 декораторов Python, которые улучшают код
Следующая статья10 практических примеров использования функций высшего порядка при разработке Android