在 React 中使用 Context 从 Parent 更新 Child 状态

Update Child state from Parent using Context in React

我有几个按钮和“查看全部”按钮。各个按钮加载该索引的相应数据或通过单击“查看全部”按钮显示所有数据。我 运行 遇到的问题是,当我单击父组件中的“查看全部”按钮时,它不会更新子组件中的状态。安装时它正常工作,但在“查看全部”中的事件处理程序上它不会更新。想知道我哪里出错了吗?

JS:

...
const Context = createContext(false);

const useStyles = makeStyles((theme) => ({
  root: {
    display: "flex",
    "& > *": {
      margin: theme.spacing(1)
    }
  },
  orange: {
    color: theme.palette.getContrastText(deepOrange[500]),
    backgroundColor: deepOrange[500],
    border: "4px solid black"
  },
  info: {
    margin: "10px"
  },
  wrapper: {
    display: "flex"
  },
  contentWrapper: {
    display: "flex",
    flexDirection: "column"
  },
  elWrapper: {
    opacity: 0,
    "&.active": {
      opacity: 1
    }
  }
}));

const ToggleItem = ({ id, styles, discription }) => {
  const { activeViewAll, handleChange } = useContext(Context);
  const [toggleThisButton, setToggleThisButton] = useState();

  const handleClick = () => {
    setToggleThisButton((prev) => !prev);
    handleChange(discription, !toggleThisButton);
  };

  return (
    <>
      <Avatar
        className={toggleThisButton && !activeViewAll ? styles.orange : ""}
        onClick={handleClick}
      >
        {id}
      </Avatar>
      <p>{JSON.stringify(toggleThisButton)}</p>
    </>
  );
};

const ToggleContainer = ({ className, selected }) => {
  return (
    <div className={className}>
      {selected.map((item, idx) => (
        <div key={idx}>Content {item}</div>
      ))}
    </div>
  );
};

export default function App() {
  const data = ["first", "second", "third"];
  const classes = useStyles();
  const [selected, setSelected] = useState([]);
  const [activeViewAll, setActiveViewAll] = useState(false);

  useEffect(() => {
    setActiveViewAll(true);
    setSelected([...data]);
  }, []);

  const handleChange = (val, action) => {
    let newVal = [];
    if (activeViewAll) {
      selected.splice(0, 3);
      setActiveViewAll(false);
    }
    if (action) {
      newVal = [...selected, val];
    } else {
      // If toggle off, then remove content from selected state
      newVal = selected.filter((v) => v !== val);
    }
    console.log("action", action);
    setSelected(newVal);
  };

  const handleViewAll = () => {
    console.log("all clicked");
    setActiveViewAll(true);
    setSelected([...data]);
  };

  return (
    <Context.Provider value={{ activeViewAll, handleChange }}>
      <div className={classes.wrapper}>
        <Avatar
          className={activeViewAll ? classes.orange : null}
          onClick={handleViewAll}
        >
          <span style={{ fontSize: "0.75rem", textAlign: "center" }}>
            View All
          </span>
        </Avatar>
        {data.map((d, id) => {
          return (
            <div key={id}>
              <ToggleItem id={id} styles={classes} discription={d} />
            </div>
          );
        })}
      </div>
      <div className={classes.contentWrapper}>
        <ToggleContainer styles={classes} selected={selected} />
      </div>
    </Context.Provider>
  );
}

Codesanbox: https://codesandbox.io/s/72166087-forked-jvn59i?file=/src/App.js:260-3117

问题

问题似乎是您混淆了布尔 activeViewAll 状态与 selected 状态的管理。

解决方案

activeViewAll 为真时,将 data 数组作为 selected prop 值传递给 ToggleContainer 组件,否则实际上传递 选中,selected状态。

简化处理程序。 handleViewAll 回调仅将视图所有状态切换为 true,handleChange 回调将视图所有状态切换回 false 和 selects/deselects 数据项。

function App() {
  const data = ["first", "second", "third"];
  const classes = useStyles();
  const [selected, setSelected] = useState([]); // none selected b/c view all true
  const [activeViewAll, setActiveViewAll] = useState(true); // initially view all

  const handleChange = (val, action) => {
    setActiveViewAll(false); // deselect view all
    setSelected(selected => {
      if (action) {
        return [...selected, val];
      } else {
        return selected.filter(v => v !== val)
      }
    });
  };

  const handleViewAll = () => {
    setActiveViewAll(true); // select view all
  };

  return (
    <Context.Provider value={{ activeViewAll, handleChange }}>
      <div className={classes.wrapper}>
        <Avatar
          className={activeViewAll ? classes.orange : null}
          onClick={handleViewAll}
        >
          <span style={{ fontSize: "0.75rem", textAlign: "center" }}>
            View All
          </span>
        </Avatar>
        {data.map((d, id) => {
          return (
            <div key={id}>
              <ToggleItem id={id} styles={classes} discription={d} />
            </div>
          );
        })}
      </div>
      <div className={classes.contentWrapper}>
        <ToggleContainer
          styles={classes}
          selected={activeViewAll ? data : selected} // pass all data, or selected only
        />
      </div>
    </Context.Provider>
  );
}

ToggleContainer 中,不要使用数组索引作为 React 键,因为您正在改变数组。使用元素值,因为它们是唯一的并且更改 order/index 不会影响值。

const ToggleContainer = ({ className, selected }) => {
  return (
    <div className={className}>
      {selected.map((item) => (
        <div key={item}>Content {item}</div>
      ))}
    </div>
  );
};

更新

既然现在了解到您不想记住切换 activeViewAll 之前选择的内容,那么当切换为 true 时清除 selected 状态数组。不是在子组件中复制选定状态,而是在上下文中传递 selected 数组并计算派生的 isSelected 状态。这为所选择的内容保持了单一的真实来源,并且消除了在组件之间“同步”状态的需要。

const ToggleItem = ({ id, styles, description }) => {
  const { handleChange, selected } = useContext(Context);
  const isSelected = selected.includes(description);

  const handleClick = () => {
    handleChange(description);
  };

  return (
    <>
      <Avatar
        className={isSelected ? styles.orange : ""}
        onClick={handleClick}
      >
        {id}
      </Avatar>
      <p>{JSON.stringify(isSelected)}</p>
    </>
  );
};

const ToggleContainer = ({ className, selected }) => {
  return (
    <div className={className}>
      {selected.map((item) => (
        <div key={item}>Content {item}</div>
      ))}
    </div>
  );
};

更新 handleChange 组件以仅采用选定的值并确定它是否需要 add/remove 该值。

export default function App() {
  const data = ["first", "second", "third"];
  const classes = useStyles();
  const [selected, setSelected] = useState([]);
  const [activeViewAll, setActiveViewAll] = useState(true);

  const handleChange = (val) => {
    setActiveViewAll(false);
    setSelected((selected) => {
      if (selected.includes(val)) {
        return selected.filter((v) => v !== val);
      } else {
        return [...selected, val];
      }
    });
  };

  const handleViewAll = () => {
    setActiveViewAll(true);
    setSelected([]);
  };

  return (
    <Context.Provider value={{ activeViewAll, handleChange, selected }}>
      <div className={classes.wrapper}>
        <Avatar
          className={activeViewAll ? classes.orange : null}
          onClick={handleViewAll}
        >
          <span style={{ fontSize: "0.75rem", textAlign: "center" }}>
            View All
          </span>
        </Avatar>
        {data.map((d, id) => {
          return (
            <div key={d}>
              <ToggleItem id={id} styles={classes} description={d} />
            </div>
          );
        })}
      </div>
      <div className={classes.contentWrapper}>
        <ToggleContainer
          styles={classes}
          selected={activeViewAll ? data : selected}
        />
      </div>
    </Context.Provider>
  );
}