Согласно документам:
componentDidUpdate()is invoked immediately after updating occurs. This method is not called for the initial render.
Мы можем использовать новый хук useEffect() для имитации componentDidUpdate(), но похоже, что useEffect() запускается после каждого рендеринга, даже в первый раз. Как мне заставить его не запускаться при первоначальном рендеринге?
Как вы можете видеть в примере ниже, componentDidUpdateFunction печатается во время первоначального рендеринга, но componentDidUpdateClass не печатается во время первоначального рендеринга.
function ComponentDidUpdateFunction() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.info("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick = {() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
class ComponentDidUpdateClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidUpdate() {
console.info("componentDidUpdateClass");
}
render() {
return (
<div>
<p>componentDidUpdateClass: {this.state.count} times</p>
<button
onClick = {() => {
this.setState({ count: this.state.count + 1 });
}}
>
Click Me
</button>
</div>
);
}
}
ReactDOM.render(
<div>
<ComponentDidUpdateFunction />
<ComponentDidUpdateClass />
</div>,
document.querySelector("#app")
);<script src = "https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id = "app"></div>@Aprillion, в моем случае измените содержимое H2, у которого есть текст, который необходимо изменить после списка элементов, пуст и даже был другим в начале. Тот же список также пуст в начале перед выборкой данных из API, поэтому при нормальном условном рендеринге на основе длины массива начальное значение переопределяется



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Мы можем использовать ловушку useRef для хранения любого изменяемого значения, которое нам нравится., поэтому мы могли бы использовать это, чтобы отслеживать, запускается ли функция useEffect в первый раз.
Если мы хотим, чтобы эффект выполнялся в той же фазе, что и componentDidUpdate, мы можем вместо этого использовать useLayoutEffect.
Пример
const { useState, useRef, useLayoutEffect } = React;
function ComponentDidUpdateFunction() {
const [count, setCount] = useState(0);
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
console.info("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick = {() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdateFunction />,
document.getElementById("app")
);<script src = "https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id = "app"></div>Я попытался заменить useRef на useState, но использование установщика вызвало повторный рендеринг, чего не происходит при назначении на firstUpdate.current, поэтому я думаю, что это единственный хороший способ :)
Может ли кто-нибудь объяснить, зачем использовать эффект макета, если мы не изменяем и не измеряем DOM?
@ZenVentzi В этом примере это не обязательно, но вопрос был в том, как имитировать componentDidUpdate с помощью хуков, поэтому я его и использовал.
Я создал собственный хук здесь на основе этого ответа. Спасибо за реализацию!
Вы можете превратить его в нестандартные хуки, вот так:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Пример использования:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...
Этот подход вызывает предупреждения о том, что список зависимостей не является литералом массива.
Я использую этот хук в своих проектах, и я не видел никаких предупреждений, не могли бы вы предоставить дополнительную информацию?
@vsync Вы думаете о другом случае, когда хотите запустить эффект один раз при первоначальном рендеринге и никогда больше
@vsync В разделе примечаний reactjs.org/docs/… специально сказано: «Если вы хотите запустить эффект и очистить его только один раз (при монтировании и размонтировании), вы можете передать пустой массив ([]) в качестве второго аргумента». Это соответствует наблюдаемому мной поведению.
Я также получаю предупреждения о том, что список зависимостей не является литералом массива и отсутствует зависимость: 'func'. Правило линтера, отмеченное для обоих, - это react-hooks / excustive-deps.
@ plong0 Хорошо, я протестировал упомянутый код (в своем ответе), и я не получил никакого предупреждения, я думаю, что это связано с версией реакции, которую вы используете, или вашей зависимостью (state.key в моем ответе), поэтому мне нужно больше информации в чтобы дать вам лучший ответ.
Нет необходимости импортировать React в файл хуков
он забыл добавить: [] вокруг deps ... должно быть что-то вроде [deps] ... но у меня это не работает
@CarlosEduardoOliveraFilho Я не забыл, это не так. когда вы используете этот хук, вы должны передавать зависимости в виде массива, как и сам useEffect. напишите свой код с помощью useEffect, если все работает, просто замените useEffect на useDidMountEffect, и все готово.
@Mehdi о, хорошо, вы правы, извините, deps, полученные в качестве параметра вашего пользовательского хука, уже являются массивом, поэтому вам не нужен []. Спасибо!
@MehdiDehghani, ваше решение работает отлично, одно дополнение, которое вам нужно сделать, это размонтировать, сбросив значение didMount.current на false. Когда пытаться использовать этот настраиваемый хук где-нибудь еще, вы не получите значение кеша.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;
Я не уверен, что это необходимо, поскольку, если компонент все-таки размонтируется, потому что, если он будет перемонтирован, didMount уже будет повторно инициализирован на false.
@CameronYick это необходимо, потому что при использовании Fast Refresh функция очистки будет работать, но значение ref не будет очищено. Это приводит к сбоям во время разработки - но в продакшене это нормально.
@ravi, ваш не вызывает переданную функцию размонтирования. Вот версия, которая немного более полная:
/**
* Identical to React.useEffect, except that it never runs on mount. This is
* the equivalent of the componentDidUpdate lifecycle function.
*
* @param {function:function} effect - A useEffect effect.
* @param {array} [dependencies] - useEffect dependency list.
*/
export const useEffectExceptOnMount = (effect, dependencies) => {
const mounted = React.useRef(false);
React.useEffect(() => {
if (mounted.current) {
const unmount = effect();
return () => unmount && unmount();
} else {
mounted.current = true;
}
}, dependencies);
// Reset on unmount for the next mount.
React.useEffect(() => {
return () => mounted.current = false;
}, []);
};Привет @Whatabrain, как использовать этот настраиваемый перехватчик для передачи списка независимостей? Не пустой, который будет таким же, как componentDidmount, а что-то вроде useEffect(() => {...});
@KevDing Должно быть так же просто, как опустить параметр dependencies при его вызове.
Я сделал простой перехватчик useFirstRender для обработки таких случаев, как фокусирование ввода формы:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
Он начинается как true, затем переключается на false в useEffect, который запускается только один раз и никогда больше.
В своем компоненте используйте его:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
В вашем случае просто используйте if (!firstRender) { ....
Это лучшая реализация, которую я когда-либо создавал с использованием typescript. По сути, идея та же самая, с использованием Ref, но я также рассматриваю обратный вызов, возвращаемый useEffect, для выполнения очистки при размонтировании компонента.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* @param effect
* @param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
Вы можете просто использовать его, как обычно, как и хук useEffect, но на этот раз он не будет работать при первоначальном рендеринге. Вот как вы можете использовать этот крючок.
useuseNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
Тот же подход, что и Толле ответ, но с использованием useState вместо useRef.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
Это был хороший вариант, который позволил мне найти свое решение
Если вы хотите пропустить первую визуализацию, вы можете создать состояние firstRenderDone и установить для него значение true в useEffect с пустым списком зависимостей (который работает как didMount). Затем в другом вашем useEffect вы можете проверить, был ли уже выполнен первый рендеринг, прежде чем что-то делать.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if (firstRenderDone){
console.info("componentDidUpdateFunction");
}
}, [firstRenderDone]);
Все предыдущие хороши, но это может быть достигнуто более простым способом, учитывая, что действие в useEffect можно «пропустить», поместив условие if (или любое другое), которое по сути не запускается в первый раз, но все еще с зависимостью.
Например, у меня был случай:
поэтому моим «решением» было создание другого useState для создания логического значения, которое изменится только после того, как выборка данных сделает другое условие в useEffect истинным, чтобы запустить другую функцию, которая также зависит от продолжительности обхода.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
вот мой App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if (tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.info(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if (loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours = {tours} title = {title} changeTitle = {changeTitle}
removeTours = {removeTours} />
</main>
)
}
}
export default App
Я не вижу разницы в этом подходе, за исключением порядка условий в инструкции if.
Могу я спросить, каков вариант использования, когда имеет смысл делать что-то на основе количества визуализаций, а не явной переменной состояния, такой как
count?