在外部组件中完成回调时,useState 值为 null

useState value is null when callback is done in external component

提问人:John kerich 提问时间:3/10/2023 最后编辑:John kerich 更新时间:3/15/2023 访问量:129

问:

我正在为多个网页创建许多按钮,并创建了一个名为 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;
reactjs react-hooks 回调

评论

0赞 Phaki 3/10/2023
您能否从按钮道具结构和传递给 ToolButtons 组件的 clickhandlers 中展示一点?
0赞 John kerich 3/10/2023
添加更多代码以显示我在做什么。

答:

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 中执行按钮,则不会发生这种情况。