【问题标题】:React Warning: Cannot update a component from inside the function body of a different component反应警告:无法从不同组件的函数体内更新组件
【发布时间】:2020-06-16 23:43:36
【问题描述】:

我在 React 中使用带有类组件的 Redux。在 Redux 存储中具有以下两种状态。

{ spinner: false, refresh: false }

在父组件中,我有一个调度函数来改变这种状态。

class App extends React.Component {
  reloadHandler = () => {
    console.log("[App] reloadComponent");

    this.props.onShowSpinner();
    this.props.onRefresh();
  };

  render() {
    return <Child reloadApp={this.reloadHandler} />;
  }
}

在子组件中,我正在尝试重新加载父组件,如下所示。

class Child extends React.Component {
  static getDerivedStateFromProps(props, state) {
    if (somecondition) {
      // doing some redux store update
      props.reloadApp();
    }
  }

  render() {
    return <button />;
  }
}

我收到如下错误。

警告:无法从函数体内部更新组件 不同的组件。

如何删除此警告?我在这里做错了什么?

【问题讨论】:

  • 将代码放在getDerivedStateFromProps而不是componentDidUpdate的原因是什么?

标签: javascript reactjs redux


【解决方案1】:

对我来说,我是在 React Hook 中派发到我的 redux 商店。我不得不发送 useEffect 以正确与 React 渲染周期同步:

export const useOrderbookSubscription = marketId => {
  const { data, error, loading } = useSubscription(ORDERBOOK_SUBSCRIPTION, {
    variables: {
      marketId,
    },
  })

  const formattedData = useMemo(() => {
    // DISPATCHING HERE CAUSED THE WARNING
  }, [data])

  // DISPATCHING HERE CAUSED THE WARNING TOO

  // Note: Dispatching to the store has to be done in a useEffect so that React
  // can sync the update with the render cycle otherwise it causes the message:
  // `Warning: Cannot update a component from inside the function body of a different component.`
  useEffect(() => {
    orderbookStore.dispatch(setOrderbookData(formattedData))
  }, [formattedData])

  return { data: formattedData, error, loading }
}

【讨论】:

  • 传奇。对我来说,我在根本不使用钩子的情况下调度到 Redux 商店,我仍然会收到这个警告,但很奇怪(就像所有 React 的情况一样),只有一些调度,而不是全部,即使他们都更新了商店,导致重新渲染。我会说这是 v16.13.0 中引入的一个错误,错误报告了警告,如此处所述:github.com/facebook/react/issues/18178#issuecomment-601425520 正如警告所示,我当然不会从另一个组件更新一个组件,只是相同的组件。无论如何,useEffect 使警告消失了。
  • 这也是给我的。我一一删除了我的调度事件,直到找到有问题的 fn。将其移至 componentDidMount(或 useEffect)并且它起作用了!由于我有条件地加载组件,这是一个问题。或者,您可以使用棘轮并将有条件地加载组件的标志(例如您的 useState 的 set 方法)放在 setTimeout 中,它也可以工作......但你没有听到我的声音:x跨度>
【解决方案2】:

如果您的代码在满足如下条件时调用父组件中的函数:

const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
    const { data, loading, error } = useQuery(QUERY);

    if (data && data.users.length === 0) {
        return handleNoUsersLoaded();
    }

    return (
        <div>
            <p>Users are loaded.</p>
        </div>
    );
};

尝试将条件包装在useEffect

const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
    const { data, loading, error } = useQuery(QUERY);

    useEffect(() => {
        if (data && data.users.length === 0) {
            return handleNoUsersLoaded();
        }
    }, [data, handleNoUsersLoaded]);

    return (
        <div>
            <p>Users are loaded.</p>
        </div>
    );
};

【讨论】:

  • 将条件包装在 useEffect 中对我有用。获取数据后,我收到此代码的错误:if (data) dispatch(actionFunction({ data }); if (!data) return &lt;Loading /&gt;; 并修复了它:useEffect(() =&gt; { if (data) dispatch(actionFunction({ data }); }, [data, dispatch]); if (!data) return &lt;Loading /&gt;;
  • 谢谢你,我有代码可以响应道具的变化,但我忘了把它放在useEffect 钩子里。感谢您的帮助!
【解决方案3】:

您似乎拥有React@16.13.x 的最新版本。你可以找到更多关于它的细节here。指定您不应该 setState 来自其他组件的另一个组件。

来自文档:

渲染时支持调用 setState,但仅限于同一个组件。如果您在渲染不同组件的过程中调用 setState,您现在将看到一条警告:
Warning: Cannot update a component from inside the function body of a different component.
此警告将帮助您找到由意外状态更改引起的应用程序错误。 em> 在极少数情况下,由于渲染而有意更改另一个组件的状态,您可以将 setState 调用包装到 useEffect 中。


回到实际问题。

我认为子组件主体中不需要getDerivedStateFromProps。如果要触发绑定事件。然后您可以通过子组件的onClick 调用它,因为我可以看到它是&lt;button/&gt;

class Child extends React.Component {
  constructor(props){
    super(props);
    this.updateState = this.updateState.bind(this);
  }
  updateState() { // call this onClick to trigger the update
    if (somecondition) {
      // doing some redux store update
      this.props.reloadApp();
    }
  }

  render() {
    return <button onClick={this.updateState} />;
  }
}

【讨论】:

  • 这个答案帮助我解决了问题。我遇到了罕见的情况。将其包装在 useEffect 中会消除错误。非常感谢。如果有人仍然不确定,请随时与我们联系。
  • @f0rfun 你可以在这里发布。这肯定会对其他人有所帮助,因为这是新事物。
  • 在某种程度上这解决了我的问题(甚至没有直接相关,但仍然存在)。早上的棉眼用onPress={navigation.navigate(...)}代替onPress={() =&gt; navigation.navigate(...)}
【解决方案4】:

同样的错误但不同的场景

tl;drsetTimeout 中封装状态更新修复了它。

这种情况导致了 IMO 是有效用例的问题。

const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
  setSomeState(someNewValue);
}).current;
return (
  <SomeComponent onSomeUpdate={doUpdate} />
);

修复

const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
  setTimeout(() => {
   setSomeState(someNewValue);
  }, 0);
}).current;
return (
  <SomeComponent onSomeUpdate={doUpdate} />
);

【讨论】:

  • wrapping state update in setTimeout fixes it 但为什么呢?
  • @AbhilashKishore setTimout 安排函数在当前事件循环之后发生,在当前事件上下文之外,该上下文具有否则会触发控制台警告的标志。
  • 虽然我不知道它为什么会修复,但你的解决方案对我来说很好。谢谢杰森!
【解决方案5】:

就我而言,我错过了箭头函数()=&gt;{}

而不是onDismiss={()=&gt;{/*do something*/}}

我是onDismiss={/*do something*/}

【讨论】:

  • 我在两个地方遇到了这个错误,修复了两个地方,让箭头功能得到帮助。
【解决方案6】:

升级 react 和 react native 后我遇到了同样的问题,我只是通过将 props.navigation.setOptions 放入 useEffect 解决了这个问题。如果有人面临与我相同的问题,我只想建议他将您的状态更改或在 useEffect 中进行任何操作

【讨论】:

    【解决方案7】:

    注释了一些代码行,但这个问题是可以解决的 :) 出现此警告是因为您在其他类中同步调用 reloadApp,将调用推迟到 componentDidMount()

    import React from "react";
    
    export default class App extends React.Component {
      reloadHandler = () => {
        console.log("[App] reloadComponent");
    
        // this.props.onShowSpinner();
        // this.props.onRefresh();
      };
    
      render() {
        return <Child reloadApp={this.reloadHandler} />;
      }
    }
    
    class Child extends React.Component {
      static getDerivedStateFromProps(props, state) {
        // if (somecondition) {
        // doing some redux store update
        props.reloadApp();
        // }
      }
    
      componentDidMount(props) {
        if (props) {
          props.reloadApp();
        }
      }
    
      render() {
        return <h1>This is a child.</h1>;
      }
    }
    

    【讨论】:

    • 老实说,我不敢相信这不是公认的答案,甚至是投票率很高的答案。这有确切的原因和完美的解决方案。
    【解决方案8】:

    我在使用 redux 和 react-native-swiper 保存 swiperIndex 时遇到了这个错误

    通过将 changeSwiperIndex 设置为超时来修复它

    【讨论】:

    • 我遇到了同样的问题并按照您的建议进行了修复...我想知道我们是否不能直接在 react-native-swiper 中修复它...
    【解决方案9】:

    在调用屏幕之间的导航时,我得到了一个 react native 项目的以下内容。

        Warning: Cannot update a component from inside the function body of a different component.
    

    我以为是因为我使用了 TouchableOpacity。这不是使用 Pressable、Button 或 TouchableOpacity 的问题。当我收到错误消息时,我从主屏幕调用 ChatRoom 屏幕的代码如下:

        const HomeScreen = ({navigation}) => { 
        return (<View> <Button title = {'Chats'} onPress = { navigation.navigate('ChatRoom')} <View>) } 
    

    由此产生的行为是代码发出了该警告,我无法返回之前的 HomeScreen 并重新使用该按钮导航到 ChatRoom。解决方案是在内联匿名函数中执行 onPress。

        onPress{ () => navigation.navigate('ChatRoom')}
    

    而不是以前的

        onPress{ navigation.navigate('ChatRoom')}
    

    所以现在就像预期的行为一样,我可以使用可重复使用的按钮从 Home 转到 ChatRoom 并再次返回。

    PS:StackOverflow 中的第一个答案。还在学习社区礼仪。让我知道我可以改进什么以更好地回答。谢谢

    【讨论】:

      【解决方案10】:

      如果您想从子组件自动调用作为 props 传递的某些函数,那么最好的位置是在类组件的情况下使用 componentDidMount 生命周期方法,或者在功能组件的情况下使用 useEffect 挂钩,因为此时组件已完全创建并已安装。

      【讨论】:

        【解决方案11】:

        我在编写一个带有几个文本框的过滤器组件时遇到了这个问题,它允许用户将列表中的项目限制在另一个组件中。我在 Redux 状态下跟踪我的过滤项目。该解决方案本质上是@Rajnikant 的解决方案;带有一些示例代码。

        由于关注,我收到了警告。注意渲染函数中的props.setFilteredItems

        import {setFilteredItems} from './myActions';
        const myFilters = props => {
          const [nameFilter, setNameFilter] = useState('');
          const [cityFilter, setCityFilter] = useState('');
        
          const filterName = record => record.name.startsWith(nameFilter);
          const filterCity = record => record.city.startsWith(cityFilter);
        
          const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
          props.setFilteredItems(selectedRecords); //  <-- Danger! Updates Redux during a render!
        
          return <div>
            <input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
            <input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
          </div>
        };
        
        const mapStateToProps = state => ({
          records: state.stuff.items,
          filteredItems: state.stuff.filteredItems
        });
        const mapDispatchToProps = { setFilteredItems };
        export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
        

        当我使用 React 16.12.0 运行此代码时,我在浏览器控制台中收到了此线程主题中列出的警告。根据堆栈跟踪,有问题的行是我在渲染函数中的props.setFilteredItems 调用。所以我简单地将过滤器调用和状态更改包含在useEffect 中,如下所示。

        import {setFilteredItems} from './myActions';
        const myFilters = props => {
          const [nameFilter, setNameFilter] = useState('');
          const [cityFilter, setCityFilter] = useState('');
        
          useEffect(() => {
            const filterName = record => record.name.startsWith(nameFilter);
            const filterCity = record => record.city.startsWith(cityFilter);
        
            const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
            props.setFilteredItems(selectedRecords); //  <-- OK now; effect runs outside of render.
          }, [nameFilter, cityFilter]);
        
          return <div>
            <input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
            <input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
          </div>
        };
        
        const mapStateToProps = state => ({
          records: state.stuff.items,
          filteredItems: state.stuff.filteredItems
        });
        const mapDispatchToProps = { setFilteredItems };
        export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
        

        当我第一次添加useEffect 时,我将堆栈顶部从堆栈中炸开,因为每次调用useEffect 都会导致状态更改。我必须添加一组跳过效果,这样效果只会在过滤器字段本身发生变化时运行。

        【讨论】:

        • 我注意到我发布的解决方案存在一个小问题。当 Redux 存储的记录被异步更新时,useEffect 也会更新,但 props 会保持旧状态作为上一次渲染的闭包。渲染最终会发生,更新道具。但是“跳过效果”会阻止效果与这些更新的道具一起重新运行。我通过将 props.records.length 添加到我的跳过列表来解决此问题。
        【解决方案12】:

        我建议看下面的视频。正如 OP 问题中的警告所暗示的那样,由于另一个兄弟孩子(孩子 1)对父母的回调,父母(父母)试图过早更新一个孩子(孩子 2)的属性,因此存在变化检测问题。对我来说,Child 2 过早/错误地调用了传入的 Parent 回调,从而引发了警告。

        请注意,此通信工作流程只是一个选项。我个人更喜欢通过共享的 Redux 存储在组件之间交换和更新数据。然而,有时它是矫枉过正的。该视频提出了一个干净的替代方案,其中孩子们“愚蠢”并且只能通过道具和回调进行交流。

        另请注意,如果在 Child 1 的“事件”上调用回调,例如单击按钮,它将起作用,因为到那时,孩子已经更新。不需要超时、useEffects 等。UseState 足以满足这种狭窄的场景。

        这是链接(感谢 Masoud):

        https://www.youtube.com/watch?v=Qf68sssXPtM

        【讨论】:

          【解决方案13】:

          在 react native 中,如果您自己在代码中使用热重载更改状态,我发现会出现此错误,但使用按钮更改状态会使错误消失。

          但是将我的 useEffect 内容包装在一个 :

          setTimeout(() => {
              //....
          }, 0);
          

          即使是热重载也能工作,但我不想无缘无故地设置愚蠢的 setTimeout,所以我删除了它,发现通过代码更改它就可以了!

          【讨论】:

            【解决方案14】:

            我同时更新了多个子组件中的状态,这导致了意外行为。用 useRef 替换 useState 钩子对我有用。

            【讨论】:

              【解决方案15】:

              尝试使用setTimeout,当我在没有setTimeout的情况下调用props.showNotification时,出现这个错误,可能生命周期中的一切都运行在Time,UI无法更新。

              const showNotifyTimeout = setTimeout(() => {
                        this.props.showNotification();
                        clearTimeout(showNotifyTimeout);
                      }, 100);
              

              【讨论】:

              • 请将代码和数据添加为文本 (using code formatting),而不是图像。图片:A)不允许我们复制粘贴代码/错误/数据进行测试; B) 不允许根据代码/错误/数据内容进行搜索;和many more reasons。除了代码格式的文本之外,只有在图像添加了一些重要的东西,而不仅仅是文本代码/错误/数据传达的内容时,才应该使用图像。
              猜你喜欢
              • 2021-02-06
              • 2020-08-05
              • 1970-01-01
              • 1970-01-01
              • 2022-01-13
              • 1970-01-01
              • 1970-01-01
              • 2020-11-06
              • 2020-11-08
              相关资源
              最近更新 更多