美文网首页
第二九章 管理状态-选择状态结构

第二九章 管理状态-选择状态结构

作者: 深圳都这么冷 | 来源:发表于2023-02-19 10:23 被阅读0次

选择状态结构

良好地构建状态可以区分一个易于修改和调试的组件,以及一个经常产生错误的组件。 以下是构建状态时应考虑的一些提示。

你将学习

  • 何时使用单个与多个状态变量
  • 组织状态时要避免什么
  • 如何解决状态结构的常见问题

结构化状态的原则

当您编写一个包含某些状态的组件时,您必须选择使用多少个状态变量以及它们的数据应该是什么形状。 虽然即使使用次优状态结构也可以编写正确的程序,但有一些原则可以指导您做出更好的选择:

  1. 组相关状态。 如果您总是同时更新两个或多个状态变量,请考虑将它们合并为一个状态变量。
  2. 避免状态上的矛盾。 当状态的结构方式使多个状态可能相互矛盾和“不一致”时,就会为错误留下空间。 尽量避免这种情况。
  3. 避免冗余状态。 如果您可以在渲染期间从组件的 props 或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。
  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 同时为真的情况。 您的组件越复杂,就越难理解发生了什么。

由于 isSending 和 isSent 绝不能同时为真,因此最好将它们替换为一个可能采用三种有效状态之一的状态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';

但它们不是状态变量,因此您无需担心它们彼此不同步。

避免状态冗余

如果您可以在渲染期间从组件的 props 或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。
例如,采用这种形式。 它有效,但你能在其中找到任何冗余状态吗?

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 是多余的。 您始终可以在渲染期间从 firstName 和 lastName 计算 fullName,因此将其从状态中删除。

你可以这样做:

  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);
  }

在这里,fullName 不是状态变量。 相反,它是在渲染期间计算的:
因此,更改处理程序不需要做任何特殊的事情来更新它。 当您调用 setFirstName 或 setLastName 时,您会触发重新渲染,然后下一个 fullName 将从新数据中计算出来。

深度阅读:不要再将props镜像保存为state

冗余状态的一个常见示例是这样的代码:

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

在这里,颜色状态变量被初始化为 messageColor 属性。 问题在于,如果父组件稍后传递了不同的 messageColor 值(例如,'red' 而不是 'blue'),颜色状态变量将不会更新! 状态仅在第一次渲染期间初始化。

这就是为什么在状态变量中“镜像”某些 prop 会导致混淆。 相反,直接在您的代码中使用 messageColor 道具。 如果你想给它一个更短的名字,使用一个常量:

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

这样它就不会与从父组件传递的 prop 不同步。

仅当您想忽略特定道具的所有更新时,将prop“镜像”到状态中才有意义。 按照惯例,prop 名称以 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 的内容与项目列表中的项目之一是同一个对象。 这意味着有关项目本身的信息在两个地方重复。

为什么这是个问题? 让我们让每个项目都可编辑:

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,但更简单的解决方法是删除重复项。 在此示例中,您将 selectedId 保持在 state 中,而不是 selectedItem 对象(它在项目内部创建对象的重复),然后通过在项目数组中搜索具有该 ID 的项目来获取 selectedItem:

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: '椒盐卷饼'}, ...]
  • selectedItem = {id: 0, title: '椒盐卷饼'}

但是改完之后是这样的:

  • items = [{ id: 0, title: '椒盐卷饼'}, ...]
  • 选择的 ID = 0

重复没有了,你只保留本质状态!

现在,如果您编辑所选项目,下面的消息将立即更新。 这是因为 setItems 会触发重新渲染,而 items.find(...) 会找到具有更新标题的项目。 您不需要将所选项目保持在状态中,因为只有所选 ID 是必需的。 其余的可以在渲染期间计算。

避免深度嵌套状态

想象一个由行星、大陆和国家组成的旅行计划。 您可能会想使用嵌套对象和数组来构建其状态,如本例所示:
places.js

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: '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: []      
    }]
  }]
};

App.js

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

function PlaceTree({ place }) {
  const childPlaces = place.childPlaces;
  return (
    <li>
      {place.title}
      {childPlaces.length > 0 && (
        <ol>
          {childPlaces.map(place => (
            <PlaceTree key={place.id} place={place} />
          ))}
        </ol>
      )}
    </li>
  );
}

export default function TravelPlan() {
  const [plan, setPlan] = useState(initialTravelPlan);
  const planets = plan.childPlaces;
  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {planets.map(place => (
          <PlaceTree key={place.id} place={place} />
        ))}
      </ol>
    </>
  );
}

现在假设你想添加一个按钮来删除你已经访问过的地方。 你会怎么做? 更新嵌套状态涉及从更改的部分一直向上复制对象。 删除深度嵌套的位置将涉及复制其整个父位置链。 这样的代码可能非常冗长。

如果状态嵌套太多而不易更新,请考虑使其“扁平化”。 这是您可以重组此数据的一种方法。 您可以让每个地点保存其子地点 ID 的数组,而不是每个地点都有其子地点数组的树状结构。 然后你可以存储一个从每个地点ID到对应地点的映射。

这种数据重组可能会让您想起看到的数据库表:

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

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

export default function TravelPlan() {
  const [plan, setPlan] = useState(initialTravelPlan);
  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {planetIds.map(id => (
          <PlaceTree
            key={id}
            id={id}
            placesById={plan}
          />
        ))}
      </ol>
    </>
  );
}

现在状态是“平坦的”(也称为“规范化”),更新嵌套项变得更容易。
为了现在删除一个地方,你只需要更新两个级别的状态:

  • 其父位置的更新版本应从其 childIds 数组中排除已删除的 ID。
  • 根“表”对象的更新版本应包括父位置的更新版本。

下面是一个如何去做的例子:
places.js

export const initialTravelPlan = {
  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: []
  }
};

App.js

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 使更新逻辑更加简洁。

import { useImmer } from 'use-immer';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, updatePlan] = useImmer(initialTravelPlan);

  function handleComplete(parentId, childId) {
    updatePlan(draft => {
      // Remove from the parent place's child IDs.
      const parent = draft[parentId];
      parent.childIds = parent.childIds
        .filter(id => id !== childId);

      // Forget this place and all its subtree.
      deleteAllChildren(childId);
      function deleteAllChildren(id) {
        const place = draft[id];
        place.childIds.forEach(deleteAllChildren);
        delete draft[id];
      }
    });
  }

  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>
  );
}

有时,您还可以通过将一些嵌套状态移动到子组件中来减少状态嵌套。 这适用于不需要存储的短暂 UI 状态,例如项目是否悬停。

回顾

  • 如果两个状态变量总是一起更新,考虑将它们合并为一个。
  • 仔细选择状态变量以避免产生“不可能”的状态。
  • 以一种减少更新错误的可能性的方式来构建你的状态。
  • 避免冗余和重复的状态,这样你就不需要保持同步。
  • 除非你特别想阻止更新,否则不要将道具放入状态。
  • 对于像选择这样的 UI 模式,将 ID 或索引保持在状态而不是对象本身。
  • 如果更新深层嵌套状态很复杂,请尝试将其展平。

相关文章

  • 状态模式|桥接模式|访问者模式

    状态模式 概念层次事物的状态,状态对应的行为,状态需要管理器,状态的转换。 状态模式和策略模式结构差不多,但是概念...

  • 2018-01-24

    追书神器WEB版开发心得 项目结构 vuex 状态管理

  • Redux 学习笔记

    Redux 是 JavaScript 状态容器,提供可预测化的状态管理。 action 是纯声明式的数据结构,只提...

  • 学习笔记(十八)Vuex状态管理

    Vuex状态管理 组件状态管理及组件间通信回顾 状态管理 状态集中管理和分发,解决多个组件共享状态的问题 状态自管...

  • CSS 伪类/伪元素 + CSS计数器

    1. CSS伪类 CSS伪类分为两类:状态性和结构性; (1)状态性伪类根据当前元素状态进行选择。当元素处于某状态...

  • 状态管理

    1、状态管理(1)HTTP协议中每次连接处理一个请求,当连接断开时,服务器不会记住用户曾经访问过,该特点叫做无 ...

  • 状态管理

    响应式的编程框架中都会有一个永恒的主题——“状态管理”,无论是在React/Vue(两者都是支持响应式编程的web...

  • 状态管理

    小时候我也是一个蛮努力的人,别人学习的时候 我在学习,别人玩的时候我也在学习,但是学习成绩一直都排着末尾,明...

  • 状态管理

    1.局部状态管理 局部状态管理实际上是替代了setState带来的没必要的渲染 1.状态管理 按照个人的理解来看,...

  • 状态管理

    对于状态管理,有现成的vuex和redux等框架,能方便我们的开发,但在使用这些框架的时候,还是有必要先了解下什么...

网友评论

      本文标题:第二九章 管理状态-选择状态结构

      本文链接:https://www.haomeiwen.com/subject/rrjikdtx.html