提问人:John kerich 提问时间:3/10/2023 最后编辑:John kerich 更新时间:3/15/2023 访问量:129
在外部组件中完成回调时,useState 值为 null
useState value is null when callback is done in external component
问:
我正在为多个网页创建许多按钮,并创建了一个名为 ToolButtons 的组件文件来完成所有工作。
import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import SendIcon from '@mui/icons-material/Send';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
const defaultStyling = {
flex: 1,
marginLeft: '10px',
marginRight: '10px',
textTransform: 'none'
}
/**
* This function loops through the button list and generates the button section of the page.
*
* @param props tools - Json array of buttons, title - Title for the button group, optional
* @returns {JSX.Element}
* @constructor
*/
function ToolButtons(props) {
return (
<Card variant='outlined' square>
<CardContent sx={{
display: "flex",
flexDirection: 'column',
height: '100%',
padding: 1
}}>
<Typography gutterBottom variant='h4' align='center'>{props.title}</Typography>
<Box sx ={{
flex: 1,
display: "flex",
padding: 2,
}}>
{
props.tools.buttons.map(val => (
<Button key={val.name}
sx={{...defaultStyling}}
variant='contained'
endIcon={<SendIcon />}
onClick={val.clickHandler}
>{val.text}</Button>
))
}
</Box>
</CardContent>
</Card>
);
}
export default ToolButtons;
这个 js 有按钮的开销
export const WP_BUTTONS = {
buttons: [
{
text: 'Clear Scratch Files',
name: 'clear',
clickHandler: null
},
{
text: 'Reload BaseLine',
name: 'rb',
clickHandler: null
},
{
text: 'Submit',
name: 'submit',
clickHandler: null
},
{
text: 'Restart Remote Tower',
name: 'reboot',
clickHandler: null
},
{
text: 'Deploy Files',
name: 'deploy',
clickHandler: null
}
]
}
这是 location.js 的完整版本,修复了回调。数据变量是通过后端的 useEffect 设置的。设置数据后,TextFields 都具有值。如果我编辑 a 字段,我可以在调试中看到数据变量已设置,例如:
"cameras":[{"file":"cameraConfiguration.xml","id":"panoramaCamera_Visual","baseDeviation":{"azimuth":"140","elevation":"51"},"planeDeviation":{"azimuth":"10.0","elevation":"20.0"}}, {"file":"arcotronAdapterConfiguration.xml","id":"arcotronPtz_LC","baseDeviation":{"azimuth":"23.2","elevation":"44.0"}},
{"file":"common","id":"geolocation","geolocation":{"latitude":"49.174612","longitude":"-86.813558","altitude":"135.0"}}]
但是如果我按下任何按钮,则在使用 ToolButtons.js 时回调函数显示数据为 null。如果我在位置函数中声明了按钮,则数据仍会设置。因此,回调不知何故没有共享 locationTool 内存。有什么想法为什么吗?
import React, { useEffect, useState, useRef } from "react";
import TextField from '@mui/material/TextField';
import Divider from '@mui/material/Divider';
import Box from "@mui/material/Grid";
import Grid from "@mui/material/Grid";
import Stack from "@mui/material/Stack";
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import { Restore } from '@mui/icons-material';
import * as cloneDeep from 'lodash/cloneDeep';
import ValidateLocationFields from "./ValidateLocationFields";
import Alerts from '../Alerts';
import InputAdornment from "@mui/material/InputAdornment";
import ToolButtons from "../ToolButtons";
import {WP_BUTTONS} from "./Constants";
const TWO_COLUMNS = 6; // Take up 6 of 12 slots for 2 columns
const THREE_COLUMNS = 4; // Take up 4 of 12 slots for 3 columns
const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT;
const SERVLET_NAME = process.env.REACT_APP_SERVLET_NAME;
function LocationTool() {
const [data, setData] = useState(null);
const [dataOrg, setDataOrg] = useState(cloneDeep(null));
// Holds the status for all the command buttons of the page.
const [buttons, setButtons] = useState(null);
const initializeButtons = useRef(false); // only setup activities.buttons callback 1 time
// These variables are used to populate the Alert. The open variable change will cause a redrawing of the page.
const [alertOpen, setAlertOpen] = useState(false);
const alertTitle = useRef(null);
const alertMessage = useRef(null);
const alertSeverity = useRef("info");
const url_IR = API_ENDPOINT + "/" + SERVLET_NAME + "/mcd/location";
const url_CSF = API_ENDPOINT + "/" + SERVLET_NAME + "/mcd/location/clearScratchFiles";
const url_W = API_ENDPOINT + "/" + SERVLET_NAME + "/mcd/location/write";
const url_RBL = API_ENDPOINT + "/" + SERVLET_NAME + "/mcd/location/readBL";
const url_RS = API_ENDPOINT + "/" + SERVLET_NAME + "/mcd/location/rebootServices";
const url_DS = API_ENDPOINT + "/" + SERVLET_NAME + "/mcd/location/deployServices";
const CSF_WFR= useRef(false);
const W_WFS = useRef(false);
const RBL_WFR = useRef(false);
const RS_WFR = useRef(false);
const DS_WFR = useRef(false);
const initializeAlert = (title, response) => {
if (response[0] && response[1]["status"]["code"] === 0) {
alertTitle.current = "Success";
alertMessage.current = response[1]["status"]["message"];
alertSeverity.current = "success";
} else {
alertTitle.current = title;
alertMessage.current = response[1]["status"]["message"];
alertSeverity.current = "error";
}
setAlertOpen(true);
}
const closeAlert= () => {
setAlertOpen(false);
};
const postFetchCameras = async (url) => {
try {
let response = await fetch(url, {
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
});
let message = await response.json();
if (message["status"]["code"] === 0) {
setData(message.data); /* save the json list */
const items = cloneDeep(message.data);
setDataOrg(items); /* save the json list for restore */
}
return [true, message];
} catch (error) {
console.log(error);
let message = {"status": {"code":1, "message": error.message}};
return [false, message];
}
};
const handleReloadBL = (event) => {
event.preventDefault();
if (!RBL_WFR.current) {
RBL_WFR.current = true;
postFetchCameras(url_RBL).then(response => {
initializeAlert("Error Reading Baseline Location Files", response);
RBL_WFR.current = false;
});
}
}
const postHandleSubmit = async (jsonObj) => {
try {
let response = await fetch(url_W, {
method: 'POST',
body: JSON.stringify(jsonObj),
headers: {
'Content-type': 'application/json; charset=UTF-8',
"Accept": "application/json",
},
});
let message = await response.json();
return [true, message];
} catch (error) {
console.log(error);
let message = {"status": {"code":1, "message": error.message}};
return [false, message];
}
};
const handleSubmit = (event) => {
event.preventDefault();
let errorMsg = ValidateLocationFields(data);
// Did all fields pass validation check?
if (errorMsg.length === 0) {
if (!W_WFS.current) {
W_WFS.current = true;
postHandleSubmit(data).then(response => {
initializeAlert("Error Updating Location Fields", response);
W_WFS.current = false;
});
}
} else {
alertTitle.current = "Invalid fields found";
alertMessage.current = "Please fix the following fields and submit again.\n\n".concat(errorMsg);
alertSeverity.current = "error";
setAlertOpen(true);
}
}
const postCommandResponse = async (url) => {
try {
let response = await fetch(url, {
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
});
let message = await response.json();
return [true, message];
} catch (error) {
console.log(error);
let message = {"status": {"code":1, "message": error.message}};
return [false, message];
}
};
const handleClearFiles = (event) => {
event.preventDefault();
if (!CSF_WFR.current) {
CSF_WFR.current = true;
postCommandResponse(url_CSF).then(response => {
initializeAlert("Error Clearing Location Scratch Files", response);
CSF_WFR.current = false;
});
}
}
const handleRestart = (event) => {
event.preventDefault();
if (!RS_WFR.current) {
RS_WFR.current = true;
postCommandResponse(url_RS).then(response => {
initializeAlert("Error Rebooting System", response);
RS_WFR.current = false;
});
}
}
const handleDeploy = (event) => {
event.preventDefault();
console.log("data; " + JSON.stringify(data));
if (!DS_WFR.current) {
DS_WFR.current = true;
postCommandResponse(url_DS).then(response => {
initializeAlert("Error Deploying Location Files", response);
DS_WFR.current = false;
});
}
}
const handleRestore = (groupTag, myTag, j) => {
// get the current data and copy it to the replacement json object
const items = {...data};
// Example of object pair that needs to be reset using dataOrg
// items.cameras[j].geolocation.altitude = dataOrg.cameras[j].geolocation.altitude
items.cameras[j][groupTag][myTag] = dataOrg.cameras[j][groupTag][myTag];
// now replace the old data object with the new object so the change its saved.
setData(items);
};
const handleChange = (groupTag, myTag, j, e) => {
let re = new RegExp(''
+ /^-$|/.source // dash only
+ /^-?\d+$|/.source // dash optional, n, nn
+ /^-?\d+\.$|/.source // dash optional, n., nn.
+ /^-?\d+\.\d+$|/.source // dash optional, n.n, nn.nn
+ /^-?[1-9]\d*[Ee]$|/.source // dash optional, starts with nonzero nEe, nneE
+ /^-?[1-9]\d*\.\d+[Ee]$|/.source // dash optional, starts with nonzero n.neE, nn.nneE
+ /^-?[1-9]\d*\.\d+[Ee][-+]$|/.source // dash optional, starts with nonzero n.neE-+, nn.nneE-+
+ /^-?[1-9]\d*(?:\.\d+)?[Ee][-+]?\d+$/.source // dash optional, starts with nonzero n.neE-+n, nn.nneE-+nn
);
if (e.target.value === '' || re.test(e.target.value)) {
const items = {...data};
// Example of object pair that needs to be updated
// items.cameras[j].geolocation.altitude
items.cameras[j][groupTag][myTag] = e.target.value;
setData(items);
}
}
function CameraConfiguration(index) {
return (
<>
</>
);
}
function ArcotronAdapterConfiguration(index) {
return (
<>
<br />
<Stack direction="row" sx={{my: 4}} alignItems="center" divider={<Divider orientation="vertical" flexItem />} spacing={2}>
<Typography variant="h6" align="left" gutterBottom>
File: {data.cameras[index].file}
</Typography>
<Typography variant="h6" align="left" gutterBottom>
Id: {data.cameras[index].id}
</Typography>
</Stack>
<Typography variant="h6" align="left" gutterBottom>
Tag: baseDeviation
</Typography>
<Grid container spacing={1}>
<Grid item xs={THREE_COLUMNS} sm={TWO_COLUMNS} md={TWO_COLUMNS}>
<TextField
id={"aplcbda"+index}
label="azimuth (degrees 360)"
variant="outlined"
value={data.cameras[index].baseDeviation.azimuth}
onChange={(e) => handleChange("baseDeviation", "azimuth", index, e)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
color="primary"
size="large"
sx={{ "&:hover": { color: "green" } }}
onClick={() => handleRestore("baseDeviation", "azimuth", index)}
edge="end"
>
<Restore />
</IconButton>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={THREE_COLUMNS} sm={TWO_COLUMNS} md={TWO_COLUMNS}>
<TextField
id={"aplcbde"+index}
label="elevation (+-degrees 90)"
variant="outlined"
value={data.cameras[index].baseDeviation.elevation}
onChange={(e) => handleChange("baseDeviation", "elevation", index, e)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
color="primary"
size="large"
sx={{ "&:hover": { color: "green" } }}
onClick={() => handleRestore("baseDeviation", "elevation", index)}
edge="end"
>
<Restore />
</IconButton>
</InputAdornment>
)
}}
/>
</Grid>
</Grid>
<br />
</>
);
}
function Geolocation(index) {
return (
<>
<br />
<Stack direction="row" sx={{my: 4}} alignItems="center" divider={<Divider orientation="vertical" flexItem />} spacing={2}>
<Typography variant="h6" align="left" gutterBottom>
File: {data.cameras[index].file}
</Typography>
<Typography variant="h6" align="left" gutterBottom>
Tag: geolocation
</Typography>
</Stack>
<Grid container spacing={1}>
<Grid item xs={THREE_COLUMNS} sm={THREE_COLUMNS} md={THREE_COLUMNS}>
<TextField
id={"geolat"+index}
label="latitude (+-degrees 90)"
variant="outlined"
value={data.cameras[index].geolocation.latitude}
onChange={(e) => handleChange("geolocation", "latitude", index, e)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
color="primary"
size="large"
sx={{ "&:hover": { color: "green" } }}
onClick={() => handleRestore("geolocation", "latitude", index)}
edge="end"
>
<Restore />
</IconButton>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={THREE_COLUMNS} sm={THREE_COLUMNS} md={THREE_COLUMNS}>
<TextField
id={"geolon"+index}
label="longitude (+-degrees 180)"
variant="outlined"
value={data.cameras[index].geolocation.longitude}
onChange={(e) => handleChange("geolocation", "longitude", index, e)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
color="primary"
size="large"
sx={{ "&:hover": { color: "green" } }}
onClick={() => handleRestore("geolocation", "longitude", index)}
edge="end"
>
<Restore />
</IconButton>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={THREE_COLUMNS} sm={THREE_COLUMNS} md={THREE_COLUMNS}>
<TextField
id={"geoalt"+index}
label="altitude (double) Meters"
variant="outlined"
value={data.cameras[index].geolocation.altitude}
onChange={(e) => handleChange("geolocation", "altitude", index, e)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
color="primary"
size="large"
sx={{ "&:hover": { color: "green" } }}
onClick={() => handleRestore("geolocation", "altitude", index)}
edge="end"
>
<Restore />
</IconButton>
</InputAdornment>
)
}}
/>
</Grid>
</Grid>
<br />
</>
);
}
const buttonStates = {
clear: handleClearFiles,
rb: handleReloadBL,
submit: handleSubmit,
reboot: handleRestart,
deploy: handleDeploy,
};
useEffect(() => {
// We only want to initialize this one like a constructor, not everytime the page is rendered
if (!initializeButtons.current) {
setButtons({
...WP_BUTTONS,
buttons: WP_BUTTONS.buttons.map((button) => ({
...button,
clickHandler: buttonStates[button.name],
})),
});
initializeButtons.current = true;
postFetchCameras(url_IR).then(response => {
if (!response[0]) {
initializeAlert("Error Reading Configuration Files", response);
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/**
Create the location html fields for the page.
*/
return buttons && data ? (
<Box sx={{ margin: 2 }}>
<Typography variant="h2" align="center" sx={{mb: 1}} gutterBottom>
Geolocation of Remote Tower
</Typography>
{
data && data.cameras.map((item, index) => {
if (item.file === "cameraConfiguration.xml") {
return (
CameraConfiguration(index)
)
} else if (item.file === "arcotronAdapterConfiguration.xml") {
return (
ArcotronAdapterConfiguration(index)
)
} else if (item.file === "common") {
return (
Geolocation(index)
)
}
return("")
})
}
<ToolButtons tools={buttons} title=''/>
<Alerts open={alertOpen} severity={alertSeverity.current} onClose={closeAlert}
title={alertTitle.current} message={alertMessage.current}/>
</Box>
) : null;
}
export default LocationTool;
答:
0赞
Phaki
3/10/2023
#1
所以最初MCD_STATE有 3 个按钮,它们都是空的。您正在尝试改变它们,但您没有更新您的状态“活动”。
我会创建一个活动状态的副本,修改按钮的 clickHandler 等值,然后更新MCD_STATE。此外,活动状态最初可以为 null,然后不需要创建的 ref。仅当活动不为 null 时才呈现 ToolButton。使用 useEffect 更新活动状态后,它将呈现 ToolButtons 组件。
请注意,我没有尝试过,我的代码中可能有错误!
useEffect(() => {
if (!initializeActivities.current) {
const activityButtons = MCD_STATE.buttons.map((button) => {
let clickHandler = "";
if (button.name === "reboot") {
clickHandler = "handleRestart";
} else if (button.name === "deploy") {
clickHandler = "handleDeploy";
} else if (button.name === "rpm") {
clickHandler = "handleCreateRpm";
} else {
console.error("Wrong tool option: " + button.name);
}
return {
...button,
clickHandler,
};
});
const newActivities = {
...MCD_STATE,
buttons: activityButtons,
};
setActivities(newActivities);
initializeActivities.current = true;
}
}, []);
如果您熟悉嵌套的三元运算符,那么较短的版本:
useEffect(() => {
if (!initializeActivities.current) {
const activityButtons = MCD_STATE.buttons.map((button) => ({
...button,
clickHandler:
button.name === "reboot"
? "handleRestart"
: button.name === "deploy"
? "handleDeploy"
: button.name === "rpm"
? "handleCreateRpm"
: `tool option not found ${button.name}`,
}));
setActivities({
...MCD_STATE,
buttons: activityButtons,
});
initializeActivities.current = true;
}
}, []);
带开关盒:
useEffect(() => {
if (!initializeActivities.current) {
const activityButtons = MCD_STATE.buttons.map((button) => {
let clickHandler = "";
switch(button.name) {
case "reboot":
clickHandler = "handleRestart";
break;
case "deploy":
clickHandler = "handleDeploy";
break;
case "rpm":
clickHandler = "handleCreateRpm";
break;
default:
clickHandler = `tool option not found ${button.name}`;
}
return {
...button,
clickHandler,
};
});
setActivities({
...MCD_STATE,
buttons: activityButtons,
});
initializeActivities.current = true;
}
}, []);
最短:
const buttonStates = {
reboot: "handleRestart",
deploy: "handleDeploy",
rpm: "handleCreateRpm",
};
useEffect(() => {
if (!initializeActivities.current) {
setActivities({
...MCD_STATE,
buttons: MCD_STATE.buttons.map((button) => ({
...button,
clickHandler: buttonStates[button.name],
})),
});
initializeActivities.current = true;
}
}, []);
最后是 LocationTool 组件:
const cardStyles = {
width: "86%",
marginLeft: "7%",
padding: 1,
border: "none",
boxShadow: "none",
};
function LocationTool() {
const [data, setData] = useState(null);
const [dataOrg, setDataOrg] = useState(cloneDeep(null));
const [activities, setActivities] = useState(null);
const buttonStates = {
reboot: "handleRestart",
deploy: "handleDeploy",
rpm: "handleCreateRpm",
};
useEffect(() => {
setActivities({
...MCD_STATE,
buttons: MCD_STATE.buttons.map((button) => ({
...button,
clickHandler: buttonStates[button.name],
})),
});
}, []);
const handleSubmit = (event) => {
let errorMsg = ValidateLocationFields(data);
};
return activities ? (
<Card sx={cardStyles} square>
<CardContent>
<ActivitiesSection
state={activities}
title="Maintenance and Configuration Dashboard"
/>
<ToolButtons tools={activities} title="System Tools" />
</CardContent>
</Card>
) : null;
}
评论
0赞
John kerich
3/11/2023
我更改了代码,按钮现在可以正常调用回调。代码更改的唯一问题是 buttonStates 块和 useEffect 必须在所有函数声明之后,并且双引号使函数变成字符串名称,因此我删除了它们。但是 useState() 变量在回调中仍然是 null,但在非回调函数中设置的。同样,回调内存似乎在某种程度上不是 locationTool 内存的一部分。知道useState是怎么回事吗?
0赞
Phaki
3/14/2023
我真的不明白“useState()变量在回调中仍然是null,但在非回调函数中设置”是什么意思。如果你能提供一个代码沙箱,一个小演示,这样我就可以理解你的应用程序的目的,那就太好了。useState 是属于您的组件的本地状态。可能是重新渲染导致状态刷新。你调用回调,其中发生了一些副作用,你的组件重新渲染,你的状态将像最初一样为 null。
0赞
John kerich
3/15/2023
好的,我粘贴了完整的 js 文件,我不得不剪掉相机布局以适应。如果我更改值,我会看到数据是在调试器中设置的,并且值位于其文本字段中。但是,如果我输入回调,则所有useEffect值均为null或默认值。如果我直接在 locationTools 中执行按钮,则不会发生这种情况。
评论