Состояние полностью приватно для компонента, объявившего его. Родительские компоненты не могут его изменить.

Хорошо! Я знаю, что и заголовок, и подзаголовок противоречат друг другу. В заголовке сказано, что мы можем изменить состояние родительского компонента, а в подзаголовке сказано, что нельзя, так как состояние является приватным для компонента, объявившего его. Второе утверждение верно. Государство является изолированным и частным. Состояние является локальным для экземпляра компонента на экране. Другими словами, если вы рендерите один и тот же компонент дважды, каждая копия будет иметь полностью изолированное состояние! Изменение одного из них не повлияет на другой. Состояние не привязано к конкретному вызову функции или месту в коде, но оно «локально» для определенного места на экране. Это позволяет добавлять состояние к любому компоненту или удалять его, не затрагивая остальные компоненты.

Однако изменение состояния дочернего элемента от родителя возможно. Позвольте мне подробно объяснить это на реальном примере. Я подготовил песочницу кода. Вы можете перейти по ссылке. У нас есть компонент Table со столбцом действий. В столбце действий есть компонент Popover со ссылкой для открытия модального окна. Как только мы откроем Modal, вы увидите, что Popover не закрывается автоматически; он срабатывает только по событию клика.

Чтобы управлять этим поведением Popover, мы используем свойство open, которое представляет собой логическое значение, указывающее состояние Popover, открыто оно или закрыто. Мы определяем состояние и изменяем состояние с помощью onOpenChange обратного вызова, выполняемого при изменении видимости карточки Popover. Здесь, если мы определим состояние в компоненте Таблица, как показано в приведенном ниже коде, состояние теперь будет общим для каждого экземпляра всплывающего окна, и у нас будет другое поведение, когда изменение состояния одного компонента всплывающего окна запускает все Popover для открытия и закрытия одновременно. Вы можете посетить codesandbox здесь для демонстрации.

// file: TableComponent.tsx

const TableComponent = () => {
  const [open, setOpen] = useState(false);

  const handleOpenChange = (newOpen: boolean) => {
    setOpen(newOpen);
  };
  
  const columns: ColumnsType<DataType> = [
    {
      title: "Name",
      dataIndex: "name",
      key: "name",
      render: (text) => <a>{text}</a>
    },
    {
      title: "Age",
      dataIndex: "age",
      key: "age"
    },
    {
      title: "Address",
      dataIndex: "address",
      key: "address"
    },
    {
      title: "Tags",
      key: "tags",
      dataIndex: "tags",
      render: (_, { tags }) => (
        <>
          {tags.map((tag) => {
            let color = tag.length > 5 ? "geekblue" : "green";
            if (tag === "loser") {
              color = "volcano";
            }
            return (
              <Tag color={color} key={tag}>
                {tag.toUpperCase()}
              </Tag>
            );
          })}
        </>
      )
    },
    {
      title: "Action",
      key: "action",
      render: (_, record) => (
        <Popover
          content={<Actions />}
          trigger="click"
          open={open}
          onOpenChange={handleOpenChange}
        >
          <EllipsisOutlined style={{ fontSize: 16 }} />
        </Popover>
      )
    }
  ];

  return <Table columns={columns} dataSource={data} pagination={false} />;
};

Чтобы решить эту проблему, мы можем поместить всплывающее окно в отдельный компонент, у которого есть внутреннее состояние, которое само отслеживает видимость. Демо представлено здесь. Тем не менее, мы еще не решили первую проблему, которую мы обсуждали, с автоматическим закрытием всплывающего окна при открытом модальном окне. Теперь у нас есть контроль над состоянием всплывающего окна. Все, что осталось сделать, это установить состояние всплывающего окна в false, когда мы нажимаем на значок Open Modal. Но как мы можем этого достичь? Поскольку состояние создается в дочернем компоненте (PopoverComponent) и для его обновления из родительского компонента (TableComponent), мы можем использовать атрибут ref. Мы предоставляем функцию setOpen родительскому компоненту, используя useImperativeHandle и ref.

В React обычно рекомендуется сохранять как можно больше состояний и логики в компонентах, которые в них нуждаются. Однако бывают случаи, когда родительскому компоненту необходимо иметь возможность вызвать метод или получить доступ к свойству дочернего компонента. Вот тут и появляется useImperativeHandle.

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

// file: PopoverComponent.tsx

import React, { useState, useImperativeHandle } from "react";
import { Popover, PopoverProps } from "antd";

export type OpenState = {
  setOpen: (visible: boolean) => void;
};

const PopoverComponent = React.forwardRef<OpenState, PopoverProps>(
  ({ ...rest }, ref) => {
    const [open, setOpen] = useState(false);

    // expose the setOpen function to the parent component using useImperativeHandle
    useImperativeHandle(ref, () => ({
      setOpen(visible: boolean) {
        setOpen(visible);
      }
    }));

    return (
      <Popover
        open={open}
        onOpenChange={(newOpen: boolean) => setOpen(newOpen)}
        {...rest}
      >
        {rest.children}
      </Popover>
    );
  }
);

export { PopoverComponent as Popover };

Мы определили тип OpenState, который используется для определения интерфейса объекта ref, который возвращается из React.forwardRef. Интерфейс OpenState определяет одно свойство, setOpen, которое представляет собой функцию, которая принимает логическое значение и возвращает void. Хук useImperativeHandle используется для предоставления функции setOpen родительскому компоненту с помощью объекта ref. Эта функция может быть вызвана родительским компонентом для установки значения состояния open в пределах PopoverComponent.

// file: TableComponent.tsx

import { useRef, useState } from "react";
....

const TableComponent = () => {
  const popoverOpenRef = useRef(null);
  ....

  const columns: ColumnsType<DataType> = [
  ....,
  {
      title: "Action",
      key: "action",
      render: (_, record) => (
        <Popover
          ref={popoverOpenRef}
          content={
            <Actions showModal={showModal} popoverOpenRef={popoverOpenRef} />
          }
          trigger="click"
        >
          <EllipsisOutlined style={{ fontSize: 16 }} />
        </Popover>
      )
    }
  ];

....
// file: Actions.tsx

import { OpenState } from "./PopoverComponent";

interface ActionsProps {
  showModal: () => void;
  popoverOpenRef: React.RefObject<OpenState>;
}

const Actions: React.FC<ActionsProps> = ({ showModal, popoverOpenRef }) => (
  <>
    <p
      style={{ fontSize: "16px", cursor: "pointer", fontWeight: 700 }}
      onClick={() => {
        showModal();
        popoverOpenRef.current?.setOpen(false);
      }}
    >
      Open Modal
    </p>
  </>
);

export default Actions;

В компоненте Table мы создаем popoverOpenRef, используя useRef, и передаем его в PopoverComponent, используя реквизит ref. Наконец, мы передаем свойство popoverOpenRef компоненту Actions, а в обработчике onClick мы вызываем функцию setOpen для popoverOpenRef, чтобы обновить состояние PopoverComponent до false.

Компонент Actions типизирован с использованием интерфейса ActionsProps, который определяет ожидаемые типы свойств. Свойство showModal определяется как функция, которая не принимает аргументов и возвращает void. Свойство popoverOpenRef определяется как объект React Ref, который ссылается на тип OpenState.

Финальное демо проекта можно найти здесь.

Заключение

Таким образом, первая проблема, которую мы обсуждали с автоматическим закрытием всплывающего окна, когда модальное окно открыто, решена, и в этом примере мы получили краткий обзор того, как мы можем обновить внутреннее состояние дочернего элемента из родительского компонента с помощью useImperativeHandle react hook. .

Удачного кодирования! Увидимся в следующей статье.