Скажем, у меня есть 3 входа: скорость, sendAmount и receiveAmount. Я поместил эти 3 входа в параметры различий useEffect. Правила следующие:
receiveAmount = sendAmount * ratesendAmount = receiveAmount / ratereceiveAmount = sendAmount * rate, когда sendAmount > 0, или я вычисляю sendAmount = receiveAmount / rate, когда receiveAmount > 0Вот коды и ящик https://codesandbox.io/s/pkl6vn7x6j, чтобы продемонстрировать проблему.
Есть ли способ сравнить oldValues и newValues, как на componentDidUpdate, вместо того, чтобы делать 3 обработчика для этого случая?
Спасибо
Вот мое окончательное решение с usePrevioushttps://codesandbox.io/s/30n01w2r06
В этом случае я не могу использовать несколько useEffect, потому что каждое изменение приводит к одному и тому же сетевому вызову. Вот почему я также использую changeCount для отслеживания изменений. Этот changeCount также полезен для отслеживания изменений только локально, поэтому я могу предотвратить ненужные сетевые вызовы из-за изменений с сервера.
Я добавил ответ с двумя дополнительными решениями. Один из них вам подходит?





Вы можете написать собственный хук, чтобы предоставить вам предыдущие реквизиты с использованием useRef
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
а затем использовать его в useEffect
const Component = (props) => {
const {receiveAmount, sendAmount } = props
const prevAmount = usePrevious({receiveAmount, sendAmount});
useEffect(() => {
if (prevAmount.receiveAmount !== receiveAmount) {
// process here
}
if (prevAmount.sendAmount !== sendAmount) {
// process here
}
}, [receiveAmount, sendAmount])
}
Однако его яснее и, вероятно, лучше и яснее для чтения и понимания, если вы используете два useEffect отдельно для каждого идентификатора изменения, который вы хотите обрабатывать их отдельно.
Благодарим за замечание об использовании двух вызовов useEffect по отдельности. Не знал, что можно использовать его несколько раз!
Я пробовал приведенный выше код, но eslint предупреждает меня, что у useEffect отсутствуют зависимости prevAmount
Поскольку prevAmount содержит значение для предыдущего значения state / props, вам не нужно передавать его как зависимость для useEffect, и вы можете отключить это предупреждение для конкретного случая. Вы можете прочитать этот пост для более подробной информации?
Люблю это - useRef всегда приходит на помощь.
@ShubhamKhatri: В чем причина обертывания useEffect ref.current = value?
Ссылки @curly_brackets не изменяют ссылки между рендерингами, и поскольку мы изменяем объект ref, устанавливая ключ current, если он не вызывает повторного рендеринга, тем самым обеспечивая предыдущее значение во время следующего повторного рендеринга
также хорошо объяснено здесь blog.logrocket.com/…
Будет ли && работать или будет запускаться useEffect один раз для каждой переменной в разное время? if (prevAmount.receiveAmount !== receiveAmount && prevAmount.sendAmount !== sendAmount)
В usePrevious, разве useEffect не должен зависеть от value? В противном случае, если компонент будет повторно визуализирован из-за другого изменения состояния, в следующем рендере previousValue будет равен value, верно? Или я что-то упускаю?
это круто! но это не сработает только в первый раз, когда используется usePrevious?
react становится все более ужасным: люди используют useRef только для получения предыдущих реквизитов (!) И не заботятся о том, сколько это будет стоить.
Черт, это похоже на колдовство. Красивый.
Это прекрасно, потому что useRef сохраняет одно и то же значение для двух последовательных визуализаций, даже если зависимость изменяется. В результате сохраняется предыдущее значение.
Нет необходимости использовать функцию usePrevious (), если код в функции Component должен быть таким, как указано выше. Итак, установка значения useRef в функции Component useEffect выполнит свою работу. const Component = (реквизиты) => {const {someProp} = реквизиты; const ref = useRef (); // значение ref не изменится при изменении состояния. useEffect (() => {if (ref.current! == someProp) {// ... ref.current = someProp;}}, [someProp]); }
Поскольку состояние не тесно связано с экземпляром компонента в функциональных компонентах, предыдущее состояние не может быть достигнуто в useEffect без его предварительного сохранения, например, с помощью useRef. Это также означает, что обновление состояния, возможно, было неправильно реализовано в неправильном месте, поскольку предыдущее состояние доступно в функции средства обновления setState.
Это хороший вариант использования useReducer, который предоставляет хранилище в стиле Redux и позволяет реализовать соответствующий шаблон. Обновления состояния выполняются явно, поэтому нет необходимости выяснять, какое свойство состояния обновляется; это уже ясно из отправленного действия.
Вот как это может выглядеть пример:
function reducer({ sendAmount, receiveAmount, rate }, action) {
switch (action.type) {
case "sendAmount":
sendAmount = action.payload;
return {
sendAmount,
receiveAmount: sendAmount * rate,
rate
};
case "receiveAmount":
receiveAmount = action.payload;
return {
sendAmount: receiveAmount / rate,
receiveAmount,
rate
};
case "rate":
rate = action.payload;
return {
sendAmount: receiveAmount ? receiveAmount / rate : sendAmount,
receiveAmount: sendAmount ? sendAmount * rate : receiveAmount,
rate
};
default:
throw new Error();
}
}
function handleChangeEvent(e) {
const { name, value } = e.target;
dispatch({
type: name,
payload: value
});
}
...
const [state, dispatch] = useReducer(reducer, {
rate: 2,
sendAmount: 0,
receiveAmount: 0
});
...
Привет @estus, спасибо за идею. Это дает мне другой образ мышления. Но я забыл упомянуть, что мне нужно вызывать API для каждого случая. У вас есть какое-нибудь решение?
Что ты имеешь в виду? Внутренняя ручка Означает ли «API» удаленную конечную точку API? Можете ли вы обновить коды из ответа с подробностями?
Из обновления я могу предположить, что вы хотите получать sendAmount и т. д. Асинхронно, а не вычислять их, верно? Это многое меняет. Это можно сделать с useReduce, но это может быть сложно. Если вы имели дело с Redux раньше, вы можете знать, что асинхронные операции там не просты. github.com/reduxjs/redux-thunk - популярное расширение для Redux, которое позволяет выполнять асинхронные действия. Вот демонстрация, которая дополняет useReducer тем же шаблоном (useThunkReducer), codeandbox.io/s/6z4r79ymwr. Обратите внимание, что отправленная функция - это async, вы можете делать запросы там (прокомментировано).
Проблема с вопросом в том, что вы спросили о другом, что не было вашим реальным случаем, а затем изменили его, так что теперь это совсем другой вопрос, в то время как существующие ответы выглядят так, как будто они просто проигнорировали вопрос. Это не рекомендуется для SO, потому что это не поможет вам получить желаемый ответ. Пожалуйста, не удаляйте важные части из вопроса, ответы на который уже относятся (формулы расчета). Если у вас все еще есть проблемы (после моего предыдущего комментария (а вы, вероятно, так и есть), подумайте о том, чтобы задать новый вопрос, который отражает ваш случай, и укажите ссылку на него как на свою предыдущую попытку.
Привет, эстус, я думаю этот кодовый ящик codeandbox.io/s/6z4r79ymwr еще не обновлен. Я верну вопрос обратно, извините за это.
Да, после разветвления автоматически не сохранялось. Я его обновил.
const Component = (props) => {
useEffect(() => {
console.info("val1 has changed");
}, [val1]);
return <div>...</div>;
};
Сравнение текущего значения с предыдущим значением является распространенным шаблоном и оправдывает собственный собственный крючок, скрывающий детали реализации.
const Component = (props) => {
const hasVal1Changed = useHasChanged(val1)
useEffect(() => {
if (hasVal1Changed ) {
console.info("val1 has changed");
}
});
return <div>...</div>;
};
const useHasChanged= (val: any) => {
const prevVal = usePrevious(val)
return prevVal !== val
}
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Второй вариант у меня сработал. Не могли бы вы объяснить мне, почему я должен писать useEffect Twice?
@TarunNagpal, вам не обязательно использовать useEffect дважды. Это зависит от вашего варианта использования. Представьте, что мы просто хотим записать, какая val изменилась. Если у нас есть как val1, так и val2 в нашем массиве зависимостей, он будет запускаться каждый раз, когда любое из значений изменится. Затем внутри функции, которую мы передаем, нам нужно будет выяснить, какой val был изменен, чтобы записать правильное сообщение.
Спасибо за ответ. Когда вы говорите «внутри функции, которую мы передаем, нам нужно выяснить, у какого val есть изменения». Как мы можем этого добиться. Пожалуйста, направьте
Спасибо, вариант 2 - именно то, что мне нужно
На случай, если кто-то ищет версию использования TypeScript.
В модуле .tsx:
import { useEffect, useRef } from "react";
const usePrevious = <T extends unknown>(value: T): T | undefined => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
Или в модуле .ts:
import { useEffect, useRef } from "react";
const usePrevious = <T>(value: T): T | undefined => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
Обратите внимание, что это не будет работать в файле TSX, чтобы заставить это работать, измените файл TSX на const usePrevious = <T extends any>(..., чтобы интерпретатор увидел, что <T> не JSX и является общим ограничением
Вы можете объяснить, почему <T extends {}> не работает. Кажется, это работает для меня, но я пытаюсь понять сложность его использования таким образом.
Файлы .tsx содержат jsx, который должен быть идентичен html. Возможно, универсальный <T> является незакрытым тегом компонента.
А как насчет возвращаемого типа? Я получаю Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
@SabaAhang Извини! TS будет использовать вывод типа для обработки возвращаемых типов по большей части, но если у вас включен линтинг, я добавил тип возвращаемого значения, чтобы избавиться от предупреждений.
@SabaAhang Я настоятельно рекомендую использовать VSCode для работы TS, также, если вы удерживаете контроль и нажимаете на имена функций, вы получаете их ТОЧНЫЕ типы, поэтому такие вещи тривиально найти. Языковой сервер TS отлично справляется с документами и подписями типов.
Лучше расширить unknown или просто поместить ловушку в файл .ts. Если вы расширите {}, вы получите ошибки, если пропустите указание T в usePrevious<T>.
@fgblomqvist С момента написания этого поста я многое узнал о TS. Мне нужно обновить это, чтобы отразить это. Спасибо за комментарий!
np, и да, всегда приятно держать его свежим / правильным / актуальным, так как многие тысячи людей используют эти ответы как ссылки :)
@fgblomqvist Обновил мой ответ. Еще раз спасибо за отзыв.
Чтобы он работал в файле TSX, вы также можете написать <T,>, чтобы отличаться от синтаксиса JSX (сфокусируйтесь на конечной запятой)
@Corvince Действует ли это в более старых версиях tsc, не думаю, что я видел это
Для действительно простого сравнения опор вы можете использовать useEffect, чтобы легко проверить, обновилась ли опора.
const myComponent = ({ prop }) => {
useEffect(() => {
---Do stuffhere----
}, [prop])
}
useEffect будет запускать ваш код только в том случае, если свойство изменится.
Это работает только один раз, после последовательных изменений prop вы не сможете использовать ~ do stuff here ~.
Похоже, вам следует вызвать в setHasPropChanged(false) в конце ~ do stuff here ~, чтобы «сбросить» ваше состояние. (Но это будет сброшено при дополнительном повторном рендеринге)
Спасибо за отзыв, вы оба правы, обновленное решение
@ AntonioPavicevac-Ortiz Я обновил ответ, чтобы теперь отображать propHasChanged как true, который затем вызывает его один раз при рендеринге, может быть лучшим решением, просто вырвать useEffect и просто проверить опору
Я думаю, что мое первоначальное использование этого было утеряно. Оглядываясь назад на код, вы можете просто использовать useEffect
Это просто и умно. Почему я не подумал об этом, смеется :) Хорошая работа.
Использование Ref внесет в приложение новый вид ошибки.
Давайте посмотрим на этот случай с использованием usePrevious, который кто-то ранее прокомментировал:
Как мы видим здесь, мы не обновляем внутренний ref, потому что мы используем useEffect.
У React есть этот пример, но они используют state ... а не props ... когда вы заботитесь о старых реквизитах, произойдет ошибка.
Я только что опубликовал реакция-дельта, который решает именно такой сценарий. На мой взгляд, у useEffect слишком много обязанностей.
Object.is.react-delta разбивает обязанности useEffect на несколько более мелких крючков.
Ответственность # 1
usePrevious(value)useLatest(value)useDelta(value, options)useDeltaArray(valueArray, options)useDeltaObject(valueObject, options)some(deltaArray)every(deltaArray)Ответственность # 2
По моему опыту, этот подход более гибкий, чистый и лаконичный, чем решения useEffect / useRef.
Именно то, что я ищу. Собираюсь попробовать!
@Hisato, это могло быть излишним. Это что-то вроде экспериментального API. И, честно говоря, я мало использовал его в своих командах, потому что он не получил широкой известности и не принят. В теории это звучит неплохо, но на практике оно того не стоит.
Исходя из принятого ответа, альтернативного решения, не требующего настраиваемого крючка:
const Component = ({ receiveAmount, sendAmount }) => {
const prevAmount = useRef({ receiveAmount, sendAmount }).current;
useEffect(() => {
if (prevAmount.receiveAmount !== receiveAmount) {
// process here
}
if (prevAmount.sendAmount !== sendAmount) {
// process here
}
return () => {
prevAmount.receiveAmount = receiveAmount;
prevAmount.sendAmount = sendAmount;
};
}, [receiveAmount, sendAmount]);
};
Это предполагает, что вам действительно нужна ссылка на предыдущие значения для чего-либо в битах «процесс здесь». В противном случае, если ваши условия не выходят за рамки прямого сравнения !==, самым простым решением здесь было бы просто:
const Component = ({ receiveAmount, sendAmount }) => {
useEffect(() => {
// process here
}, [receiveAmount]);
useEffect(() => {
// process here
}, [sendAmount]);
};
Хорошее решение, но содержит синтаксические ошибки. useRef, а не userRef. Забыл использовать текущий prevAmount.current
Это супер круто, если бы я мог проголосовать больше, я бы проголосовал! Также имеет смысл, мой первоначальный ответ пришел из незнания. Я думаю, что это, наверное, самый простой и элегантный узор, который я когда-либо видел.
Вы, вероятно, могли бы абстрагироваться от этого еще дальше, чтобы сделать сверхпростую утилиту.
Это даст вам значения исходный, а не значения предыдущий. Причина, по которой перехватчик usePrevious() работает, заключается в том, что он упакован в useEffect().
@Justin Функция возврата должна обновлять предыдущие значения для каждого рендера. Вы это проверяли?
Это супер чистый ответ, я бы лично выбрал это решение, оно отлично работает в моем проекте. Спасибо !
Вот пользовательский хук, который я использую, который, на мой взгляд, более интуитивно понятен, чем использование usePrevious.
import { useRef, useEffect } from 'react'
// useTransition :: Array a => (a -> Void, a) -> Void
// |_______| |
// | |
// callback deps
//
// The useTransition hook is similar to the useEffect hook. It requires
// a callback function and an array of dependencies. Unlike the useEffect
// hook, the callback function is only called when the dependencies change.
// Hence, it's not called when the component mounts because there is no change
// in the dependencies. The callback function is supplied the previous array of
// dependencies which it can use to perform transition-based effects.
const useTransition = (callback, deps) => {
const func = useRef(null)
useEffect(() => {
func.current = callback
}, [callback])
const args = useRef(null)
useEffect(() => {
if (args.current !== null) func.current(...args.current)
args.current = deps
}, deps)
}
Вы бы использовали useTransition следующим образом.
useTransition((prevRate, prevSendAmount, prevReceiveAmount) => {
if (sendAmount !== prevSendAmount || rate !== prevRate && sendAmount > 0) {
const newReceiveAmount = sendAmount * rate
// do something
} else {
const newSendAmount = receiveAmount / rate
// do something
}
}, [rate, sendAmount, receiveAmount])
Надеюсь, это поможет.
Если вы предпочитаете подход замены useEffect:
const usePreviousEffect = (fn, inputs = []) => {
const previousInputsRef = useRef([...inputs])
useEffect(() => {
fn(previousInputsRef.current)
previousInputsRef.current = [...inputs]
}, inputs)
}
И используйте это так:
usePreviousEffect(
([prevReceiveAmount, prevSendAmount]) => {
if (prevReceiveAmount !== receiveAmount) // side effect here
if (prevSendAmount !== sendAmount) // side effect here
},
[receiveAmount, sendAmount]
)
Обратите внимание, что за время выполнения эффекта первый предыдущие значения, переданные в ваш fn, будут такими же, как ваши исходные входные значения. Это будет иметь значение для вас только в том случае, если вы захотите что-то сделать при изменении значения не.
Вы можете использовать useImmer вместо useState и получить доступ к состоянию. Пример: https://css-tricks.com/build-a-chat-app-using-react-hooks-in-100-lines-of-code/
В вашем случае (простой объект):
useEffect(()=>{
// your logic
}, [rate, sendAmount, receiveAmount])
В другом случае (сложный объект)
const {cityInfo} = props;
useEffect(()=>{
// some logic
}, [cityInfo.cityId])
Мне не понравился ни один из приведенных выше ответов, мне нужна была возможность передавать массив логических значений, и если одно из них истинно, поэтому повторно выполните рендеринг
/**
* effect fires if one of the conditions in the dependency array is true
*/
export const useEffectCompare = (callback: () => void, conditions: boolean[], effect = useEffect) => {
const shouldUpdate = useRef(false);
if (conditions.some((cond) => cond)) shouldUpdate.current = !shouldUpdate.current;
effect(callback, [shouldUpdate.current]);
};
//usage - will fire because one of the dependencies is true.
useEffectCompare(() => {
console.info('test!');
}, [false, true]);
Как именно componentDidUpdate должен помочь? Вам все равно нужно будет написать эти 3 условия.