首页 > 解决方案 > 在重新渲染期间保持 React Portal 显示在外部窗口上

问题描述

我对应用程序这一部分的目标是,当用户检索信息时,它会显示在外部窗口中。我通过使用单独的组件来完成这项工作,但如果我打开第二个窗口,第一个门户中的内容就会消失。我认为这与状态有关。因此,我用useReducer重写了父容器,不再引用子组件而是尝试在父组件中使用createPortal。虽然窗口弹出打开正常,状态更新正常,但门户永远不会将内容呈现为第一个孩子。代码如下。

import React, {useEffect, useState, useReducer} from "react";
import fiostv from "../api/fiostv";
import Accordion from "./Accordion";
import "./App.css";
import ReactDOM from "react-dom";

const initialState = []

function reducer(state, action) {
  /*
  TODO: needs a 'REMOVE' action,
  TODO: not using the open property. Use it or delete it
   */
  
  switch (action.type) {
     case "ADD":
       return [
         ...state,
         {
           win: null,
           title: action.payload.device,
           content: action.payload.data,
           open: false
          }
         ];
     default:
       return state;
   }
}

function ConfigFetcher() {
  
  const [state, dispatch] = useReducer(reducer, initialState);
  
  const [device, setDevice] = useState("");
  const [locations, setLocations] = useState(null );
  const [selectedOption, setSelectedOption] = useState(null);
  const [content, setContent] = useState(null);
  const [firstDate, setFirstDate] = useState(null);
  const [secondDate, setSecondDate] = useState(null);
 
  
  async function fetchData(url){
    let response = await fiostv.get(url);
    response = await response.data;
    return response;
  }
  
  function formSubmit(e) {
    e.preventDefault();
    if (!firstDate || !secondDate) {
      fetchData(`/backups/${selectedOption}`).then(data => {
        if (!state.find(o => o.title === device)) {
          dispatch({type: "ADD", payload: {device, data}})
        }
      });
      
    }
    else {
      fetchData(`/diffs/${selectedOption}/${firstDate}/${secondDate}`).then(data => {
        if (data.includes("Error:")) {
          alert(data)
        }
        else {
          setContent(data)
        }
      });
    }
  }
  
  const createPortal = () => {
     if (state.length > 0 ) {
      const obj = state.find(o => o.title === device)
      const pre = document.createElement('pre');
      const div = document.createElement('div');
      div.appendChild(pre)
      const container = document.body.appendChild(div);
      obj.win = window.open('',
        obj.title,
        'width=600,height=400,left=200,top=200'
      );
      obj.win.document.title = obj.title;
      obj.win.document.body.appendChild(container);
  
      if (obj) {
        return (
          ReactDOM.createPortal(
            obj.content, container.firstChild
          )
        );
      } else {
        return null
      }
    }
  }
  
  useEffect(() => {
    createPortal();
  }, [state])
  
  const rearrange = (date_string) => {
    let d = date_string.split('-');
    let e = d.splice(0, 1)[0]
    d.push(e)
    return d.join('-');
  }
  
  function onDateChange(e) {
    if (content != null) {
      setContent(null);
    }
    e.preventDefault();
    if (e.target.name === "firstDate"){
      setFirstDate(rearrange(e.target.value));
    }
    else {
      setSecondDate(rearrange(e.target.value));
    }
  }
  
  const onValueChange = (e) => {
    if (content != null) {
      setContent(null);
    }
    setSelectedOption(e.target.value);
    setDevice(e.target.name)
  }
  
  const Values = (objects) => {
    return (
      <form onSubmit={formSubmit} className={"form-group"}>
        <fieldset>
          <fieldset>
            <div className={"datePickers"}>
              <label htmlFor={"firstDate"}>First config date</label>
              <input name={"firstDate"} type={"date"} onChange={onDateChange}/>
              <label htmlFor={"secondDate"}>Second config date</label>
              <input name={"secondDate"} type={"date"} onChange={onDateChange}/>
            </div>
          </fieldset>
          {Object.values(objects).map(val => (
            <div className={"radio"}
                 style={{textAlign: "left", width: "100%"}}>
                <input type={"radio"}
                   name={val.sysname}
                   value={val.uuid}
                   checked={selectedOption === val.uuid}
                   onChange={onValueChange}
                   style={{verticalAlign: "middle"}}
                  />
                <label
                  style={{verticalAlign: "middle", textAlign: "left", width: "50%"}}>
                  {val.sysname}
                </label>
            </div>
          ))}
          <div className={"formButtons"}>
            <button type={"submit"} className={"btn-submit"}>Submit</button>
          </div>
        </fieldset>
      </form>
    );
  }
  
  useEffect(() => {
    fetchData('/devicelocations').then(data => {setLocations(data)});
  }, []);
  

标签: javascriptreactjs

解决方案


由于 SPA 的性质,这不会像我想象的那样工作。当打开第二个窗口时,组件产生的内容总是会在新内容显示在新窗口中时“卸载”。最好的办法是只打开一个窗口并确保你有清理代码。例如,当用户关闭窗口时,有一个侦听器通过调度清理状态。如果用户没有关闭窗口但渲染了另一个组件,请确保 useEffect 挂钩中存在清理函数来清理任何状态并关闭窗口。


推荐阅读