我有这个奇怪的问题,这是我第一次遇到它。我创建了一个使用 Redux 工具包处理应用程序创建的按钮。根据 UI 设计,按钮应该在如下所示的页面上出现两次。突出显示的按钮是相同的组件。
如果我尝试创建一个应用程序,它会显示两条 toast 消息:
我注意到,如果我删除“创建应用程序”按钮之一并保留一个,然后我尝试创建一个仅显示一条 Toast 消息的应用程序,如预期的那样。
创建一个处理一项功能的单独按钮是理想且最佳的做法吗?
这是 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;
还有我的应用程序Slice:
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;