【问题标题】:Updating an immutable variable in a child component更新子组件中的不可变变量
【发布时间】:2021-08-04 17:36:32
【问题描述】:

我在 App.js 中有一个全局变量 plyViewed,它设置在我的 App 组件之外。

let plyViewed = 0;

function App() {

它监控棋盘游戏的动作。板下方是一些导航按钮。如果你点击< 我做plyViewed--,如果你点击我做plyViewed++。你明白了。

这一切都很好,直到我重构!

我拿了导航按钮,谁的 JSX 代码都在 App() 函数中,并把它放在 <MoveButtons /> 中的 MoveButtons.js 中。所以,我想我可以将plyViewed 作为道具传递,然后在MoveButton 子组件中更新我的代码中的值。然后我发现道具是不可变的!现在我被卡住了。

下面的代码给出了我如何使用plyViewed 代码的示例。当有人点击导航按钮时,它会触发一个事件,触发代码更新plyViewed,尽管现在它不再工作了,因为它是一个道具。其余的游戏数据存储在一个名为 gameDetails 的对象中。

我像这样传递plyViewed

<MoveButtons
  plyViewed={plyViewed}
  // etc.
/>

下面是我的MoveList 组件的简化版本。

plyViewed 在我的应用程序的多个区域中使用,例如gameDetails。我考虑过将它放在gameDetails 对象中,但我仍然遇到gameDetails 作为道具传递时不可变的问题。然后如果我将plyViewed 设置为状态变量,它就会变成异步的,因此不适合在计算中使用。

我认为这一切都错了吗?

export default function MoveButtons(props) {
  return (
    <Grid item xs={6}>
      <Button
        variant="contained"
        color="primary"
        size="large"
        style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
        onClick={() => {
          if (props.plyViewed > 0) {
            props.plyViewed--;
            props.board.current.setPosition(props.fenHistory[props.plyViewed]);
            props.setFen(props.fenHistory[props.plyViewed]);
            props.setSelectedIndex(props.plyViewed);
          }
        }}
      >
        <NavigateBeforeIcon />
      </Button>
      <Button
        variant="contained"
        color="primary"
        size="large"
        style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
        onClick={() => {
          if (props.plyViewed < props.fenHistory.length - 1) {
            props.plyViewed++;
            props.board.current.setPosition(props.fenHistory[props.plyViewed]);
            props.setFen(props.fenHistory[props.plyViewed]);
            props.setSelectedIndex(props.plyViewed);
          }
        }}
      >
        <NavigateNextIcon />
      </Button>
    </Grid>
  );
}

【问题讨论】:

  • 嗨 Jon,尝试使用 Redux 管理全局状态
  • 不可变道具的参考:props.plyViewed++ 改变了 React 中的 props, which are immutable(即使不可执行)。
  • @AyoubGharbi 是的,我看了一下 Redux,虽然它对我来说似乎很复杂,并且想知道 Context 是否可能是一个更简单的解决方案?

标签: javascript reactjs


【解决方案1】:

您正在尝试更新从组件树中的更高级别组件向下传递的道具,这是不可能的。

您可以选择使用 React 的 useState 钩子创建一个状态并传递值和调度程序,但不建议这样做,因为您会在树下钻取道具。

您还可以将 onClick 事件(或其中的一部分)传递给您的 App 组件,这是对第一种方法的改进,但在您的情况下不是最佳实践 .

您真正应该做的是使用 React 自己的 Context APIRedux 来管理全局状态。我认为 this 可以帮助您。

【讨论】:

    【解决方案2】:

    虽然我们错过了全貌,但听起来plyViewed 应该是一个状态,并且异步行为不应该阻止任何计算如果使用 React 正确完成

    很容易忽略新的状态值是我们在设置状态时同步计算出来的。我们可以使用相同的本地值来计算其他任何东西,异步行为根本不会影响我们。

        onClick={() => {
          if (props.plyViewed > 0) {
    
            // New local value computed by ourselves synchronously.
            const updatedPlyViewed = props.plyViewed - 1;
    
            // Set the state with the new value to reflect changes on the app.
            props.setPlyViewed(updatedPlyViewed);
    
            // Use the up-to-date local value to compute anything else
            props.board.current.setPosition(props.fenHistory[updatedPlyViewed]);
            props.setFen(props.fenHistory[updatedPlyViewed]);
            props.setSelectedIndex(updatedPlyViewed);
          }
        }}
    

    这是一个非常简单的模式,应该有助于解决新状态值的最基本问题。

    简单计算

    可以在渲染阶段进行快速计算。此时将始终提供最新的状态值。如果可以从单个值轻松计算多个状态值,则无需同步多个状态值,例如此处的 plyViewed

    const [plyViewed, setPlyViewed] = useState(0);
    
    // No special state or function needed to get the position value.
    const position = fenHistory[plyViewed];
    

    这是一个交互式示例,说明如何使用简单状态在渲染阶段计算大量不同的派生信息。

    // Get a hook function
    const { useState } = React;
    
    // This component only cares about displaying buttons, the actual logic
    // is kept outside, in a parent component.
    const MoveButtons = ({ onBack, onNext }) => (
      <div>
        <button type="button" onClick={onBack}>
          Back
        </button>
        <button type="button" onClick={onNext}>
          Next
        </button>
      </div>
    );
    
    const App = () => {
      const [fenHistory, setFenHistory] = useState(["a", "b"]);
      const [plyViewed, setPlyViewed] = useState(0);
    
      const position = fenHistory[plyViewed];
    
      const onBack = () => setPlyViewed((curr) => Math.max(curr - 1, 0));
      const onNext = () =>
        setPlyViewed((curr) => Math.min(curr + 1, fenHistory.length - 1));
    
      return (
        <div>
          <p>Ply viewed: {plyViewed}</p>
          <p>Fen position: {position}</p>
          <p>Fen history: {fenHistory.join(", ")}</p>
          <button
            type="button"
            onClick={() =>
              setFenHistory((history) => [...history, `new-${history.length}`])
            }
          >
            Add to fen history
          </button>
    
          <MoveButtons onBack={onBack} onNext={onNext} />
        </div>
      );
    };
    
    // Render it
    ReactDOM.render(<App />, document.getElementById("app"));
    button {
      margin-right: 5px;
    }
    <div id="app"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

    昂贵的计算

    如果计算需要相当长的时间才能完成,并且每个渲染周期都执行此操作会明显减慢渲染速度,那么我们可以进行一些优化。

    useMemo 只会在以下情况之一重新计算记忆值 依赖关系发生了变化。这种优化有助于避免昂贵的 每次渲染都进行计算。

    const [plyViewed, setPlyViewed] = useState(0);
    
    const position = useMemo(() => /* costly computation here */, [plyViewed]);
    

    复杂的计算

    如果计算有很多依赖,我们可以使用useReducer来管理状态对象。

    请注意,以下示例不能证明使用 useReducer 的合理性,它仅用作实现示例。

    const initialState = {
      plyViewed: 0,
      fenHistory: ["a", "b"],
      positionValue: "a",
    };
    
    function reducer(state, action) {
      const { plyViewed, fenHistory } = state;
      switch (action.type) {
        case "back":
          if (fenHistory.length <= 0) return state;
          const newIndex = plyViewed - 1;
          return {
            ...state,
            plyViewed: newIndex,
            positionValue: fenHistory[newIndex],
          };
        case "next":
          if (fenHistory.length - 1 > plyViewed) return state;
          const newIndex = plyViewed + 1;
          return {
            ...state,
            plyViewed: newIndex,
            positionValue: fenHistory[newIndex],
          };
        case "add":
          return {
            ...state,
            fenHistory: [...fenHistory, action.value],
          };
        default:
          throw new Error();
      }
    }
    
    const App = () => {
      const [{ plyViewed, fenHistory, positionValue }, dispatch] = useReducer(
        reducer,
        initialState
      );
    
      const onBack = () => dispatch({ type: "back" });
      const onNext = () => dispatch({ type: "next" });
      const onAdd = () => dispatch({ type: "add", value: 'anything' });
      // ...
    

    异步计算

    如果我们需要从远程服务器获取结果,那么我们可以使用useEffect,它会在值更改时运行一次。

    const App = () => {
      const [plyViewed, setPlyViewed] = useState(0);
      const [position, setPosition] = useState(null);
    
      useEffect(() => {
        fetchPosition(plyViewed).then((newPosition) => setPosition(newPosition));
      }, [plyViewed]);
    

    useEffect 和异步设置状态存在一些缺陷。

    【讨论】:

    • 感谢您的详细回复。我一直在消化它以获得一些金块。我什至从未考虑过根据 prop 设置局部变量。在我的例子中,这很容易,但是我不能更新 plyViewed 本身,因为它是一个道具。在您的简单计算示例中,通常建议将逻辑保留在子组件之外? plyViewed 在我的应用程序的 70 行不同的代码中使用。当您说有很多“依赖项”时,这就是意思吗?
    • @Jon 通常建议将耦合保持在最低限度,无论使用何种框架。在这种情况下,buttons 组件不需要管理整个应用程序,该组件只关心渲染按钮,所以它应该只需要管理导航相关的 props 和逻辑。传递函数是将逻辑包装在父级中并将其传递给子级的好方法,而不会泄露实现细节并最终导致紧密耦合。
    • @jon 我所说的“大量依赖项”是指当某些状态需要一堆不同的数据(例如plyViewedposition、某个对象、用户等)时正确计算。由于状态更新是异步的,并且由 React 批量处理,因此有时拥有大量单独的 useState() 会使所有其他计算不同步。 useReducer 可以轻松地一次性管理状态对象,使用不同的操作,同时将逻辑隔离到 reducer 函数,这也有助于最大限度地减少耦合。
    • 如果我将逻辑保留在父组件中,我什至需要 plyViewed 的状态吗?我无法更新它,因为它位于子组件中。但是,如果我将逻辑保留在父级中并传递一个函数,我认为我可以更新 plyViewed 因为虽然它作为一个函数传递下来,但函数逻辑是在父级中定义的。还是我弄错了?
    • plyViewed 问题的出现只是因为我开始学习重构,并浏览了同名的书。然后这些代码破坏开始发生。我确实通过了 Udemy 的教程。未完成,但学习了很多。我看了 Redux,做了几节课,但我希望当我最终走这条路时,上下文 API 可能对我来说是一个更简单的解决方案。但是感谢您的帮助,这很棒。我会试一试的!
    【解决方案3】:

    然后,如果我将 plyViewed 设置为状态变量,它就会变成异步的,因此不适合在计算中使用。

    我认为这是不正确的,您只需要开始使用useEffect 即可:

    function App() {
        const [plyViewed, setPlyViewed] = useState(0);
        <MoveButtons
            plyViewed={plyViewed}
            setPlyViewed={setPlyViewed}
        />
    }
    
    export default function MoveButtons(props) {
        useEffect(() => {
            props.board.current.setPosition(props.fenHistory[props.plyViewed]);
            props.setFen(props.fenHistory[props.plyViewed]);
            props.setSelectedIndex(props.plyViewed);
        }, [props.plyViewed, props.fenHistory]);
        return (
        <Grid item xs={6}>
          <Button
            variant="contained"
            color="primary"
            size="large"
            style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
            onClick={() => {
              if (props.plyViewed > 0) {
                props.plyViewed--;
              }
            }}
          >
            <NavigateBeforeIcon />
          </Button>
          <Button
            variant="contained"
            color="primary"
            size="large"
            style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
            onClick={() => {
            if (props.plyViewed < props.fenHistory.length - 1) {
              props.plyViewed++;
            }}
          >
            <NavigateNextIcon />
          </Button>
        </Grid>
      );
    

    【讨论】:

    • plyViewed 在我的整个应用程序中使用,在其他子组件、帮助程序库等中。例如,在 gameAnalyser.js 文件中,我有 plyViewed 调用如下:` gameDetails = checkUncastledKing(gameDetails, fen, plyViewed);` 那我还需要在那个被调用的函数中做 useEffect 吗?
    • 可能不会 - 如果您已经将 plyViewed 作为道具传递,它应该继续工作,您真的只需要在您更新的地方传递 setPlyViewed,但在这个组件中您正在尝试更新,然后在下一次渲染之前使用更新的值。您可能只是传递了setPlyView(newVal); props.setFen(newVal),但是在更新然后使用新值时,我发现 useEffect 更干净
    猜你喜欢
    • 1970-01-01
    • 2021-06-12
    • 2022-10-04
    • 1970-01-01
    • 2019-07-13
    • 1970-01-01
    • 1970-01-01
    • 2023-01-04
    • 2021-06-30
    相关资源
    最近更新 更多