简体   繁体   English

React JS 中的奇怪行为

[英]Strange Behavior in React JS

I am trying to render a function component in an Array.map loop, When I delete the first element of the Array it re-renders the main component, and instead of showing the remaining Array elements it removes the last element from the UI!我正在尝试在 Array.map 循环中渲染 function 组件,当我删除 Array 的第一个元素时,它会重新渲染主要组件,而不是显示剩余的 Array 元素,它会从 UI 中删除最后一个元素! (The deleted element stays on the UI!) (删除的元素保留在 UI 上!)

I am sure that I am deleting the correct element.我确信我正在删除正确的元素。 I have correctly used key={index} in the Array.map function.我在 Array.map function 中正确使用了 key={index}。 I tried to console.log the elements that are being rendered to the DOM in the Array.map and it shows the correct elements!我试图 console.log 在 Array.map 中渲染到 DOM 的元素,它显示了正确的元素! But the UI is different from what is logged!!!但是用户界面与记录的不同!!!


// index.tsx

/* eslint-disable prettier/prettier */
/* eslint-disable no-unused-vars */
import React, {
  forwardRef,
  useImperativeHandle,
  Ref,
  useRef,
  useState,
} from "react";
// @ts-ignore
import ReactCursorPosition from "react-cursor-position";
import { EpicFileBlock } from "./components/block";
import {
  ClassesInterface,
  StylesInterface,
  EpicStylesProvider,
  EpicClassesProvider,
  Themes,
  Layouts,
} from "./lib/theme-provider";
import "./styles.module.css";

export interface EpicFileRef {
  addFiles: (files: File | FileList | File[]) => void;
  removeFiles: (...fileNames: string[]) => void;
  empty: () => void;
}

export interface EpicFileProps {
  ref?: Ref<EpicFileRef>;
  className?: ClassesInterface;
  style?: StylesInterface;
  name?: string;
  accept?: string[];
  multiple?: boolean;
  limit?: {
    min?: number;
    max?: number;
  };
  size?: {
    min?: number;
    max?: number;
  };
  enableImagePreview?: boolean;
  instantUpload?: boolean;
  title?: string;
  text?: string;
  actionText?: string;
  theme?: Themes;
  layout?: Layouts;
  disabled?: boolean;
}

export interface FileType {
  name: string;
  type: string;
  size: number;
  ref?: string;
  src?: string;
  file?: File;
}

export const EpicFile = forwardRef<EpicFileRef, EpicFileProps>(
  (
    {
      multiple,
      title,
      text,
      actionText,
      instantUpload,
      className,
      style,
      theme,
      layout,
      disabled,
    },
    Ref
  ) => {
    // Initialize Styles Provider
    const StylesProvider = new EpicStylesProvider(
      "epic-file",
      style,
      theme,
      layout
    );

    // Initialize Classes Provider
    const ClassesProvider = new EpicClassesProvider(className);

    // Use Styles
    const DefaultClasses = StylesProvider.useStyles();

    // Overridden Classes
    const Classes = ClassesProvider.useClasses(DefaultClasses);

    const BubbleRef = useRef<HTMLDivElement>(null);

    const ShowBubble = (bubble: HTMLDivElement, X: number, Y: number) => {
      bubble.style.opacity = ".1";
      bubble.style.transform = "scale(1)";
      bubble.style.top = `${Y - 55}px`;
      bubble.style.left = `${X - 55}px`;
    };

    const HideBubble = (bubble: HTMLDivElement) => {
      bubble.style.opacity = "0";
      bubble.style.transform = "scale(3)";
    };

    const [Files, setFiles] = useState([] as Array<FileType>);

    const CreateNewFileType = (file: File, ref?: string): FileType => {
      return {
        name: file.name,
        type: file.type,
        size: file.size,
        ref,
        file,
      };
    };

    const AddFiles = (files: File | FileList | File[]) => {
      if (files instanceof File) files = [files];
      else if (files instanceof Array)
        files = files.filter((file) => file instanceof File);
      else if (files instanceof FileList) files = Array.from(files);

      // Add To Files State
      setFiles((list) => {
        // Collect Non Duplicate Files
        const FilesToAdd: File[] = [];

        // Filter Non Duplicate Files
        (files as File[]).forEach((file) => {
          if (!list.filter((fileType) => fileType.name === file.name).length)
            FilesToAdd.push(file);
        });

        // Add New Files
        FilesToAdd.forEach((file) => list.push(CreateNewFileType(file)));

        return [...list];
      });
    };

    const RemoveFiles = (...fileNames: string[]) => {
      setFiles((list) => {
        list = list.filter((file) => {
          if (fileNames.includes(file.name)) return false;
          else return true;
        });
        return [...list];
      });
    };

    const Empty = () => {
      setFiles([]);
    };

    // Put Methods to the Reference
    useImperativeHandle(Ref, () => ({
      addFiles: AddFiles,
      removeFiles: RemoveFiles,
      empty: Empty,
    }));

    const DefaultText = (
      <span>
        {text || `Drag & Drop your ${multiple ? "files" : "file"}, or`}{" "}
        <span className={Classes.action}>{actionText || "Browse"}</span>
      </span>
    );

    return (
      <div className={[Classes.mainContainer, theme].join(" ")}>
        <div ref={BubbleRef} className={[Classes.bubble, theme].join(" ")} />

        {/* Files Browser */}
        {multiple || !Files.length ? (
          <div className={Classes.inputContainer}>
            <input
              className={Classes.input}
              type="file"
              title={title || "Choose your file(s)"}
              onDragOver={(e) => {
                if (BubbleRef.current)
                  ShowBubble(
                    BubbleRef.current,
                    e.nativeEvent.x,
                    e.nativeEvent.y
                  );
              }}
              onDragLeave={() => {
                if (BubbleRef.current) HideBubble(BubbleRef.current);
              }}
              onChange={(e) => {
                if (BubbleRef.current) HideBubble(BubbleRef.current);
                e.persist();
                const Target = e.currentTarget;
                if (Target.files) AddFiles(Target.files);

                // Reset File Browser
                Target.value = "";
              }}
              multiple={multiple}
              disabled={disabled}
            />
            <p className={[Classes.text, theme].join(" ")}>{DefaultText}</p>
          </div>
        ) : (
          ""
        )}

        {/* File Blocks */}

        {/* Here is the Problem... */}

        <div className={Classes.blocksContainer}>
          {Files.map((fileType, index) => {
            console.log("List Item::", fileType.name);
            return (
              <EpicFileBlock
                index={index}
                key={index}
                fileType={fileType}
                instantUpload={instantUpload}
                theme={theme}
                layout={layout}
                Classes={Classes}
                remove={(f) => {
                  console.log("Deleting::", f.name);
                  RemoveFiles(f.name);
                }}
              />
            );
          })}
        </div>
      </div>
    );
  }
);


// ./components/block.tsx

/* eslint-disable prettier/prettier */
/* eslint-disable no-unused-vars */
import React, {
  forwardRef,
  useImperativeHandle,
  Fragment,
  useState,
} from "react";
import { Elements, Layouts, Themes } from "../lib/theme-provider";
import { motion } from "framer-motion";
import { FileType } from "..";
import { FiX, FiRotateCcw, FiUpload, FiSlash } from "react-icons/fi";

export interface EpicFileBlockProps {
  index: number;
  Classes: { [key in Elements]: string };
  fileType: FileType;
  instantUpload?: boolean;
  theme?: Themes;
  layout?: Layouts;
  remove?: (file: FileType) => void;
}

export interface EpicFileBlockRef {
  onAdd: (callback: (file: FileType) => Promise<void> | void) => void;
  onUpload: <T extends any>(
    callback: (file: FileType, response: T) => Promise<void> | void
  ) => void;
  onRevert: <T extends any>(
    callback: (file: FileType, response: T) => Promise<void> | void
  ) => void;
  onAbort: (callback: (file: FileType) => Promise<void> | void) => void;
  onRemove: (callback: (file: FileType) => Promise<void> | void) => void;
}

export type BlockStates =
  | "waiting"
  | "uploading"
  | "completed"
  | "aborted"
  | "reverted"
  | "removed";

export const EpicFileBlock = forwardRef<EpicFileBlockRef, EpicFileBlockProps>(
  ({ index, Classes, theme, fileType, remove }, Ref) => {
    const [state] = useState<BlockStates>("waiting");

    const [FileType] = useState(fileType);

    const [, setOnAdd] = useState<(file: FileType) => Promise<void> | void>(
      () => {}
    );

    const [, setOnUpload] = useState<
      (file: FileType, response: any) => Promise<void> | void
    >(() => {});

    const [, setOnRevert] = useState<
      (file: FileType, response: any) => Promise<void> | void
    >(() => {});

    const [, setOnAbort] = useState<(file: FileType) => Promise<void> | void>(
      () => {}
    );

    const [onRemove, setOnRemove] = useState<
      (file: FileType) => Promise<void> | void
    >(() => {});

    const StatusText = (state: BlockStates) => {
      if (state === "waiting") return "Waiting";
      else if (state === "uploading") return "Uploading...";
      else if (state === "completed") return "Completed";
      else if (state === "aborted") return "Aborted";
      else if (state === "reverted") return "Reverted";
      else if (state === "removed") return "Removed";
      else return "Unknown Status";
    };

    //   const FilesList = (...files: File[]) => {
    //     const Transfer = new DataTransfer();
    //     files.forEach((file) => {
    //       if (file instanceof File) Transfer.items.add(file);
    //       else
    //         throw new TypeError(
    //           "Expected argument to FileList is File or Array of File objects"
    //         );
    //     });

    //     return Transfer.files;
    //   };

    const OnAdd = (callback: (file: FileType) => Promise<void> | void) =>
      setOnAdd(callback);

    const OnUpload = <T extends any>(
      callback: (file: FileType, response: T) => Promise<void> | void
    ) => setOnUpload(callback);

    const OnRevert = <T extends any>(
      callback: (file: FileType, response: T) => Promise<void> | void
    ) => setOnRevert(callback);

    const OnAbort = (callback: (file: FileType) => Promise<void> | void) =>
      setOnAbort(callback);

    const OnRemove = (callback: (file: FileType) => Promise<void> | void) =>
      setOnRemove(callback);

    // Put Methods to the Reference
    useImperativeHandle(Ref, () => ({
      onAdd: OnAdd,
      onUpload: OnUpload,
      onRevert: OnRevert,
      onAbort: OnAbort,
      onRemove: OnRemove,
    }));

    return (
      <motion.div
        id={index.toString()}
        initial="hidden"
        animate={state === "removed" ? "hidden" : "visible"}
        variants={{
          hidden: { opacity: 0, marginTop: "-80px" },
          visible: { opacity: 1, marginTop: "0px" },
        }}
        transition={{ duration: 0.3 }}
        className={Classes.blockContainer}
      >
        <motion.div
          initial="slideDown"
          animate={state === "removed" ? "slideDown" : "slideUp"}
          variants={{
            slideDown: { transform: "translateY(100px)" },
            slideUp: { transform: "translateY(0px)" },
          }}
          transition={{ duration: 0.3 }}
          className={[Classes.block, Classes.blockPrimary, theme].join(" ")}
        >
          <div className={Classes.optionsContainer}>
            <div className={Classes.optionsColumn}>
              <p
                className={[
                  Classes.blockText,
                  Classes.blockHeading,
                  theme,
                ].join(" ")}
              >
                {StatusText(state)}
              </p>
              <motion.p
                initial="fadeInDown"
                animate="fadeInUp"
                variants={{
                  fadeInDown: { opacity: 0, transform: "translateY(10px)" },
                  fadeInUp: { opacity: 1, transform: "translateY(0px)" },
                }}
                transition={{ duration: 0.3, delay: 0.2 }}
                className={[Classes.blockText, theme].join(" ")}
              >
                {FileType.name}
              </motion.p>
            </div>
            <div className={Classes.optionsColumn}>
              {/* Controls */}

              {state === "completed" ? (
                <motion.div
                  initial="fadeInDown"
                  animate="fadeInUp"
                  variants={{
                    fadeInDown: { opacity: 0, transform: "translateY(10px)" },
                    fadeInUp: { opacity: 1, transform: "translateY(0px)" },
                  }}
                  transition={{ duration: 0.3, delay: 0.2 }}
                  className={Classes.actionButtonContainer}
                >
                  <button
                    className={[
                      Classes.actionButton,
                      Classes.actionPrimary,
                      theme,
                    ].join(" ")}
                  >
                    <FiRotateCcw />
                  </button>
                </motion.div>
              ) : (
                ""
              )}

              {["waiting", "uploading", "aborted", "reverted"].includes(
                state
              ) ? (
                <Fragment>
                  {state !== "uploading" ? (
                    <Fragment>
                      <motion.div
                        initial="fadeInDown"
                        animate="fadeInUp"
                        variants={{
                          fadeInDown: {
                            opacity: 0,
                            transform: "translateY(10px)",
                          },
                          fadeInUp: {
                            opacity: 1,
                            transform: "translateY(0px)",
                          },
                        }}
                        transition={{ duration: 0.3, delay: 0.2 }}
                        className={Classes.actionButtonContainer}
                      >
                        <button
                          className={[
                            Classes.actionButton,
                            Classes.actionDanger,
                          ].join(" ")}
                          onClick={() => {
                            // setState("removed");
                            setTimeout(async () => {
                              if (typeof onRemove === "function")
                                await onRemove(FileType);
                              return typeof remove === "function"
                                ? remove(FileType)
                                : undefined;
                            }, 1000);
                          }}
                        >
                          <FiX />
                        </button>
                      </motion.div>
                      <motion.div
                        initial="fadeInDown"
                        animate="fadeInUp"
                        variants={{
                          fadeInDown: {
                            opacity: 0,
                            transform: "translateY(10px)",
                          },
                          fadeInUp: {
                            opacity: 1,
                            transform: "translateY(0px)",
                          },
                        }}
                        transition={{ duration: 0.3, delay: 0.2 }}
                        className={Classes.actionButtonContainer}
                      >
                        <button
                          className={[
                            Classes.actionButton,
                            Classes.actionSuccess,
                            theme,
                          ].join(" ")}
                        >
                          <FiUpload />
                        </button>
                      </motion.div>
                    </Fragment>
                  ) : (
                    <motion.div
                      initial="fadeInDown"
                      animate="fadeInUp"
                      variants={{
                        fadeInDown: {
                          opacity: 0,
                          transform: "translateY(10px)",
                        },
                        fadeInUp: { opacity: 1, transform: "translateY(0px)" },
                      }}
                      transition={{ duration: 0.3, delay: 0.2 }}
                      className={Classes.actionButtonContainer}
                    >
                      <button
                        className={[
                          Classes.actionButton,
                          Classes.actionDanger,
                        ].join(" ")}
                      >
                        <FiSlash />
                      </button>
                    </motion.div>
                  )}
                </Fragment>
              ) : (
                ""
              )}
            </div>
          </div>
        </motion.div>
      </motion.div>
    );
  }
);


Actually the Array.map was working properly.实际上 Array.map 工作正常。 The problem was inside the child component that was getting rendered inside the Array.map loop.问题出在 Array.map 循环内呈现的子组件内部。

The mistake is inside the delete element function (RemoveFiles on index.tsx file):错误在删除元素 function 内(RemoveFiles on index.tsx 文件):


// Old Function
const RemoveFiles = (...fileNames: string[]) => {
      setFiles((list) => {
        list = list.filter((file) => {
          if (fileNames.includes(file.name)) return false;
          else return true;
        });
        return [...list];
      });
    };

// New Function
const RemoveFiles = (...fileNames: string[]) => {
      setFiles((list) => {
        list.forEach((file, index) => {
          if (fileNames.includes(file.name)) delete list[index];
        });
        return [...list];
      });
    };

This update will prevent from providing new indexes to all the elements.此更新将阻止为所有元素提供新索引。 For example, if I delete element on index 0 then it will keep undefined on index 0 instead of repositioning all the elements.例如,如果我删除索引 0 上的元素,那么它将在索引 0 上保持未定义,而不是重新定位所有元素。 Which will fix the problem.这将解决问题。

If I reposition all the elements in the Array then the React will try to load the same states of the deleted element which caused the strange behavior.如果我重新定位 Array 中的所有元素,那么 React 将尝试加载已删除元素的相同状态,这会导致奇怪的行为。 React will load old states because of key={0} (No matter if the props are updated!).由于 key={0},React 将加载旧状态(无论 props 是否更新!)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM