搜尋

首頁  >  問答  >  主體

在一頁匯入 2 個元件時,Toast 訊息出現兩次

我遇到了這個奇怪的問題,這是我第一次遇到它。我創建了一個使用 Redux 工具包處理應用程式建立的按鈕。根據 UI 設計,按鈕應該會在如下所示的頁面上出現兩次。反白的按鈕是同一組件。

如果我嘗試創建一個應用程序,它會顯示兩個 toast 訊息:

我注意到,如果我刪除“創建應用程式”按鈕之一併保留一個,然後我嘗試創建一個僅顯示一條 Toast 消息的應用程序,如預期的那樣。

建立 2 個單獨的按鈕來處理一項功能是理想的最佳做法嗎?

這是 CreateAnApp 按鈕:

import React, { useState, useEffect } from "react";
import { Box, Button, Checkbox, FormControl, FormLabel, Flex, Input, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, Spinner, Text, ModalBody, ModalCloseButton, Wrap, Select, Textarea } from "@chakra-ui/react";
import { Select as Select1 } from "chakra-react-select";
import { useToast } from "@chakra-ui/react";
import { useDropzone } from "react-dropzone";
import "./style.css";
import { AiOutlineCloudUpload } from "react-icons/ai";
import { useDispatch, useSelector } from "react-redux";
import { createApp, reset } from "../../features/apps/appSlice";

export const CreateAnApp = (props) => {

  const { isOpen, onOpen, onClose } = useDisclosure();

  const { variant, bg, textColor, fontSize, fontWeight, leftIcon, hover, children, ...rest } = props;

  const { isAppLoading, isError, isAppSuccess, message } = useSelector(
    (state) => state.app
  );

  const toast = useToast();

  const [formData, setFormData] = useState({
    name: "",
    displayName: "",
    reason: "",
    product: "",
    environment: "",
  });
  const { name, displayName, reason, product, environment } = formData;

  const [icon, setIcon] = useState([]);
  const { getRootProps, getInputProps } = useDropzone({
    accept: "image/*",
    onDrop: (acceptedFiles) => {
      setIcon(
        acceptedFiles.map((file) =>
          Object.assign(file, {
            preview: URL.createObjectURL(file),
          })
        )
      );
    },
  });

  // const [product, setProduct] = useState([]);
  const [scopes, setScopes] = useState([]);
  const [institutionScope, setInstitutionScope] = useState([]);
  // const [environment, setEnvironment] = useState([]);

  const images = icon.map((file) => (
    <img
      key={file.name}
      src={file.preview}
      alt="image"
      style={{ width: "50%", height: "50%" }}
    />
  ));

  const onChange = (e) => {
    setFormData((prevState) => ({
      ...prevState,
      [e.target.name]: e.target.value,
    }));
  };

  const onCheckBoxChange = (event) => {
    if (event.target.checked) {
        setFormData((prevState) => ({
            ...prevState,
            displayName: prevState.name,
        }));
    }
}

  // handle onChange event of the dropdown
  const handleScopes = (e) => {
    setScopes(Array.isArray(e) ? e.map((x) => x.value) : []);
  };
  const handleInstitutionScope = (e) => {
    setInstitutionScope(Array.isArray(e) ? e.map((x) => x.value) : []);
  };
  // const handleEnvironment = (e) => {
  //   setEnvironment(Array.isArray(e) ? e.map((x) => x.value) : []);
  // };

  const scopesOptions = [
    {
      label: "Transactions",
      value: "Transactions",
    },
    {
      label: "Accounts",
      value: "Accounts",
    },
  ];

  const institutionScopeOptions = [
    {
      label: "Neobanks",
      value: "Neobanks",
    },
    {
      label: "DeFi/CeFi",
      value: "DeFi/CeFi",
    },
    {
      label: "Personal finance",
      value: "Personal finance",
    },
    {
      label: "Investments",
      value: "Investments",
    },
    {
      label: "Wallets",
      value: "Wallets",
    },
  ];

  const dispatch = useDispatch();

  useEffect(() => {

    if (isError) {
      toast({
        title: "Error",
        description: message,
        status: "error",
        position: "top-right",
        duration: 5000,
        isClosable: true,
      });
      dispatch(reset());
    }

    if (isAppSuccess) {
      toast({
        title: "App created",
        description: "Refreshing page",
        status: "success",
        position: "top-right",
        duration: 5000,
        isClosable: true,
      });
      dispatch(reset());
      onClose();
    }
  }, [isAppSuccess, reset]);

  const onSubmit = async (e) => {
    e.preventDefault();

    const appData = {
      name,
      displayName,
      product,
      // icon,
      scopes,
      reason,
      institutionScope,
      environment,
    };
    dispatch(createApp(appData));
  };

  function SubmitButton() {
    if (
      name?.length &&
      displayName?.length &&
      scopes?.length &&
      environment?.length &&
      reason?.length > 8 &&
      institutionScope?.length > 0
    ) {
      return (
        <Button
          fontSize={{ sm: "12px", md: "14px" }}
          type="submit"
          borderRadius="md"
          color="white"
          bg="#002C8A"
          _hover={{ bg: "#002C6A" }}
          width={{ sm: "300px", md: "400px" }}
        >
          {isAppLoading ? <Spinner /> : "Create app"}
        </Button>
      );
    } else {
      return (
        <Button
          fontSize={{ sm: "12px", md: "14px" }}
          type="submit"
          borderRadius="md"
          color="white"
          bg="#002C8A"
          _hover={{ bg: "#002C6A" }}
          width={{ sm: "300px", md: "400px" }}
          isDisabled
        >
          {isAppLoading ? <Spinner /> : "Create app"}
        </Button>
      );
    }
  }

  return (
    <div>
      <Button
        {...rest}
        leftIcon={leftIcon}
        onClick={onOpen}
        bg={bg}
        textColor={textColor}
        borderRadius="lg"
        variant="solid"
        fontSize={fontSize}
        _hover={_hover}
        fontWeight={fontWeight}
      >
        Create an app
      </Button>

      <Modal
        size="lg"
        closeOnOverlayClick={false}
        isOpen={isOpen}
        onClose={onClose}
      >
        <ModalOverlay />
        <ModalContent mt={1}>
          <ModalHeader textAlign="center" fontSize="md" color="#002c8a">
            Create an app
          </ModalHeader>
          <ModalCloseButton />
          <form onSubmit={onSubmit}>
            <ModalBody pb={6}>
              <Flex flexDirection={{ sm: "column", md: "row" }}>
                <Box>
                  <Box mb={6}>
                    <FormControl>
                      <FormLabel fontSize="sm" fontWeight="semibold">
                        Add a logo to personalize your app
                      </FormLabel>
                      <Box
                        width={{ sm: "340px", md: "450px" }}
                        className="dropArea"
                        {...getRootProps()}
                      >
                        <input {...getInputProps()} />
                        <Flex
                          className="text"
                          width={{ sm: "340px", md: "450px" }}
                        >
                          {images?.length > 0 && (
                            <>
                              <div>{images}</div>
                            </>
                          )}
                          {images?.length === 0 && (
                            <>
                              <Box>
                                <AiOutlineCloudUpload size={30} />
                              </Box>
                              <Box>
                                <Text fontSize="sm">
                                  Drop app icon here or{" "}
                                  <Button
                                    variant="link"
                                    fontSize="sm"
                                    color="#002c8a"
                                  >
                                    browse
                                  </Button>
                                </Text>
                              </Box>
                            </>
                          )}
                        </Flex>
                      </Box>
                    </FormControl>
                  </Box>
                  <Flex mt={6}>
                    <Box>
                      <FormControl>
                        <FormLabel fontSize="sm" fontWeight="semibold">
                          App Name
                        </FormLabel>
                        <Input
                          fontSize="14"
                          width={{ sm: "165px", md: "225px" }}
                          name="name"
                          type="name"
                          value={name}
                          onChange={onChange}
                        />
                      </FormControl>
                      <Checkbox mt={1} onChange={onCheckBoxChange} size='sm'  css={`
                      > span:first-of-type {
                        box-shadow: unset;
                      }
                    `}><Text fontSize="10.9px">Use as display name</Text></Checkbox>
                    </Box>
                    <Box ml={2}>
                      <FormControl>
                        <FormLabel fontSize="sm" fontWeight="semibold">
                          Display Name
                        </FormLabel>
                        <Input
                          fontSize="14"
                          width={{ sm: "165px", md: "225px" }}
                          name="displayName"
                          type="name"
                          value={displayName}
                          onChange={onChange}
                        />
                      </FormControl>
                    </Box>
                  </Flex>
                  <Box mt={3}>
                    <FormLabel fontSize="sm" fontWeight="semibold">
                      Product
                    </FormLabel>
                    <Select
                      name="product"
                      placeholder=" "
                      fontSize="14"
                      value={product}
                      onChange={onChange}
                    >
                      <option value="Connect">Connect</option>
                      <option value="Directpay">Directpay</option>
                    </Select>
                  </Box>
                  <Box w="100%" mt={3}>
                    <FormLabel fontSize="sm" fontWeight="semibold">
                      Account
                    </FormLabel>
                    <Select1
                      useBasicStyles
                      isMulti
                      name="scopes"
                      colorScheme="blue"
                      placeholder=" "
                      options={scopesOptions}
                      closeMenuOnSelect={false}
                      value={scopesOptions?.filter((obj) =>
                        scopes?.includes(obj.value)
                      )} // set selected values
                      onChange={handleScopes}
                    />
                  </Box>
                  <Box w="100%" mt={3}>
                    <FormLabel fontSize="sm" fontWeight="semibold">
                      Institution
                    </FormLabel>
                    <Select1
                      useBasicStyles
                      isMulti
                      name="institution"
                      colorScheme="blue"
                      placeholder=" "
                      _placeholder={{ color: "red" }}
                      options={institutionScopeOptions}
                      closeMenuOnSelect={false}
                      value={institutionScopeOptions?.filter((obj) =>
                        institutionScope?.includes(obj.value)
                      )} // set selected values
                      onChange={handleInstitutionScope}
                    />
                  </Box>
                  <Box w="100%" mt={3}>
                  <FormLabel fontSize="sm" fontWeight="semibold">
                      Environment
                    </FormLabel>
                    <Select
                      name="environment"
                      placeholder=" "
                      fontSize="14"
                      color="black"
                      value={environment}
                      onChange={onChange}
                    >
                      <option value="Sandbox">Sandbox</option>
                      <option value="Production">Production</option></Select>
                    {/*<FormLabel fontSize="sm" fontWeight="semibold">
                      Environment
                    </FormLabel>
                    <Select1
                      useBasicStyles
                      name="environment"
                      isMulti
                      placeholder=" "
                      options={environmentOptions}
                      closeMenuOnSelect={true}
                      color="black"
                      value={environmentOptions?.filter((obj) =>
                        environment?.includes(obj.value)
                      )} // set selected values
                      onChange={handleEnvironment}
                      />*/}
                  </Box>

                  <Box w="100%" mt={4}>
                    <Textarea
                      placeholder="Reason for data access"
                      fontSize="sm"
                      value={reason}
                      name="reason"
                      type="string"
                      onChange={onChange}
                      colorScheme="blue"
                    />
                  </Box>
                </Box>
              </Flex>
            </ModalBody>

            <Wrap mb={6} justify="center">
              <SubmitButton />
            </Wrap>
          </form>
        </ModalContent>
      </Modal>
    </div>
  );
};

這是應用程式頁面:

import {
  Box,
  Button,
  Flex,
  Spacer,
  Center,
  Skeleton,
  SkeletonCircle,
  SkeletonText,
  Text,
  VStack,
  Image,
  Spinner,
  SimpleGrid,
  HStack,
  Avatar,
  Stack,
  Select,
  Hide,
  Tag,
} from "@chakra-ui/react";
import React, { useState, useEffect } from "react";
import { MdFilterList } from "react-icons/md";
import { IoIosApps } from "react-icons/io";
import { ArrowLeftIcon, ArrowRightIcon, SpinnerIcon } from "@chakra-ui/icons";
import { CreateAnApp } from "../../../../components/Buttons/CreateAnApp";
import { useDispatch, useSelector } from "react-redux";
import { getAllApps } from "../../../../features/apps/appSlice";
import moment from "moment";
import { Link, useNavigate } from "react-router-dom";
import Card from "#components/Card/Card";
import CardBody from "#components/Card/CardBody";
import transaction_blue from "#assets/svg/transaction_blue.svg";
import { BsPlusCircleFill } from "react-icons/bs";
import useLocalStorage from "use-local-storage";

const Apps = () => {
  const dispatch = useDispatch();

  const { apps, isLoading, isAppSuccess, meta } = useSelector(
    (state) => state.app
  );
  
  const [mode] = useLocalStorage("apiEnv", false);

  const [loading, setLoading] = useState(true);
  useEffect(() => {
    setTimeout(() => {
      setLoading(false);
    }, 2000);
  }, [loading]);

  const fetchApps = () => {
    dispatch(getAllApps());
  };

  useEffect(() => {
    fetchApps();
  }, [isAppSuccess]);

  return (
    <>
        <Flex alignItems="center" mt={-3} ml={-4} p="5px" mb="10px">
          <Spacer />
          <Flex>
            <Box>
              <Skeleton borderRadius="lg" isLoaded={!loading}>
                <Button
                  leftIcon={<MdFilterList size={20} />}
                  variant="outline"
                  textColor="black"
                  borderRadius="lg"
                  fontSize={{ sm: "xs", md: "sm" }}
                  fontWeight="normal"
                >
                  Filter
                </Button>
              </Skeleton>
            </Box>
            <Box ml={4}>
              <Skeleton borderRadius="lg" isLoaded={!loading}>
                <CreateAnApp
                  bg="#002C8A"
                  textColor="white"
                  fontSize={{ sm: "xs", md: "sm" }}
                  _hover={{ bg: "#002C6A" }}
                  leftIcon={<BsPlusCircleFill size={16} />}
                  fontWeight="normal"
                />
              </Skeleton>
            </Box>
          </Flex>
        </Flex>
        {isLoading ? (
          <Center>
            <Spinner mt={20} />
          </Center>
        ) : (
          <SimpleGrid mt={10} minChildWidth="360px" spacing="40px">
            {isLoading ? (
              <Center>
                <Spinner mt={20} />
              </Center>
            ) : apps && apps?.length > 0 ? (
              apps &&
              apps?.map((app) => {
                return (
                  <Skeleton borderRadius="lg" isLoaded={!loading}>
                    <Box
                      _hover={{ bg: "white" }}
                      h="150px"
                      as="button"
                      shadow="lg"
                      p={2}
                      w={{ sm: "85%", md: "350px" }}
                      bg="#f5f5f5"
                      borderRadius="lg"
                    >
                      <Link to={`/admin/viewapp/${app.uid}`}>
                        <Box>
                          {app.environment === "Sandbox" && (
                            <Box align="right" mt={-4}>
                              <Tag
                                variant="solid"
                                borderRadius="10px"
                                size="sm"
                                colorScheme="orange"
                                fontSize="xs"
                                textTransform="uppercase"
                              >
                                Sandbox
                              </Tag>
                            </Box>
                          )}

                          {app.environment === "Production" && (
                            <Box align="right" mt={-4}>
                              <Tag
                                variant="solid"
                                borderRadius="10px"
                                colorScheme="green"
                                size="sm"
                                fontSize="xs"
                                textTransform="uppercase"
                              >
                                Production
                              </Tag>
                            </Box>
                          )}
                          <HStack>
                            <Box mt={3} ml={6}>
                              <Avatar
                                bg="black"
                                color="white"
                                name={app.name}
                              />
                            </Box>
                            <Box>
                              <Stack ml={2}>
                                <Box mt={-1}>
                                  <Text
                                    color="orange"
                                    textTransform="uppercase"
                                    fontSize="12px"
                                  >
                                    {app.product}
                                  </Text>
                                </Box>
                                <Box>
                                  <Text
                                    fontSize={{ sm: "sm", md: "lg" }}
                                    fontWeight="bold"
                                  >
                                    <SkeletonText isLoaded={!loading}>
                                      {app.displayName}
                                    </SkeletonText>
                                  </Text>
                                </Box>
                              </Stack>
                            </Box>
                          </HStack>
                          <Text fontSize="sm">
                            Created on {moment(app.createdAt).format("LL")}
                          </Text>
                        </Box>
                      </Link>
                    </Box>
                  </Skeleton>
                );
              })
            ) : (
              <Center ml={{ sm: "0", md: -32 }}>
                <VStack spacing={4} align="stretch">
                  <Box>
                    <Center>
                      <SkeletonCircle isLoaded={!loading}>
                        <IoIosApps size={45} />
                      </SkeletonCircle>
                    </Center>
                  </Box>
                  <Box>
                    <Text fontSize="30px" fontWeight={700}>
                      <SkeletonText isLoaded={!loading}>
                        No apps yet
                      </SkeletonText>
                    </Text>
                    <Text mb={4}>
                      <SkeletonText noOfLines={1} mt={2} isLoaded={!loading}>
                        Create an app to get started
                      </SkeletonText>
                    </Text>
                    <Skeleton borderRadius="lg" isLoaded={!loading}>
                      <CreateAnApp
                        w="200px"
                        h="50px"
                        leftIcon={
                          <BsPlusCircleFill
                            className="bg-[#002C8A] hover: none text-white"
                            size={30}
                          />
                        }
                        bg="#002C8A"
                        _hover={{ bg: "#002C6A" }}
                        color="white"
                      />
                    </Skeleton>
                  </Box>
                  <Box></Box>
                </VStack>
              </Center>
            )}

            {apps && apps?.length > 1 && (
              <Skeleton borderRadius="lg" isLoaded={!loading}>
                <CreateAnApp
                  h="150px"
                  ml={{ sm: 0, md: -20 }}
                  shadow="lg"
                  leftIcon={
                    <BsPlusCircleFill
                      className="bg-[#f5f5f5] text-blue-800"
                      size={30}
                    />
                  }
                  bg="#f5f5f5"
                  textColor="black"
                  border="2px"
                  borderColor="gray.400"
                  borderStyle="dashed"
                  fontSize={{ sm: "sm", md: "2xl" }}
                  fontWeight="bold"
                  p={2}
                  w={{ sm: "85%", md: "350px" }}
                />
              </Skeleton>
            )}
          </SimpleGrid>
        )}
      </Box>
    </>
  );
};

export default Apps;

還有我的 appSlice:

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import appService from "./appService";

const initialState = {
  apps: [],
  app: [],
  isLoading: false,
  isAppLoading: false,
  isError: false,
  isAppSuccess: false,
  isSuccess: false,
  message: "",
};

// Create new app
export const createApp = createAsyncThunk(
  "app/createApp",
  async (appData, thunkAPI) => {
    try {
      const token = sessionStorage.getItem("token");
      return await appService.createApp(appData, token);
    } catch (error) {
      const message =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();
      return thunkAPI.rejectWithValue(message);
    }
  }
);

// Get all apps
export const getAllApps = createAsyncThunk(
  "app/getAllApps",
  async (_, thunkAPI) => {
    try {
      const token = sessionStorage.getItem("token");
      return await appService.getAllApps(token);
    } catch (error) {
      const message =
        (error.response &&
          error.response.data &&
          error.response.data.message) ||
        error.message ||
        error.toString();
      return thunkAPI.rejectWithValue(message);
    }
  }
);

export const appSlice = createSlice({
  name: "app",
  initialState,
  reducers: {
    reset: (state) => {
      (state.isLoading = false),
      (state.isAppSuccess= false),
      (state.isAppLoading = false),
        (state.isSuccess = false),
        (state.isError = false),
        (state.message = "");
    },
  },

  extraReducers: (builder) => {
    builder
      .addCase(createApp.pending, (state) => {
        state.isAppLoading = true;
        state.isError = false;
      })
      .addCase(createApp.fulfilled, (state, action) => {
        state.isAppLoading = false;
        state.isAppSuccess = true;
        state.app = action.payload;
      })
      .addCase(createApp.rejected, (state, action) => {
        state.isAppLoading = false;
        state.isError = true;
        state.message = action.payload;
      })

      .addCase(getAllApps.pending, (state) => {
        state.isLoading = true;
        state.isError = false;
      })
      .addCase(getAllApps.fulfilled, (state, action) => {
        state.isLoading = false;
        state.apps = action.payload.payload.data;
      })
      .addCase(getAllApps.rejected, (state, action) => {
        state.isLoading = false;
        state.isError = true;
        state.message = action.payload;
      })
  },
});

export const { reset } = appSlice.actions;
export default appSlice.reducer;

P粉674999420P粉674999420471 天前652

全部回覆(1)我來回復

  • P粉282627613

    P粉2826276132023-09-15 00:34:43

    我透過將 toast 訊息函數從「建立應用程式」中的 useEffect 掛鉤移到「應用程式」頁面來修復此問題。剛剛在廁所裡想出來的哈哈。我無法對此進行更多闡述,因為我還沒有完全理解它。我們每天都在學習

    更新了「建立應用程式」按鈕中的 useEffect 掛鉤:

    useEffect(() => {
    
        if (isError) {
          dispatch(reset());
        }
    
        if (isAppSuccess) {
          dispatch(reset());
          onClose();
        }
      }, [isAppSuccess]);

    更新的應用程式頁面:

    useEffect(() => {
    
        if (isError) {
          toast({
            title: "Error",
            description: message,
            status: "error",
            position: "top-right",
            duration: 5000,
            isClosable: true,
          });
        }
    
        if (isAppSuccess) {
          toast({
            title: "App created",
            description: "Refreshing page",
            status: "success",
            position: "top-right",
            duration: 5000,
            isClosable: true,
          });
        }
      }, [isAppSuccess]);

    回覆
    0
  • 取消回覆