Spec-Zone.ru › React 18

Выбор структуры состояния

Правильное структурирование состояния может повлиять на удобство изменения и отладки компонента, а также на отсутствие постоянных ошибок. Ниже приведены некоторые советы, которые следует учитывать при структурировании состояния.

Вы узнаете

  • Когда использовать одну переменную состояния вместо нескольких
  • Что следует избегать при организации состояния
  • Как исправить распространённые проблемы со структурой состояния

Принципы структурирования состояния

Когда вы пишете компонент, который хранит состояние, вам необходимо принять решения о том, сколько переменных состояния использовать и какой должна быть форма их данных. Хотя можно написать правильные программы даже со структурой состояния, которая не является оптимальной, существуют несколько принципов, которые могут помочь вам сделать лучший выбор:

  1. Группируйте связанное состояние. Если вы всегда обновляете две или более переменных состояния одновременно, рассмотрите возможность объединения их в одну переменную состояния.
  2. Избегайте противоречий в состоянии. Когда состояние структурировано таким образом, что несколько частей состояния могут противоречить друг другу и «не соглашаться» друг с другом, это создаёт возможность ошибок. Старайтесь этого избегать.
  3. Избегайте избыточного состояния. Если вы можете вычислить какую-либо информацию из свойств компонента или его существующих переменных состояния во время отрисовки, не следует помещать эту информацию в состояние компонента.
  4. Избегайте дублирования в состоянии. Когда одни и те же данные дублируются между несколькими переменными состояния или внутри вложенных объектов, их сложно синхронизировать. По возможности уменьшайте дублирование.
  5. Избегайте глубоко вложенного состояния. Глубоко иерархическое состояние неудобно обновлять. По возможности предпочитайте плоскую структуру состояния.

Цель этих принципов заключается в том, чтобы облегчить обновление состояния без введения ошибок. Удаление избыточных и дублирующих данных из состояния помогает гарантировать, что все его части остаются синхронизированными. Это похоже на то, как разработчик базы данных может “нормализовать” структуру базы данных, чтобы уменьшить вероятность ошибок. Чтобы перефразировать Альберта Эйнштейна, «Делайте ваше состояние максимально простым, но не проще».

Теперь давайте посмотрим, как эти принципы применяются на практике.

Группировка связанного состояния

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

Следует ли делать так?

const [x, setX] = useState(0);
const [y, setY] = useState(0);

Или так?

const [position, setPosition] = useState({ x: 0, y: 0 });

Технически, вы можете использовать любой из этих подходов. Но если две переменные состояния всегда изменяются вместе, то, возможно, стоит объединить их в одну переменную состояния. Тогда вы не забудете поддерживать их синхронизацию, как в этом примере, где перемещение курсора обновляет обе координаты красной точки:

import { useState } from 'react';

export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  )
}

Другой случай, когда вы будете группировать данные в объект или массив, — это когда вы не знаете, сколько частей состояния вам понадобится. Например, это полезно, когда у вас есть форма, где пользователь может добавлять пользовательские поля.

Ловушка

Если ваша переменная состояния — это объект, помните, что вы не можете обновить только одно поле в нём без явного копирования других полей. Например, вы не можете сделать setPosition({ x: 100 }) в вышеприведённом примере, потому что у него не будет y свойства вообще! Вместо этого, если вы хотели установить x в одиночку, вы бы либо сделали setPosition({ ...position, x: 100 }), или разделили их на две переменные состояния и сделали setX(100).

Избегайте противоречий в состоянии

Вот форма отзывов о гостинице с isSending и isSent переменными состояния:

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Хотя этот код работает, он оставляет возможность «невозможных» состояний. Например, если вы забудете вызвать setIsSent и setIsSending вместе, вы можете попасть в ситуацию, где isSending и isSent одновременно являются true. Чем сложнее ваш компонент, тем труднее понять, что произошло.

Поскольку isSending и isSent никогда не должны быть true одновременно, лучше заменить их одной status переменной состояния, которая может принимать одно из трёх допустимых состояний: 'typing' (начальное), 'sending', и 'sent':

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }

  const isSending = status === 'sending';
  const isSent = status === 'sent';

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Вы по-прежнему можете объявить некоторые константы для читабельности:

const isSending = status === 'sending';
const isSent = status === 'sent';

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

Избегайте избыточного состояния

Если вы можете вычислить какую-либо информацию из свойств компонента или его существующих переменных состояния во время отрисовки, вы не должны помещать эту информацию в состояние компонента.

Например, рассмотрите эту форму. Она работает, но сможете ли вы найти в ней избыточное состояние?

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Эта форма имеет три переменные состояния: firstName, lastName, и fullName. Однако fullName избыточно. Вы всегда можете вычислить fullName из firstName и lastName во время отрисовки, поэтому удалите его из состояния.

Вот как это можно сделать:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Здесь, fullName — не переменная состояния. Вместо этого она вычисляется во время отрисовки:

const fullName = firstName + ' ' + lastName;

В результате обработчики изменений не должны делать ничего особенного, чтобы обновить её. Когда вы вызываете setFirstName или setLastName, вы вызываете повторную отрисовку, и затем следующее значение fullName будет вычислено из свежих данных.

Подробный обзор

Не дублируйте свойства в состоянии

Распространённый пример избыточного состояния — код такого вида:

function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);

Здесь переменная состояния color инициализируется свойством messageColor. Проблема в том, что если родительский компонент позднее передаст другое значение messageColor (например, 'red' вместо 'blue'), переменная состояния color не будет обновлена! Состояние инициализируется только во время первой отрисовки.

Вот почему «дублирование» свойства в переменной состояния может привести к путанице. Вместо этого используйте свойство messageColor непосредственно в своём коде. Если вы хотите дать ему более короткое имя, используйте константу:

function Message({ messageColor }) {
  const color = messageColor;

Таким образом, оно не будет разойтись со свойством, переданным родительским компонентом.

«Дублирование» свойств в состояние имеет смысл только тогда, когда вы хотите игнорировать все обновления для определённого свойства. По соглашению, начинайте имя свойства с initial или default для того, чтобы сделать чётко, что его новые значения игнорируются:

function Message({ initialColor }) {
  // The `color` state variable holds the *first* value of `initialColor`.
  // Further changes to the `initialColor` prop are ignored.
  const [color, setColor] = useState(initialColor);

Избегайте дублирования в состоянии

Этот компонент списка меню позволяет выбрать один перекус для путешествия из нескольких:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

В настоящее время он хранит выбранный элемент как объект в переменной состояния selectedItem. Однако это не очень хорошо: содержимое selectedItem — это тот же объект, что и один из элементов внутри списка items. Это означает, что информация об элементе сама по себе дублируется в двух местах.

Почему это проблема? Давайте сделаем каждый элемент редактируемым:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2> 
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

Обратите внимание, что если вы сначала нажмёте «Выбрать» на элементе, а затем отредактируете его, ввод обновится, но метка внизу не отобразит изменения. Это происходит потому, что у вас есть дублирование состояния, и вы забыли обновить selectedItem.

Хотя вы могли бы обновить selectedItem тоже, более лёгким решением является удаление дублирования. В этом примере вместо объекта selectedItem (который создаёт дублирование с объектами внутри items), вы храните selectedId в состоянии, и затем получаете selectedItem путём поиска элемента в массиве items с соответствующим идентификатором:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item =>
    item.id === selectedId
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedId(item.id);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

Состояние ранее дублировалось так:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

Но после изменения оно выглядит так:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

Дублирования больше нет, и вы храните только необходимое состояние!

Теперь, если вы отредактируете выбранный элемент, сообщение внизу обновится немедленно. Это происходит потому, что setItems вызывает повторную отрисовку, и items.find(...) найдёт элемент с обновлённым заголовком. Вам не нужно было хранить выбранный элемент в состоянии, поскольку необходим только идентификатор выбранного элемента. Остальное можно вычислить во время отрисовки.

Избегайте глубоко вложенного состояния

Представьте себе туристический план, состоящий из планет, континентов и стран. Вы можете быть искушены структурировать его состояние с использованием вложенных объектов и массивов, как в этом примере:

export const initialTravelPlan = {
  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: 'India',
        childPlaces: []
      }, {
        id: 22,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 23,
        title: 'South Korea',
        childPlaces: []
      }, {
        id: 24,
        title: 'Thailand',
        childPlaces: []
      }, {
        id: 25,
        title: 'Vietnam',
        childPlaces: []
      }]
    }, {
      id: 26,
      title: 'Europe',
      childPlaces: [{
        id: 27,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 28,
        title: 'France',
        childPlaces: [],
      }, {
        id: 29,
        title: 'Germany',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Turkey',
        childPlaces: [],
      }]
    }, {
      id: 34,
      title: 'Oceania',
      childPlaces: [{
        id: 35,
        title: 'Australia',
        childPlaces: [],
      }, {
        id: 36,
        title: 'Bora Bora (French Polynesia)',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Easter Island (Chile)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Hawaii (the USA)',
        childPlaces: [],
      }, {
        id: 40,
        title: 'New Zealand',
        childPlaces: [],
      }, {
        id: 41,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 42,
    title: 'Moon',
    childPlaces: [{
      id: 43,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 44,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 45,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 46,
    title: 'Mars',
    childPlaces: [{
      id: 47,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 48,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

Теперь предположим, что вы хотите добавить кнопку для удаления места, в котором уже побывали. Как вы это сделаете? Обновление вложенного состояния включает создание копий объектов от изменённой части вверх по цепочке. Удаление глубоко вложенного места потребует копирования всей цепочки родительских мест. Такой код может быть очень громоздким.

Если состояние слишком глубоко вложено, чтобы его легко обновлять, рассмотрите возможность его «сглаживания». Вот один способ реструктуризации данных. Вместо древовидной структуры, где каждый place имеет массив его дочерних мест, вы можете дать каждому месту массив идентификаторов его дочерних мест. Затем сохраните отображение каждого идентификатора места с соответствующим местом.

Эта реструктуризация данных может напомнить вам о представлении таблицы в базе данных:

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  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],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'India',
    childIds: []
  },
  22: {
    id: 22,
    title: 'Singapore',
    childIds: []
  },
  23: {
    id: 23,
    title: 'South Korea',
    childIds: []
  },
  24: {
    id: 24,
    title: 'Thailand',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Vietnam',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Europe',
    childIds: [27, 28, 29, 30, 31, 32, 33],   
  },
  27: {
    id: 27,
    title: 'Croatia',
    childIds: []
  },
  28: {
    id: 28,
    title: 'France',
    childIds: []
  },
  29: {
    id: 29,
    title: 'Germany',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Italy',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Portugal',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Spain',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Turkey',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Oceania',
    childIds: [35, 36, 37, 38, 39, 40, 41],   
  },
  35: {
    id: 35,
    title: 'Australia',
    childIds: []
  },
  36: {
    id: 36,
    title: 'Bora Bora (French Polynesia)',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Easter Island (Chile)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Fiji',
    childIds: []
  },
  39: {
    id: 40,
    title: 'Hawaii (the USA)',
    childIds: []
  },
  40: {
    id: 40,
    title: 'New Zealand',
    childIds: []
  },
  41: {
    id: 41,
    title: 'Vanuatu',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Moon',
    childIds: [43, 44, 45]
  },
  43: {
    id: 43,
    title: 'Rheita',
    childIds: []
  },
  44: {
    id: 44,
    title: 'Piccolomini',
    childIds: []
  },
  45: {
    id: 45,
    title: 'Tycho',
    childIds: []
  },
  46: {
    id: 46,
    title: 'Mars',
    childIds: [47, 48]
  },
  47: {
    id: 47,
    title: 'Corn Town',
    childIds: []
  },
  48: {
    id: 48,
    title: 'Green Hill',
    childIds: []
  }
};

Теперь, когда состояние «сглажено» (также известное как «нормализованное»), обновление вложенных элементов становится проще.

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

  • Обновлённая версия его родительского места должна исключить удалённый идентификатор из его массива childIds.
  • Обновлённый корневой объект «таблицы» должен включать обновлённую версию родительского места.

Вот пример того, как вы можете это сделать:

import { useState } from 'react';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, setPlan] = useState(initialTravelPlan);

  function handleComplete(parentId, childId) {
    const parent = plan[parentId];
    // Create a new version of the parent place
    // that doesn't include this child ID.
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // Update the root state object...
    setPlan({
      ...plan,
      // ...so that it has the updated parent.
      [parentId]: nextParent
    });
  }

  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {planetIds.map(id => (
          <PlaceTree
            key={id}
            id={id}
            parentId={0}
            placesById={plan}
            onComplete={handleComplete}
          />
        ))}
      </ol>
    </>
  );
}

function PlaceTree({ id, parentId, placesById, onComplete }) {
  const place = placesById[id];
  const childIds = place.childIds;
  return (
    <li>
      {place.title}
      <button onClick={() => {
        onComplete(parentId, id);
      }}>
        Complete
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

Вы можете вкладывать состояние так глубоко, как вам захочется, но его «сглаживание» может решить множество проблем. Это упрощает обновление состояния и помогает избежать дублирования в разных частях вложенного объекта.

Глубокое погружение

Улучшение использования памяти

В идеале, вы также должны удалить удаленные элементы (и их потомков!) из объекта «таблица», чтобы улучшить использование памяти. Эта версия делает это. Она также использует Immer, чтобы сделать логику обновления более лаконичной.

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Иногда можно уменьшить вложенность состояния, перенеся часть вложенного состояния в дочерние компоненты. Это хорошо работает для временного состояния пользовательского интерфейса, которое не нужно хранить, например, для того, чтобы элемент был наведён курсором.

Резюме

  • Если две переменные состояния всегда обновляются вместе, рассмотрите возможность их объединения в одну.
  • Тщательно выбирайте свои переменные состояния, чтобы избежать создания «невозможных» состояний.
  • Структурируйте состояние таким образом, чтобы снизить вероятность ошибки при его обновлении.
  • Избегайте избыточных и дублирующих состояний, чтобы не нужно было синхронизировать их.
  • Не помещайте свойства в состояние, если вы не хотите специально предотвращать обновления.
  • Для схем пользовательского интерфейса, таких как выбор, сохраняйте идентификатор или индекс в состоянии вместо самого объекта.
  • Если обновление глубоко вложенного состояния сложно, попробуйте его сплющить.

Попробуйте выполнить некоторые задания

Задание 1 из 4:
Исправить компонент, который не обновляется

Этот Clock компонент получает два свойства: color и time. Когда вы выбираете другой цвет в выпадающем списке, компонент Clock получает другое свойство color от родительского компонента. Однако по какой-то причине отображаемый цвет не обновляется. В чём причина? Исправьте проблему.

import { useState } from 'react';

export default function Clock(props) {
  const [color, setColor] = useState(props.color);
  return (
    <h1 style={{ color: color }}>
      {props.time}
    </h1>
  );
}

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/learn/choosing-the-state-structure

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API