#javascript #reactjs
#javascript #reactjs
Вопрос:
Я столкнулся с этой ситуацией недавно при разработке некоторой бизнес-логики. Вот упрощенный и надуманный пример из него
function App() {
const [myState, setMyState] = useState([])
useEffect(() => {
setTimeout(() => {
setMyState((prevState) =>
prevState.concat([
{
type: 'foo',
enabled: true,
},
{
type: 'bar',
enabled: true,
},
{
type: 'baz',
enabled: true,
},
])
)
}, 500)
}, [])
return (
<div className="App">
<ChildComponent propFromApp={myState} />
</div>
)
}
Я использовал setTimeout
для моделирования вызова API, и он передал состояние ChildComponent
.
function ChildComponent({ propFromApp }) {
const allTypes = propFromApp.map((item) => item.type)
const [checkedItems, setCheckedItems] = useState(allTypes)
return (
<div>
{allTypes.map((type, index) => (
<div key={index}>
<input
type="checkbox"
name={type}
checked={checkedItems.includes(type)}
onChange={() => {
if (checkedItems.includes(type)) {
setCheckedItems((prevState) =>
prevState.filter((prevType) => type !== prevType)
)
} else {
setCheckedItems((prevState) => prevState.concat(type))
}
}}
/>
<label htmlFor={type}>{type}</label>
</div>
))}
</div>
)
}
И ChildComponent
отвечает за рендеринг множества флажков на основе реквизита. Сначала он извлек массив type
из prop, который вызывается allTypes
, а затем установил свое собственное состояние, checkedItems
используя это allTypes
в качестве начального значения. таким образом, изначально проверяются все элементы. Причина, по которой я заставляю ChildComponent
сохранять их как свое собственное состояние, заключается в том, что компонент может таким образом управлять ими с помощью флажка.
Однако, поскольку propFromApp
изначально это пустой массив, состояние ChildComponent
so checkedItems
также инициализируется как пустой массив и не будет обновляться, когда реальные данные возвращаются из вызова API, что происходит через 500 мс после запуска обратного вызова в setTimeout
.
Итак, затем я использую useEffect
для обновления checkedItems
при обновлении реквизита.
итак, теперь ChildComponent
выглядит так
function ChildComponent({ propFromApp }) {
const allTypes = propFromApp.map((item) => item.type)
const [checkedItems, setCheckedItems] = useState(allTypes)
useEffect(() => {
setCheckedItems(allTypes)
}, [allTypes])
return (....)
Однако это привело к ошибке
Превышена максимальная глубина обновления. Это может произойти, когда компонент вызывает setState внутри useEffect, но useEffect либо не имеет массива зависимостей, либо одна из зависимостей изменяется при каждом рендеринге.
Я полагал, что это потому, что allTypes
это не один из примитивных типов, поэтому проверка равенства всегда определяла бы, что allTypes
отличается каждый раз, когда компонент повторно отображает. Итак, наконец, я должен изменить его на using useMemo
, чтобы избежать проблемы. Теперь ChildComponent
выглядит следующим образом
function ChildComponent({ propFromApp }) {
const allTypes = useMemo(() => propFromApp.map((item) => item.type), [
propFromApp,
])
const [checkedItems, setCheckedItems] = useState(allTypes)
useEffect(() => {
setCheckedItems(allTypes)
}, [allTypes])
return (....)
Теперь это решает проблему. но я не уверен, что это правильный способ сделать это или это хорошая практика. Кто-нибудь может предложить лучшую альтернативу или указать, где мой код недостаточно хорош?
Вот живой купол https://codesandbox.io/s/async-sky-3vl1r?file=/src/App.js
Ответ №1:
useMemo
в этом контексте обычно проблема решается, но может и не решаться. useMemo
может пересчитывать или не пересчитывать его значение на основе некоторой логики управления памятью, написанной людьми умнее меня. Поэтому вам нужно будет написать свой код, имея в виду, что ссылочный идентификатор сохраненного значения может измениться. Изменение ссылочного идентификатора означает, что массивы deps изменятся.
Его можно использовать useMemo
для исправления бесконечного цикла. В большинстве случаев пересчет вызывает дополнительный повторный запуск и ничего больше.
Нехорошо useMemo
влиять на логику приложения. В вашем примере пересчет будет сброшен checkedItems
, в то время как пересчет не выполняется. Чтобы устранить проблему, отложите получение значений до тех пор, пока вам не понадобится производная.
const [checkedItems, setCheckedItems] = useState(() => propFromApp.map((item) => item.type))
useEffect(() => {
const allTypes = propFromApp.map((item) => item.type)
setCheckedItems(allTypes)
}, [propFromApp])
В качестве альтернативы, если у вас много dep, может стать неоправданно обременительным столь тщательное управление dep. Затем вы можете явно отключить обратный вызов эффекта, если не произошло ожидаемое вами изменение.
const prevPropFromApp = useRef(propFromApp)
const propFromAppChanged = prevPropFromApp.current !== (prevPropFromApp.current = propFromApp)
useEffect(() => {
if (!propFromAppChanged) return;
setCheckedItems(allTypes)
}, [allTypes, propFromAppChanged])
Комментарии:
1. привет, спасибо за ответ. Я действительно думал о вашем первом решении, но для этого требуется дважды написать
propFromApp.map((item) => item.type
логику. Я не уверен, понимаю ли я, что вы имели в виду, говоря «использовать useMemo для изменения логики приложения нехорошо».. Вы имели в виду, что в моем случае я изменил логику приложения с помощьюuseMemo
? Также не могли бы вы привести пример изменения логики приложения с помощьюuseMemo
?2. Пока требуемое вам преобразование является чистым, как отображение в вашем примере, вы можете просто написать вспомогательную функцию для него, чтобы избежать дублирования. Я действительно был неясен в своей формулировке, я отредактирую ответ. Я хотел сказать, что это не подходит для
useMemo
пересчета или не пересчитывания, имеющего какую-либо разницу в терминах логики приложения. В вашем примере это имеет значение (сброс или не сброс), и поэтому это не нормально и вызывает ошибки.
Ответ №2:
Просто предложение, но я бы пропустил useMemo и просто установил оператор If вокруг обоих ваших useEffects. Это гарантирует, что useEffect обновляется только тогда, когда ваши условия действительны.
Пример в приложении:
useEffect(() => {
if(!myState) {
setTimeout(() => {...
Пример в дочернем:
useEffect(() => {
if(!checkedItems amp;amp; allTypes) {
setCheckedItems(allTypes)
}
}, [allTypes, checkedItems])
Комментарии:
1. спасибо за ответ, но я не думаю, что ваше решение здесь правильное.
checkedItems
является массивом и! checkedItems
всегда будет возвращатьfalse
.