React.useState 不会从 props 重新加载状态。

243

我期望当props改变时状态重新加载,但是这不起作用,并且在下一个useState调用中user变量没有更新,出了什么问题?

function Avatar(props) {
  const [user, setUser] = React.useState({...props.user});
  return user.avatar ? 
         (<img src={user.avatar}/>)
        : (<p>Loading...</p>);
}

codepen


2
这是组件中的全部代码还是为了简单起见而缩短了?就目前而言,使用中间状态而不是简单地使用 props.user 没有任何意义。 - tokland
7个回答

437

useState接收到的参数是初始状态,就像在类组件的构造函数中设置状态一样,并且不用于重新渲染时更新状态。

如果您想在属性更改时更新状态,请使用useEffect钩子。

function Avatar(props) {
  const [user, setUser] = React.useState({...props.user});

  React.useEffect(() => {
      setUser(props.user);
  }, [props.user])

  return user.avatar ? 
         (<img src={user.avatar}/>)
        : (<p>Loading...</p>);
}

演示


62
在构造函数中设置初始状态时,很容易理解构造函数只在类实例创建时调用一次。使用hooks时,似乎每次函数调用时都会调用useState并且每次都应该初始化状态,但是有一些未经解释的"魔法"发生了。 - vitalyster
4
这将导致初始状态被设置两次,相当烦人。我想知道是否可以将初始状态设置为null,然后使用React.useEffect在每次设置初始状态。 - CMCDragonkai
9
如果状态也控制渲染,则此方法无效。我有一个基于属性和状态进行渲染的函数组件。如果状态告诉组件因为已经发生改变而进行渲染,那么 useEffect 就会运行并将状态设置回默认值。这是他们设计中非常糟糕的部分。 - Greg Veres
1
我曾经遇到了@GregVeres提到的同样情况 - 我想创建一个乐观的开关按钮。应该很简单,但是由于新的hooks,React 16.3已经弃用了我们以前可以用干净的方式实现这个功能的方法。解决方案是一个记录在案的黑客:通过将组件的key与内部任何即将更改的内容链接起来,强制React重新创建您的组件。这样,当相关的props发生变化时,您就可以保证重置状态!不确定是笑还是哭。 - igorsantos07
2
@igorsantos07 只有在 props.user 改变时,状态才会回退到 props 值的更改,否则不会。 - Shubham Khatri
显示剩余9条评论

71
我看到几乎所有回答这个问题的答案都在推广一种不好的模式:在 useEffect 调用中作为 prop 改变的结果而更新状态。useEffect 钩子函数用于将 React 组件与外部系统同步。将其用于同步 React 状态可能会导致错误(因为其他效果引起的重新渲染可能会导致意外的状态更新)。更好的解决方案是通过在父组件中改变 组件的 key 属性来触发一次协调。
// App.jsx
function App() {
   // ...logic here
   return <Avatar initialUser={user} key={user.id} />
}

// Avatar.jsx
function Avatar({ initialUser }) {
 // I suppose you need this component to manage it's own state 
 // otherwise you can get rid of this useState altogether.
  const [user, setUser] = React.useState(initialUser);
  return user.avatar ? (
    <img src={user.avatar} />
  ) : (
    <p>Loading...</p>
  );
}

你可以把这种情况下的key属性看作是useEffect的依赖数组,但是由于组件渲染触发的意外useEffect调用不会导致意外的状态改变。
你可以在这里阅读更多相关信息: 将Props转为State 关于useEffect可能成为一个隐患的更多信息,请参考以下链接: 也许你并不需要Effect

7
谢谢!“You Might Not Need an Effect”这篇文章真是一篇珍品! - jlh
点赞。如果您有一个特定的子键,这种方法很好。我尝试启用Chrome Paint Flash,注意到整个子元素都有绿色矩形,而不仅仅是更改的元素。我觉得还是回到useEffect比较好。 - kiranpradeep
如果您已经测量了性能,并且它极大地影响了呈现行为,那么您可以考虑选择一些优化,例如通过React.memo进行备忘(还有“有条件呈现”或“提升状态”,如“将props放入State”文章中所述)。我不确定为了轻微的性能优势而交易可能的UI不一致是否是一个好的权衡。我发现useEffect是大多数时候违反幂等性的地方,因此我觉得这股“尽量避免使用useEffect”的浪潮是正确的前进道路。 - Alejandro Rodriguez P.
1
谢谢!帮助我解决了我已经困扰了几个小时的问题。 - Reina Reinhart
@AlejandroRodriguezP。如果我们依赖的属性是一个对象数组,你会如何处理?对于我来说,我正在使用一个期望行数组的AG Grid组件。如果我不更新行,它不会更新界面。所以如果我想要更新网格的行并从父组件获取新的行作为属性,我必须使用useEffect。 - Shailesh Vaishampayan
显示剩余2条评论

21

函数式组件中,我们使用useState来设置变量的初始值。如果我们通过props传递初始值,它将始终设置相同的初始值,直到您使用useEffect钩子。

例如,在您的情况下,这将完成您的工作。

 React.useEffect(() => {
      setUser(props.user);
  }, [props.user])

useEffect传递的函数将在呈现提交到屏幕后运行。

默认情况下,effect会在每次完成呈现后运行,但是您可以选择仅在某些值更改时触发它们。

React.useEffect(FunctionYouWantToRunAfterEveryRender)

如果您只将一个参数传递给 useEffect,它将在每次渲染后运行此方法。 您可以通过向 useEffect 传递第二个参数来决定何时触发这个 FunctionYouWantToRunAfterEveryRender

React.useEffect(FunctionYouWantToRunAfterEveryRender, [props.user])

请注意,现在我正在传递 [props.user]。使用 useEffect 时,只有在 props.user 发生变化时才会触发 FunctionYouWantToRunAfterEveryRender 函数。

希望这有助于您的理解。如果需要改进,请告诉我。谢谢。


9
你可以为此创建自己的定制简单钩子。当它们改变时,这些钩子的默认值也会随之改变。

https://gist.github.com/mahmut-gundogdu/193ad830be31807ee4e232a05aeec1d8

    import {useEffect, useState} from 'react';

    export function useStateWithDep(defaultValue: any) {
      const [value, setValue] = useState(defaultValue);
    
      useEffect(() => {
        setValue(defaultValue);
      }, [defaultValue]);
      return [value, setValue];
    }

#示例

const [user, setUser] = useStateWithDep(props.user);

1
让我们改进类型:function useStateWithDep<T>(defaultValue: T)return [value, setValue] as const - tokland
并不是很有用,因为 useEffect 回调函数会在 return 语句之后被调用。所以一开始它会返回旧值。 - Sumit Wadhwa

4

我创建了像这样的自定义钩子:

const usePrevious = value => {
   const ref = React.useRef();

   React.useEffect(() => {
       ref.current = value;
   }, [value]);

   return ref.current;
}

const usePropState = datas => {
    const [dataset, setDataset] = useState(datas);
    const prevDatas = usePrevious(datas);

    const handleChangeDataset = data => setDataset(data);

    React.useEffect(() => {
        if (!deepEqual(datas, prevDatas)) // deepEqual is my function to compare object/array using deep-equal
            setDataset(datas);
    }, [datas, prevDatas]);

    return [
        dataset,
        handleChangeDataset
    ]
}

使用方法:

const [state, setState] = usePropState(props.datas);

1
为什么您要创建一个不稳定的(而不是使用useCallback)handleChangeDataset,而不是返回稳定的setDataset呢? - philk

2
React.useState()中传递的参数仅为该状态的初始值。React不会将其视为更改状态,而只是设置其默认值。您需要最初设置默认状态,然后有条件地调用setUser(newValue),这将被识别为新状态并重新呈现组件。
我建议在没有某种条件来防止其不断更新(因此每次接收到props时都会重新呈现)的情况下小心更新状态。您可能希望考虑将状态功能提升到父组件并将父组件的状态作为属性传递到此Avatar中。

-4
根据ReactJS Hooks文档
但是,如果在组件仍在屏幕上时更改了friend prop会发生什么?我们的组件将继续显示不同朋友的在线状态。这是一个错误。当卸载时,我们还会导致内存泄漏或崩溃,因为取消订阅调用将使用错误的friend ID。
您在此处唯一的交互应该发生在props更改时,但似乎不起作用。您可以(仍然根据文档)使用componentDidUpdate(prevProps)主动捕获任何props更新。
PS:我没有足够的代码来判断,但您不能在Avatar(props)函数内部积极地setUser()吗?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接