Правильное структурирование состояния может сделать разницу между компонентом, который приятно модифицировать и отлаживать, и компонентом, который является постоянным источником ошибок. Вот несколько советов, которые следует учитывать при структурировании состояния.
Вы узнаете
Когда использовать одну или несколько переменных состояния
Чего следует избегать при организации состояния
Как исправить распространенные проблемы со структурой состояния
Когда вы пишете компонент, который хранит некоторое состояние, вам придется сделать выбор, сколько переменных состояния использовать и какова должна быть форма их данных. Хотя можно писать корректные программы даже с неоптимальной структурой состояния, есть несколько принципов, которые помогут вам сделать лучший выбор:
Группируйте связанные состояния. Если вы всегда обновляете две или более переменных состояния одновременно, подумайте о том, чтобы объединить их в одну переменную состояния.
Избегайте противоречий в состоянии. Когда состояние структурировано таким образом, что несколько частей состояния могут противоречить и "не соглашаться" друг с другом, вы оставляете место для ошибок. Постарайтесь избежать этого.
Если вы можете вычислить какую-то информацию из пропсов компонента или его существующих переменных состояния во время рендеринга, не стоит помещать эту информацию в состояние компонента.
Когда одни и те же данные дублируются в нескольких переменных состояния или во вложенных объектах, их трудно синхронизировать. Сократите дублирование, когда это возможно.
Избегайте глубоко вложенного состояния. Глубоко иерархическое состояние не очень удобно для обновления. Когда это возможно, предпочитайте структурировать состояние плоским образом.
Цель этих принципов - сделать состояние легко обновляемым без ошибок. Удаление избыточных и дублирующих данных из состояния помогает обеспечить синхронизацию всех его частей. Это похоже на то, как инженер базы данных может захотеть "нормализовать" структуру базы данных, чтобы уменьшить вероятность ошибок. Перефразируя Альберта Эйнштейна, "Сделайте ваше состояние настолько простым, насколько оно может быть - но не проще.".
Теперь давайте посмотрим, как эти принципы применяются на практике.
Технически, вы можете использовать любой из этих подходов. Но если некоторые две переменные состояния всегда изменяются вместе, хорошей идеей будет объединить их в одну переменную состояния. Тогда вы не забудете всегда синхронизировать их, как в этом примере, где перемещение курсора обновляет обе координаты красной точки:
Еще один случай, когда вы группируете данные в объект или массив, - это когда вы не знаете, сколько частей состояния вам понадобится. Например, это полезно, когда у вас есть форма, в которой пользователь может добавлять пользовательские поля.
Если ваша переменная состояния является объектом, помните, что вы не можете обновить только одно поле в нем без явного копирования других полей. Например, вы не можете сделать setPosition({ x: 100 }) в приведенном выше примере, потому что у него не будет свойства y вообще! Вместо этого, если бы вы хотели установить только x, вы бы либо сделали setPosition({ ...position, x: 100 }), либо разделили их на две переменные состояния и сделали setX(100).
import{useState}from'react';exportdefaultfunctionFeedbackForm(){const[text,setText]=useState('');const[isSending,setIsSending]=useState(false);const[isSent,setIsSent]=useState(false);asyncfunctionhandleSubmit(e){e.preventDefault();setIsSending(true);awaitsendMessage(text);setIsSending(false);setIsSent(true);}if(isSent){return<h1>Thanksforfeedback!</h1>;}return(<formonSubmit={handleSubmit}><p>HowwasyourstayatThePrancingPony?</p><textareadisabled={isSending}value={text}onChange={(e)=>setText(e.target.value)}/><br/><buttondisabled={isSending}type="submit">Send</button>{isSending&&<p>Sending...</p>}</form>);}// Pretend to send a message.functionsendMessage(text){returnnewPromise((resolve)=>{setTimeout(resolve,2000);});}
Хотя этот код работает, он оставляет дверь открытой для "невозможных" состояний. Например, если вы забудете вызвать setIsSent и setIsSending вместе, вы можете оказаться в ситуации, когда одновременно isSending и isSent будут true. Чем сложнее ваш компонент, тем труднее понять, что произошло.
Поскольку isSending и isSent никогда не должны быть true одновременно, лучше заменить их одной переменной состояния status, которая может принимать одно из трех допустимых состояний:'typing' (начальное), 'sending' и 'sent':
import{useState}from'react';exportdefaultfunctionFeedbackForm(){const[text,setText]=useState('');const[status,setStatus]=useState('typing');asyncfunctionhandleSubmit(e){e.preventDefault();setStatus('sending');awaitsendMessage(text);setStatus('sent');}constisSending=status==='sending';constisSent=status==='sent';if(isSent){return<h1>Thanksforfeedback!</h1>;}return(<formonSubmit={handleSubmit}><p>HowwasyourstayatThePrancingPony?</p><textareadisabled={isSending}value={text}onChange={(e)=>setText(e.target.value)}/><br/><buttondisabled={isSending}type="submit">Send</button>{isSending&&<p>Sending...</p>}</form>);}// Pretend to send a message.functionsendMessage(text){returnnewPromise((resolve)=>{setTimeout(resolve,2000);});}
Вы все еще можете объявить некоторые константы для удобства чтения:
Если вы можете вычислить некоторую информацию из пропсов компонента или его существующих переменных состояния во время рендеринга, вам не следует помещать эту информацию в состояние компонента.
Например, возьмем эту форму. Она работает, но можете ли вы найти в ней избыточное состояние?
Эта форма имеет три переменные состояния: firstName, lastName и fullName. Однако fullName является избыточной. Вы всегда можете вычислить fullName из firstName и lastName во время рендеринга, поэтому удалите ее из state..
Здесь fullName не является не переменной состояния. Вместо этого она вычисляется во время рендеринга:
1
constfullName=firstName+' '+lastName;
В результате обработчикам изменений не нужно делать ничего особенного, чтобы обновить его. Когда вы вызываете setFirstName или setLastName, вы вызываете повторный рендеринг, а затем следующее fullName будет вычислено на основе свежих данных.
Не зеркалируйте пропсы в состоянии
Частым примером избыточного состояния является код, подобный этому:
Здесь переменная состояния color инициализируется параметром messageColor. Проблема в том, что если родительский компонент позже передаст другое значение messageColor (например, 'red' вместо 'blue'), переменная состояния color не будет обновлена! Состояние инициализируется только во время первого рендеринга.
Вот почему "зеркальное отражение" какого-либо свойства в переменной состояния может привести к путанице. Вместо этого используйте свойство messageColor непосредственно в коде. Если вы хотите дать ему более короткое имя, используйте константу:
Таким образом, он не будет рассинхронизирован с пропсом, переданным из родительского компонента.
"Зеркалирование" пропсов в состояние имеет смысл только тогда, когда вы хотите игнорировать все обновления для конкретного пропса. По традиции, начните имя пропса с initial или default, чтобы уточнить, что его новые значения игнорируются:
12345
functionMessage({initialColor}){// The `color` state variable holds the *first* value of `initialColor`.// Further changes to the `initialColor` prop are ignored.const[color,setColor]=useState(initialColor);}
В настоящее время он хранит выбранный элемент как объект в переменной состояния selectedItem. Однако это не очень хорошо: содержимое selectedItem является тем же объектом, что и один из элементов списка items. Это означает, что информация о самом элементе дублируется в двух местах.
Почему это является проблемой? Давайте сделаем каждый элемент редактируемым:
Обратите внимание, что если вы сначала нажмете "Выбрать" на элементе, а затем отредактируете его, ввод обновляется, но метка внизу не отражает правки.Это потому, что у вас дублируется состояние, и вы забыли обновить selectedItem.
Хотя вы могли бы обновить selectedItem тоже, проще устранить дублирование. В этом примере вместо объекта selectedItem (который создает дублирование с объектами внутри items), вы храните selectedId в состоянии, а потом получаете selectedItem путем поиска элемента с этим ID в массиве items:
(В качестве альтернативы можно удерживать выбранный индекс в состоянии).
Раньше состояние дублировалось следующим образом:
items = [{ id: 0, title: 'pretzels'}, ...]
selectedItem = { id: 0, title: 'pretzels'}
Но после изменения это выглядит следующим образом:
items = [{ id: 0, title: 'pretzels'}, ...]
selectedId = 0
Дублирование исчезло, и вы сохранили только основное состояние!
Теперь, если вы отредактируете выбранный элемент, сообщение ниже будет немедленно обновлено. Это происходит потому, что setItems вызывает повторный рендеринг, и items.find(...) найдет элемент с обновленным заголовком. Вам не нужно было хранить выбранный элемент в состоянии, потому что только выбранный ID является существенным. Остальное можно вычислить во время рендеринга.
Представьте себе план путешествия, состоящий из планет, континентов и стран. У вас может возникнуть соблазн структурировать его состояние с помощью вложенных объектов и массивов, как в этом примере:
exportconstinitialTravelPlan={id:0,title:'(Root)',childPlaces:[{id:1,title:'Earth',childPlaces:[{id:2,title:'Africa',childPlaces:[{id:3,title:'Botswana',childPlaces:[],},{id:4,title:'Egypt',childPlaces:[],},{id:5,title:'Kenya',childPlaces:[],},{id:6,title:'Madagascar',childPlaces:[],},{id:7,title:'Morocco',childPlaces:[],},{id:8,title:'Nigeria',childPlaces:[],},{id:9,title:'South Africa',childPlaces:[],},],},{id:10,title:'Americas',childPlaces:[{id:11,title:'Argentina',childPlaces:[],},{id:12,title:'Brazil',childPlaces:[],},{id:13,title:'Barbados',childPlaces:[],},{id:14,title:'Canada',childPlaces:[],},{id:15,title:'Jamaica',childPlaces:[],},{id:16,title:'Mexico',childPlaces:[],},{id:17,title:'Trinidad and Tobago',childPlaces:[],},{id:18,title:'Venezuela',childPlaces:[],},],},{id:19,title:'Asia',childPlaces:[{id:20,title:'China',childPlaces:[],},{id:21,title:'Hong Kong',childPlaces:[],},{id:22,title:'India',childPlaces:[],},{id:23,title:'Singapore',childPlaces:[],},{id:24,title:'South Korea',childPlaces:[],},{id:25,title:'Thailand',childPlaces:[],},{id:26,title:'Vietnam',childPlaces:[],},],},{id:27,title:'Europe',childPlaces:[{id:28,title:'Croatia',childPlaces:[],},{id:29,title:'France',childPlaces:[],},{id:30,title:'Germany',childPlaces:[],},{id:31,title:'Italy',childPlaces:[],},{id:32,title:'Portugal',childPlaces:[],},{id:33,title:'Spain',childPlaces:[],},{id:34,title:'Turkey',childPlaces:[],},],},{id:35,title:'Oceania',childPlaces:[{id:36,title:'Australia',childPlaces:[],},{id:37,title:'Bora Bora (French Polynesia)',childPlaces:[],},{id:38,title:'Easter Island (Chile)',childPlaces:[],},{id:39,title:'Fiji',childPlaces:[],},{id:40,title:'Hawaii (the USA)',childPlaces:[],},{id:41,title:'New Zealand',childPlaces:[],},{id:42,title:'Vanuatu',childPlaces:[],},],},],},{id:43,title:'Moon',childPlaces:[{id:44,title:'Rheita',childPlaces:[],},{id:45,title:'Piccolomini',childPlaces:[],},{id:46,title:'Tycho',childPlaces:[],},],},{id:47,title:'Mars',childPlaces:[{id:48,title:'Corn Town',childPlaces:[],},{id:49,title:'Green Hill',childPlaces:[],},],},],};
Теперь предположим, что вы хотите добавить кнопку для удаления места, которое вы уже посетили. Как бы вы это сделали? Обновление состояния вложенных объектов включает создание копий объектов по всей цепочке вверх от той части, которая изменилась. Удаление глубоко вложенного места потребует копирования всей цепочки родительских мест. Такой код может быть очень многословным.
Если состояние слишком вложенное, чтобы его можно было легко обновить, подумайте о том, чтобы сделать его "плоским". Вот один из способов реструктуризации этих данных. Вместо древовидной структуры, где каждое место имеет массив его дочерних мест, вы можете сделать так, чтобы каждое место содержало массив идентификаторов дочерних мест. Затем хранить отображение от каждого идентификатора места к соответствующему месту.
Такая реструктуризация данных может напомнить вам таблицу базы данных:
exportconstinitialTravelPlan={0:{id:0,title:'(Root)',childIds:[1,43,47],},1:{id:1,title:'Earth',childIds:[2,10,19,27,35],},2:{id:2,title:'Africa',childIds:[3,4,5,6,7,8,9],},3:{id:3,title:'Botswana',childIds:[],},4:{id:4,title:'Egypt',childIds:[],},5:{id:5,title:'Kenya',childIds:[],},6:{id:6,title:'Madagascar',childIds:[],},7:{id:7,title:'Morocco',childIds:[],},8:{id:8,title:'Nigeria',childIds:[],},9:{id:9,title:'South Africa',childIds:[],},10:{id:10,title:'Americas',childIds:[11,12,13,14,15,16,17,18],},11:{id:11,title:'Argentina',childIds:[],},12:{id:12,title:'Brazil',childIds:[],},13:{id:13,title:'Barbados',childIds:[],},14:{id:14,title:'Canada',childIds:[],},15:{id:15,title:'Jamaica',childIds:[],},16:{id:16,title:'Mexico',childIds:[],},17:{id:17,title:'Trinidad and Tobago',childIds:[],},18:{id:18,title:'Venezuela',childIds:[],},19:{id:19,title:'Asia',childIds:[20,21,22,23,24,25,26],},20:{id:20,title:'China',childIds:[],},21:{id:21,title:'Hong Kong',childIds:[],},22:{id:22,title:'India',childIds:[],},23:{id:23,title:'Singapore',childIds:[],},24:{id:24,title:'South Korea',childIds:[],},25:{id:25,title:'Thailand',childIds:[],},26:{id:26,title:'Vietnam',childIds:[],},27:{id:27,title:'Europe',childIds:[28,29,30,31,32,33,34],},28:{id:28,title:'Croatia',childIds:[],},29:{id:29,title:'France',childIds:[],},30:{id:30,title:'Germany',childIds:[],},31:{id:31,title:'Italy',childIds:[],},32:{id:32,title:'Portugal',childIds:[],},33:{id:33,title:'Spain',childIds:[],},34:{id:34,title:'Turkey',childIds:[],},35:{id:35,title:'Oceania',childIds:[36,37,38,39,40,41,42],},36:{id:36,title:'Australia',childIds:[],},37:{id:37,title:'Bora Bora (French Polynesia)',childIds:[],},38:{id:38,title:'Easter Island (Chile)',childIds:[],},39:{id:39,title:'Fiji',childIds:[],},40:{id:40,title:'Hawaii (the USA)',childIds:[],},41:{id:41,title:'New Zealand',childIds:[],},42:{id:42,title:'Vanuatu',childIds:[],},43:{id:43,title:'Moon',childIds:[44,45,46],},44:{id:44,title:'Rheita',childIds:[],},45:{id:45,title:'Piccolomini',childIds:[],},46:{id:46,title:'Tycho',childIds:[],},47:{id:47,title:'Mars',childIds:[48,49],},48:{id:48,title:'Corn Town',childIds:[],},49:{id:49,title:'Green Hill',childIds:[],},};
Теперь, когда состояние "плоское" (также известное как "нормализованное"), обновлять вложенные элементы стало проще.
Чтобы удалить место, теперь вам нужно обновить только два уровня состояния:
Обновленная версия родительского места должна исключить удаленный ID из своего массива childIds.
Обновленная версия корневого объекта "table" должна включать обновленную версию родительского места.
import{useState}from'react';import{initialTravelPlan}from'./places.js';exportdefaultfunctionTravelPlan(){const[plan,setPlan]=useState(initialTravelPlan);functionhandleComplete(parentId,childId){constparent=plan[parentId];// Create a new version of the parent place// that doesn't include this child ID.constnextParent={...parent,childIds:parent.childIds.filter((id)=>id!==childId),};// Update the root state object...setPlan({...plan,// ...so that it has the updated parent.[parentId]:nextParent,});}constroot=plan[0];constplanetIds=root.childIds;return(<><h2>Placestovisit</h2><ol>{planetIds.map((id)=>(<PlaceTreekey={id}id={id}parentId={0}placesById={plan}onComplete={handleComplete}/>))}</ol></>);}functionPlaceTree({id,parentId,placesById,onComplete,}){constplace=placesById[id];constchildIds=place.childIds;return(<li>{place.title}<buttononClick={()=>{onComplete(parentId,id);}}>Complete</button>{childIds.length>0&&(<ol>{childIds.map((childId)=>(<PlaceTreekey={childId}id={childId}parentId={id}placesById={placesById}onComplete={onComplete}/>))}</ol>)}</li>);}
exportconstinitialTravelPlan={0:{id:0,title:'(Root)',childIds:[1,43,47],},1:{id:1,title:'Earth',childIds:[2,10,19,27,35],},2:{id:2,title:'Africa',childIds:[3,4,5,6,7,8,9],},3:{id:3,title:'Botswana',childIds:[],},4:{id:4,title:'Egypt',childIds:[],},5:{id:5,title:'Kenya',childIds:[],},6:{id:6,title:'Madagascar',childIds:[],},7:{id:7,title:'Morocco',childIds:[],},8:{id:8,title:'Nigeria',childIds:[],},9:{id:9,title:'South Africa',childIds:[],},10:{id:10,title:'Americas',childIds:[11,12,13,14,15,16,17,18],},11:{id:11,title:'Argentina',childIds:[],},12:{id:12,title:'Brazil',childIds:[],},13:{id:13,title:'Barbados',childIds:[],},14:{id:14,title:'Canada',childIds:[],},15:{id:15,title:'Jamaica',childIds:[],},16:{id:16,title:'Mexico',childIds:[],},17:{id:17,title:'Trinidad and Tobago',childIds:[],},18:{id:18,title:'Venezuela',childIds:[],},19:{id:19,title:'Asia',childIds:[20,21,22,23,24,25,26],},20:{id:20,title:'China',childIds:[],},21:{id:21,title:'Hong Kong',childIds:[],},22:{id:22,title:'India',childIds:[],},23:{id:23,title:'Singapore',childIds:[],},24:{id:24,title:'South Korea',childIds:[],},25:{id:25,title:'Thailand',childIds:[],},26:{id:26,title:'Vietnam',childIds:[],},27:{id:27,title:'Europe',childIds:[28,29,30,31,32,33,34],},28:{id:28,title:'Croatia',childIds:[],},29:{id:29,title:'France',childIds:[],},30:{id:30,title:'Germany',childIds:[],},31:{id:31,title:'Italy',childIds:[],},32:{id:32,title:'Portugal',childIds:[],},33:{id:33,title:'Spain',childIds:[],},34:{id:34,title:'Turkey',childIds:[],},35:{id:35,title:'Oceania',childIds:[36,37,38,39,40,41,,42],},36:{id:36,title:'Australia',childIds:[],},37:{id:37,title:'Bora Bora (French Polynesia)',childIds:[],},38:{id:38,title:'Easter Island (Chile)',childIds:[],},39:{id:39,title:'Fiji',childIds:[],},40:{id:40,title:'Hawaii (the USA)',childIds:[],},41:{id:41,title:'New Zealand',childIds:[],},42:{id:42,title:'Vanuatu',childIds:[],},43:{id:43,title:'Moon',childIds:[44,45,46],},44:{id:44,title:'Rheita',childIds:[],},45:{id:45,title:'Piccolomini',childIds:[],},46:{id:46,title:'Tycho',childIds:[],},47:{id:47,title:'Mars',childIds:[48,49],},48:{id:48,title:'Corn Town',childIds:[],},49:{id:49,title:'Green Hill',childIds:[],},};
Вкладывать состояние можно сколько угодно, но если сделать его "плоским", это решит множество проблем. Это облегчает обновление состояния и помогает избежать дублирования в различных частях вложенного объекта.
Улучшение использования памяти
В идеале, для улучшения использования памяти вы также должны удалить удаленные элементы (и их детей!) из объекта "table". В данной версии это сделано. Она также использует Immer, чтобы сделать логику обновления более лаконичной.
import{useImmer}from'use-immer';import{initialTravelPlan}from'./places.js';exportdefaultfunctionTravelPlan(){const[plan,updatePlan]=useImmer(initialTravelPlan);functionhandleComplete(parentId,childId){updatePlan((draft)=>{// Remove from the parent place's child IDs.constparent=draft[parentId];parent.childIds=parent.childIds.filter((id)=>id!==childId);// Forget this place and all its subtree.deleteAllChildren(childId);functiondeleteAllChildren(id){constplace=draft[id];place.childIds.forEach(deleteAllChildren);deletedraft[id];}});}constroot=plan[0];constplanetIds=root.childIds;return(<><h2>Placestovisit</h2><ol>{planetIds.map((id)=>(<PlaceTreekey={id}id={id}parentId={0}placesById={plan}onComplete={handleComplete}/>))}</ol></>);}functionPlaceTree({id,parentId,placesById,onComplete,}){constplace=placesById[id];constchildIds=place.childIds;return(<li>{place.title}<buttononClick={()=>{onComplete(parentId,id);}}>Complete</button>{childIds.length>0&&(<ol>{childIds.map((childId)=>(<PlaceTreekey={childId}id={childId}parentId={id}placesById={placesById}onComplete={onComplete}/>))}</ol>)}</li>);}
exportconstinitialTravelPlan={0:{id:0,title:'(Root)',childIds:[1,43,47],},1:{id:1,title:'Earth',childIds:[2,10,19,27,35],},2:{id:2,title:'Africa',childIds:[3,4,5,6,7,8,9],},3:{id:3,title:'Botswana',childIds:[],},4:{id:4,title:'Egypt',childIds:[],},5:{id:5,title:'Kenya',childIds:[],},6:{id:6,title:'Madagascar',childIds:[],},7:{id:7,title:'Morocco',childIds:[],},8:{id:8,title:'Nigeria',childIds:[],},9:{id:9,title:'South Africa',childIds:[],},10:{id:10,title:'Americas',childIds:[11,12,13,14,15,16,17,18],},11:{id:11,title:'Argentina',childIds:[],},12:{id:12,title:'Brazil',childIds:[],},13:{id:13,title:'Barbados',childIds:[],},14:{id:14,title:'Canada',childIds:[],},15:{id:15,title:'Jamaica',childIds:[],},16:{id:16,title:'Mexico',childIds:[],},17:{id:17,title:'Trinidad and Tobago',childIds:[],},18:{id:18,title:'Venezuela',childIds:[],},19:{id:19,title:'Asia',childIds:[20,21,22,23,24,25,26],},20:{id:20,title:'China',childIds:[],},21:{id:21,title:'Hong Kong',childIds:[],},22:{id:22,title:'India',childIds:[],},23:{id:23,title:'Singapore',childIds:[],},24:{id:24,title:'South Korea',childIds:[],},25:{id:25,title:'Thailand',childIds:[],},26:{id:26,title:'Vietnam',childIds:[],},27:{id:27,title:'Europe',childIds:[28,29,30,31,32,33,34],},28:{id:28,title:'Croatia',childIds:[],},29:{id:29,title:'France',childIds:[],},30:{id:30,title:'Germany',childIds:[],},31:{id:31,title:'Italy',childIds:[],},32:{id:32,title:'Portugal',childIds:[],},33:{id:33,title:'Spain',childIds:[],},34:{id:34,title:'Turkey',childIds:[],},35:{id:35,title:'Oceania',childIds:[36,37,38,39,40,41,,42],},36:{id:36,title:'Australia',childIds:[],},37:{id:37,title:'Bora Bora (French Polynesia)',childIds:[],},38:{id:38,title:'Easter Island (Chile)',childIds:[],},39:{id:39,title:'Fiji',childIds:[],},40:{id:40,title:'Hawaii (the USA)',childIds:[],},41:{id:41,title:'New Zealand',childIds:[],},42:{id:42,title:'Vanuatu',childIds:[],},43:{id:43,title:'Moon',childIds:[44,45,46],},44:{id:44,title:'Rheita',childIds:[],},45:{id:45,title:'Piccolomini',childIds:[],},46:{id:46,title:'Tycho',childIds:[],},47:{id:47,title:'Mars',childIds:[48,49],},48:{id:48,title:'Corn Town',childIds:[],},49:{id:49,title:'Green Hill',childIds:[],},};
Иногда вложенность состояния можно уменьшить, переместив часть вложенного состояния в дочерние компоненты. Это хорошо работает для эфемерного состояния пользовательского интерфейса, которое не нужно хранить, например, наведение курсора на элемент.
Итоги
Если две переменные состояния всегда обновляются вместе, подумайте о том, чтобы объединить их в одну.
Тщательно выбирайте переменные состояния, чтобы избежать создания "невозможных" состояний.
Структурируйте состояние таким образом, чтобы уменьшить вероятность ошибки при его обновлении.
Избегайте избыточных и дублирующих состояний, чтобы не нужно было их синхронизировать.
Не помещайте пропсы в состояние, если только вы специально не хотите предотвратить их обновление.
Для шаблонов пользовательского интерфейса, таких как выбор, храните ID или индекс в состоянии, а не сам объект.
Если обновление глубоко вложенного состояния является сложным, попробуйте сгладить его.
1. Исправление компонента, который не обновляется¶
Этот компонент Clock получает два пропса: color и time. Когда вы выбираете другой цвет в поле выбора, компонент Clock получает другой пропс color от своего родительского компонента. Однако по какой-то причине отображаемый цвет не обновляется. Почему? Устраните проблему.
Проблема в том, что у этого компонента состояние color инициализируется начальным значением свойства color. Но когда пропс color изменяется, это не влияет на переменную состояния! Поэтому они рассинхронизируются. Чтобы решить эту проблему, удалите переменную state совсем и используйте непосредственно пропс color.
Этот упаковочный лист имеет нижний колонтитул, который показывает, сколько предметов упаковано, и сколько предметов в целом. Поначалу кажется, что это работает, но на самом деле это ошибка. Например, если вы пометите предмет как упакованный, а затем удалите его, счетчик не будет обновлен правильно. Исправьте счетчик так, чтобы он всегда был корректным.
Является ли какое-либо состояние в этом примере избыточным?
Показать подсказку
Хотя вы могли бы тщательно изменить каждый обработчик событий, чтобы правильно обновлять счетчики total и packed, основная проблема заключается в том, что эти переменные состояния вообще существуют. Они избыточны, потому что вы всегда можете вычислить количество элементов (упакованных или всего) из массива items. Удалите избыточное состояние, чтобы исправить ошибку:
Обратите внимание, что после этого изменения обработчики событий занимаются только вызовом setItems. Количество элементов теперь вычисляется во время следующего рендеринга из items, поэтому они всегда актуальны.
Есть список letters в состоянии. Когда вы наводите курсор или фокус на определенное письмо - оно выделяется. Текущее выделенное письмо хранится в переменной состояния highlightedLetter. Вы можете "выделять" и "снимать выделение" отдельных писем, что приводит к обновлению массива letters в состоянии.
Этот код работает, но есть небольшой сбой в пользовательском интерфейсе. Когда вы нажимаете "Star" или "Unstar", подсветка на мгновение исчезает. Однако она снова появляется, как только вы перемещаете указатель или переключаетесь на другое письмо с клавиатуры. Почему это происходит? Исправьте это, чтобы подсветка не исчезала после нажатия кнопки.
exportconstinitialLetters=[{id:0,subject:'Ready for adventure?',isStarred:true,},{id:1,subject:'Time to check in!',isStarred:false,},{id:2,subject:'Festival Begins in Just SEVEN Days!',isStarred:false,},];
Показать решение
Проблема в том, что вы храните объект письма в highlightedLetter. Но вы также храните ту же информацию в массиве letters. Таким образом, ваше состояние дублируется! Когда вы обновляете массив letters после нажатия кнопки, вы создаете новый объект письма, который отличается от highlightedLetter. Поэтому проверка highlightedLetter === letter становится false, и выделение исчезает. Оно снова появляется при следующем вызове setHighlightedLetter, когда указатель перемещается.
Чтобы решить эту проблему, удалите дублирование из состояния. Вместо того чтобы хранить само письмо в двух местах, храните highlightedId. Тогда вы сможете проверять isHighlighted для каждого письма с letter.id === highlightedId, что будет работать, даже если объект letter изменился с момента последнего рендеринга.
exportconstinitialLetters=[{id:0,subject:'Ready for adventure?',isStarred:true,},{id:1,subject:'Time to check in!',isStarred:false,},{id:2,subject:'Festival Begins in Just SEVEN Days!',isStarred:false,},];
В этом примере каждое Letter имеет свойство isSelected и обработчик onToggle, который отмечает его как выбранное. Это работает, но состояние хранится как selectedId (либо null, либо ID), поэтому в каждый момент времени может быть выбрано только одно письмо.
Измените структуру состояния для поддержки множественного выбора (Как бы вы его структурировали? Подумайте об этом перед написанием кода). Каждый флажок должен стать независимым от других. Щелчок по выбранному письму должен снимать флажок. Наконец, нижний колонтитул должен показывать правильное количество выбранных элементов.
exportconstletters=[{id:0,subject:'Ready for adventure?',isStarred:true,},{id:1,subject:'Time to check in!',isStarred:false,},{id:2,subject:'Festival Begins in Just SEVEN Days!',isStarred:false,},];
Показать подсказку
Вместо одного выбранного ID, вы можете захотеть хранить массив или множество Set выбранных ID в состоянии.
Показать решение
Вместо одного selectedId, храните selectedIdsмассив в состоянии. Например, если вы выбираете первое и последнее письмо, он будет содержать [0, 2]. Когда ничего не выбрано, это будет пустой массив []:
import{useState}from'react';import{letters}from'./data.js';importLetterfrom'./Letter.js';exportdefaultfunctionMailClient(){const[selectedIds,setSelectedIds]=useState([]);constselectedCount=selectedIds.length;functionhandleToggle(toggledId){// Was it previously selected?if(selectedIds.includes(toggledId)){// Then remove this ID from the array.setSelectedIds(selectedIds.filter((id)=>id!==toggledId));}else{// Otherwise, add this ID to the array.setSelectedIds([...selectedIds,toggledId]);}}return(<><h2>Inbox</h2><ul>{letters.map((letter)=>(<Letterkey={letter.id}letter={letter}isSelected={selectedIds.includes(letter.id)}onToggle={handleToggle}/>))}<hr/><p><b>Youselected{selectedCount}letters</b></p></ul></>);}
exportconstletters=[{id:0,subject:'Ready for adventure?',isStarred:true,},{id:1,subject:'Time to check in!',isStarred:false,},{id:2,subject:'Festival Begins in Just SEVEN Days!',isStarred:false,},];
Одним из небольших недостатков использования массива является то, что для каждого элемента вы вызываете selectedIds.includes(letter.id), чтобы проверить, выбран ли он. Если массив очень большой, это может стать проблемой производительности, поскольку поиск в массиве с помощью includes() занимает линейное время, а вы выполняете этот поиск для каждого отдельного элемента.
Чтобы решить эту проблему, вы можете держать в состоянии Set, что обеспечивает быструю операцию has():
exportconstletters=[{id:0,subject:'Ready for adventure?',isStarred:true,},{id:1,subject:'Time to check in!',isStarred:false,},{id:2,subject:'Festival Begins in Just SEVEN Days!',isStarred:false,},];
Теперь каждый элемент выполняет проверку selectedIds.has(letter.id), что очень быстро.
Помните, что не следует мутировать объекты в состоянии, и это относится и к наборам. Вот почему функция handleToggle сначала создает копию набора, а затем обновляет эту копию.