【问题标题】:Updating a child object in React with UseState (and connecting it back to parent object)使用 UseState 更新 React 中的子对象(并将其连接回父对象)
【发布时间】:2020-10-18 18:53:59
【问题描述】:

我正在尝试在 React 和 material-ui 中构建一个主/详细类型用例的原型,使用一个简单的对象列表,以及一个用于编辑和保存列表中项目的对话框。我正在更新访问,用户对象的子对象,但我不知道如何 1. 将修改后的访问项与父用户对象集成回 2. 我将在哪里执行此操作以及我将在哪里创建 api调用以使用更新的访问对象保存用户。

我的测试 api 看起来像:

  • GET /users/ => 将用户对象返回为:{id, name, visit:[{id,visited}]}
  • PUT /users/ 相同的对象

我正在使用 3 个主要组件:

  1. UserVisitList.tsx(将用户对象的 api 与“访问”对象数组匹配)
  2. 带有编辑图标按钮的单个“访问”项目 (UserVisit.tsx)
  3. 一个编辑对话框,用于更改“访问”中的一个状态 (EditVisitDialog.tsx)。
  4. useEffect 中的 GetUser 函数只是一个文件,它返回一个带有访问量的用户对象。

UserVisitList.tsx(这个放在 App.tsx 中)

export const UserVisitList: React.FC = () => {
  const [user, setUser] = useState<User>({});
  const [open, setOpen] = useState<boolean>(false);
  const [visit, setVisit] = useState<Visit>({});

  const onOkClick = (visit: Visit) => {
    // set the active visit?
    setVisit(visit);
    setOpen(true);
  };

  const onDialogCloseClick = () => {
    setOpen(false);
  };

  const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    // update the visited prop of active visit
    setVisit({ ...visit, visited: e.currentTarget.value });
  };
  useEffect(() => {
    setUser(GetData("Jane"));
  }, []);

  return (
    <div>
      <Typography variant="h4">Visits for {user.name}</Typography>
      <List>
        <>
          <Grid key={user.id} container>
            <Grid item xs={12}>
              {user.visits?.map(function (visit: Visit, i) {
                return (
                  <UserVisit
                    key={visit.id}
                    visit={visit}
                    onEditClick={() => onOkClick(visit)}
                  />
                );
              })}
            </Grid>
          </Grid>

          {open && (
            <EditVisitDialog
              visit={visit}
              name={user.name}
              onChangeHandler={onChangeHandler}
              onCloseHandler={onDialogCloseClick}
            ></EditVisitDialog>
          )}
        </>
      </List>
    </div>
  );
};

export default UserVisitList;

UserVisit.tsx:

interface IVisitProp {
  visit: Visit;
  onEditClick: () => void;
}
export default function UserVisit({ visit, onEditClick }: IVisitProp) {
  return (
    <ListItem>
      <ListItemText primary={visit.visited} />
      <ListItemIcon>
        <IconButton onClick={onEditClick} edge="start">
          <EditIcon />
        </IconButton>
      </ListItemIcon>
      <ListItemSecondaryAction>
        <IconButton onClick={() => console.log("del not implemented")}>
          <DeleteIcon />
        </IconButton>
      </ListItemSecondaryAction>
    </ListItem>
  );
}

EditVisitDialog.tsx:

 interface IProps {
  visit: Visit;
  name?: string;
  onChangeHandler: (e: React.ChangeEvent<HTMLInputElement>) => void;
  onCloseHandler: () => void;
}
export default function EditVisitDialog({
  visit,
  name,
  onChangeHandler,
  onCloseHandler
}: IProps) {
  return (
    <div>
      <Dialog open={true}>
        <DialogTitle style={{ textAlign: "center" }}>
          {" "}
          Edit Visit for {name}
        </DialogTitle>
        <DialogContent>
          <div>
            <label>Visited </label>
            <input
              type="text"
              onChange={onChangeHandler}
              value={visit.visited}
            />
          </div>
        </DialogContent>
        <DialogActions>
          <Grid container justify="center">
            <Button onClick={onCloseHandler} variant="outlined">
              Close
            </Button>
            <Divider />
            <Button onClick={onCloseHandler} variant="outlined">
              Save
            </Button>
          </Grid>
        </DialogActions>
      </Dialog>
    </div>
  );
}

我感觉我做错了,就像我处理状态的方式一样。我把代码放在一个半工作的沙箱里:https://codesandbox.io/s/master-detail-with-dialog-wnlsz。我无法将更新后的访问对象返回给用户。而且我还想知道一旦我将访问连接回用户,我会在哪里调用以将用户保存到 api。

我不应该这样做吗?如果有的话,有什么更直接的方式来处理这个简单的用例。就像我说的,我认为我做的不太对。在 React 中更新数据与我习惯的非常不同(例如通过回调获取 id、更新 dom、ajax 等)。

【问题讨论】:

  • 您的整体设置看起来不错! onChange 处理程序应将值设置为本地状态,因为您不想使用键入的每个字符调用 API。您应该在 onDialogCloseClick 或 useEffect 挂钩中调用 API。无论哪种方式,您都想在调用之前检查数据是否实际发生了变化,以避免不必要的调用,因此您希望将获取的版本作为变量存储在某处。
  • @LindaPaiste 谢谢!我仍然不知道如何访问 User 对象。如何将其附加到用户对象,以便正确更新对用户对象的访问?因此,假设您将其中一次访问更新为“2019 年 11 月 10 日”。当他们点击完成时,我希望用户对象为:{ id: x, name: 'name',visited: [ {visited: "November 10th, 2019"} // changed visit ] }

标签: reactjs typescript master-detail use-state


【解决方案1】:

一堆乱七八糟的想法

通常,您希望树中较高的组件能够处理您已经做得很好的状态。虽然我会说在这种情况下,组件可以处理自己的临时状态,例如已输入但未提交的文本。

让我们考虑一下我们需要哪些信息才能进行 API 更新调用,这将引导我们了解我们需要为我们的状态使用哪些值。我们正在尝试更新特定用户的特定访问日期。所以我们需要知道要设置的值,但我们还需要知道用户 ID 和访问 ID,以便更新正确的访问。我们的 API 调用可能会获取整个更新的用户对象,并且组件负责制作具有更改日期的版本,但仍然:我们需要知道我们正在处理的访问 id。现在我们可以从visit.id 获得。

解决此问题的一种方法是仅将我们正在编辑的访问 ID 存储在 UserVisitList 中。除了状态openvisit,我们将拥有状态editingVisitId,它可以是numbernull。我们知道如果editingVisitId !== null 会打开一个对话框,我们可以通过调用user.visits.find( visit =&gt; visit.id === editingVisitId ) 来获取对话框的内容以找到正确的访问。 onChange 将在 EditVisitDialog 中处理,但我们的 UserVisitList 将传递一个 onClose 函数,该函数将新的访问文本值作为其参数。 EditVisitDialog 将负责用e.target.value 调用onClose

但是向上移动状态总是好的,所以可以像你所做的那样将输入值存储在UserVisitList 中。起初我担心如果我们这样做,我们将无法查看值是否已更改,但我现在看到我们有一个 user 状态,它与 visit 状态分开,所以没有问题!

我注意到您的类型 UserVisit 的所有字段都是可选的 (?:),当我们使用这些对象时可能会导致烦恼。我建议你删除问号,如果你想在你的应用程序的某个地方有一个不完整的对象,你可以使用Partial&lt;User&gt;Partial&lt;Visit&gt;;不过,您也可以选择保留这些字段并使用Required&lt;User&gt; 或Required 键入所需的版本。当我们声明一个对象变量必须是完整的时,它使生活更容易,因为我们可以确定我们试图访问的属性将被设置。 GetData 是一个没有意义的示例,API 将返回一个不完整的对象。它应该返回一个完整的对象或返回nullundefined 如果使用无效名称调用。

实际答案

您的状态很好,您的onChange 事件处理也很好。我们需要做的一切都是在单击“保存”按钮之后进行的。在此之前我们不想调用 API。如果我们用onChange 调用API,他们键入的每个字符都会调用它,如果他们单击“关闭”,我们将不得不再次调用它来恢复这些更改。我们绝对不希望这样。因此,“保存”事件处理程序肯定是调用 API 的正确位置。现在EditVisitDialog 得到一个回调,它在“关闭”和“保存”时都调用它,但我们将把它改为两个独立的(onCloseHandleronSaveHandler)。

我将首先转到MockApi.ts 来模拟一个编辑函数,以便我们知道我们的处理程序需要调用什么。参数和返回类型取决于您,因为这不是实际的 API。我们是传递完整的User 对象,还是只传递我们想要更改的字段?我们是将id 作为单独的参数传递还是从User 参数中获取?我们可能会返回更新后的User,但也可能会返回一个boolean,表示成功或失败。我们是否有一个单独的方法来更新访问,或者这只是一般用户更新方法的一个特定实例?我随意决定了一个签名,它接受一个完整的User,并返回一个完整的User

async updateUser(user: User): Promise<User>

我想出了一个如下所示的处理程序:

  const onDialogSaveClick = async () => {
    // early exit to avoid errors - but sould never be true
    if (!user) return;
    //close the open dialog
    setOpen(false);
    // validate/normalize the value here
    // apply the changed visit to user
    const editedUser = {
      ...user,
      visits: user.visits.map((v) =>
        v.id === visit.id
          ? { ...visit, visited: validateVisitText(visit.visited) }
          : v
      )
    };
    // post changes to the api and update user state with the response
    try {
      const apiResponse = await MockApi.updateUser(editedUser);
      setUser(apiResponse);
    } catch (error) {
      console.log(error);
    }
  };

您可以在前端、后端或两者都进行数据验证,也可以两者都不进行。我在编写代码时考虑了两者的想法。所以我们的onDialogSaveClick 将使用从 API 返回的新值更新用户状态,即使在这种情况下apiResponse 将始终等于editedUser

try/catchasync/await 的这些东西是因为想让 mock API 更像是一个带有异步调用的真实 API。

这是我的CodeSandbox Fork 的链接。大多数情况下,我更改了MockApi.tsUserVisitList.tsx,对EditVisitDialog.tsx 进行了少量更改。

【讨论】:

  • 嗨,琳达,哇,感谢您提供的非常有帮助的回复。在一个对象中连接用户和访问是我的主要问题。我需要一些 es6 的练习时间。我一直在想有一种方法可以在没有映射的情况下做到这一点。你给了我很多额外的想法,这些想法无疑会被证明是有用的。这个项目对我来说就像一个小型测试平台,现在可以快速了解 React/TS,非常感谢您从您的答案/努力中获得更多。我还没有完全吸收你所说的一切。我现在就梳理一下你的想法,看看有没有不清楚的地方!
  • 一个快速的事情出现了——传递给useState&lt;Visit&gt;.的虚拟访问对象我也不一定喜欢这样,但你说它是传播所必需的。那么(理想情况下)你会怎么做呢?
猜你喜欢
  • 2020-11-29
  • 1970-01-01
  • 2022-01-16
  • 2021-11-12
  • 1970-01-01
  • 2020-12-31
  • 2012-01-24
相关资源
最近更新 更多