【问题标题】:React Hooks - keep arguments reference in stateReact Hooks - 保持参数引用状态
【发布时间】:2021-09-05 16:46:16
【问题描述】:

我创建了一个钩子来使用确认对话框,这个钩子为组件提供属性以像这样使用它们:

const { setIsDialogOpen, dialogProps } = useConfirmDialog({
  title: "Are you sure you want to delete this group?",
  text: "This process is not reversible.",
  buttons: {
    confirm: {
      onPress: onDeleteGroup,
    },
  },
  width: "360px",
});
<ConfirmDialog {...dialogProps} />

这很好用,但我也想在需要时提供更改这些属性的选项,而无需在使用的组件中声明额外的状态,为了实现这一点,我所做的是将这些属性保存在内部的状态中钩子和这种方式提供了另一个函数来在显示对话框之前根据需要更改它们:

interface IState {
  isDialogOpen: boolean;
  dialogProps: TDialogProps;
}

export const useConfirmDialog = (props?: TDialogProps) => {
  const [state, setState] = useState<IState>({
    isDialogOpen: false,
    dialogProps: {
      ...props,
    },
  });

  const setIsDialogOpen = (isOpen = true) => {
    setState((prevState) => ({
      ...prevState,
      isDialogOpen: isOpen,
    }));
  };

  // Change dialog props optionally before showing it
  const showConfirmDialog = (dialogProps?: TDialogProps) => {
    if (dialogProps) {
      const updatedProps = { ...state.dialogProps, ...dialogProps };

      setState((prevState) => ({
        ...prevState,
        dialogProps: updatedProps,
      }));
    }
    setIsDialogOpen(true);
  };

  return {
    setIsDialogOpen,
    showConfirmDialog,
    dialogProps: {
      isOpen: state.isDialogOpen,
      onClose: () => setIsDialogOpen(false),
      ...state.dialogProps,
    },
  };
};

但是这里的问题是:
参数通过引用传递,因此如果我将函数传递给按钮(即 onDeleteGroup),我将保持函数更新到其最新状态,以便在其中的组 ID 发生更改时执行正确的删除。
但是当我将属性保存在一个状态中时,引用丢失了,现在我只有在开始时声明的状态的函数。 我尝试添加一个 useEffect 以在参数更改时更新钩子状态,但这会导致无限重新渲染:

useEffect(() => {
    setState((prevState) => ({
        ...prevState,
        dialogProps: props || {},
    }));
}, [props]);

我知道我可以调用 showConfirmDialog 并传递函数以使用最新的函数状态更新状态,但我正在寻找一种方法来调用钩子、声明道具并且在不需要时不触摸对话框道具. 欢迎任何答案,感谢您的阅读。

【问题讨论】:

    标签: reactjs typescript react-hooks arguments use-state


    【解决方案1】:

    你真的应该考虑不这样做,这不是一个好的编码模式,这不必要地使你的钩子复杂化并且可能导致难以调试的问题。这也违背了“单一事实来源”的原则。我的意思是像下面这样的情况

    const Component = ({title}: {title?: string}) => {
      const {showConfirmDialog} = useConfirmDialog({
        title,
        // ...
      })
    
      useEffect(() => {
        // Here you expect the title to be "title"
        if(something) showConfirmDialog()
      }, [])
      useEffect(() => {
        // Here you expect the title to be "Foo bar?"
        if(somethingElse) showConfirmDialog({title: 'Foo bar?'})
      }, [])
      // But if the second dialog is opened, then the first, the title will be 
      // "Foo bar?" in both cases
    }
    

    所以在实现之前请三思,有时最好多写一点代码,但它会为你节省很多调试时间。


    至于答案,我会将道具存储在 ref 中,并以某种方式在每次渲染时更新它们

    /** Assign properties from obj2 to obj1 that are not already equal */
    const assignChanged = <T extends Record<string, unknown>>(obj1: T, obj2: Partial<T>, deleteExcess = true): T => {
      if(obj1 === obj2) return obj1
      
      const result = {...obj1}
      Object.keys(obj2).forEach(key => {
        if(obj1[key] !== obj2[key]) {
          result[key] = obj2[key]
        }
      })
      if(deleteExcess) {
        // Remove properties that are not present on obj2 but present on obj1
        Object.keys(obj1).forEach(key => { 
          if(!obj2.hasOwnProperty(key)) delete result[key]
        })
      }
      return result
    }
    
    const useConfirmDialog = (props) => {
      const localProps = useRef(props)
      localProps.current = assignChanged(localProps.current, props)
    
      const showConfirmDialog = (changedProps?: Partial<TDialogProps>) => {
        localProps.current = assignChanged(localProps.current, changedProps, false)
        // ...
      }
      
      // ...
    }
    

    如果您在TDialogProps 中有一些可选属性,并且您希望在showConfirmDialog 中接受Partial 属性。如果不是这种情况,您可以通过删除此 deleteExcess 部分来稍微简化逻辑。

    您会发现它极大地使您的代码复杂化,并增加了性能开销(尽管它微不足道,考虑到您的对话道具中只有 4-5 个字段),所以我真的不建议这样做,只让 @ 的调用者987654327@ 有自己可以更改的状态。或者,也许您可​​以首先从useConfirmDialog 中删除道具,并强制用户始终将它们传递给showConfirmDialog,尽管在这种情况下,这个钩子变得有点没用。也许你根本不需要这个钩子,如果它只包含你在答案中实际显示的逻辑?似乎它唯一做的就是将isDialogOpen 设置为true/false。无论如何,这是你的选择,但我认为这不是最好的主意

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-11-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-29
      • 2019-08-08
      • 1970-01-01
      相关资源
      最近更新 更多