React Hooks - 保持参数引用处于状态
React Hooks - keep arguments reference in state
我创建了一个挂钩来使用确认对话框,这个挂钩为组件提供属性以像这样使用它们:
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 并传递函数以使用最新的函数状态更新状态,但我正在寻找一种方法来调用挂钩、声明道具而不触及对话框道具(如果不是)不需要。
欢迎大家回答,谢谢阅读
你真的应该考虑不这样做,这不是一个好的编码模式,这会使你的钩子不必要地复杂化,并可能导致难以调试的问题。这也违背了“单一事实来源”的原则。我的意思是像下面这样的情况
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 个字段),所以我真的建议不要这样做,而只是让调用者useConfirmDialog
有自己可以改变的状态。或者你可以首先从 useConfirmDialog
中删除 props 并强制用户始终将它们传递给 showConfirmDialog
,尽管在这种情况下这个钩子变得有点无用了。也许你根本不需要这个钩子,如果它只包含你在答案中实际显示的逻辑?看起来它所做的唯一一件事就是将 isDialogOpen
设置为 true
/false
。不管怎样,这是你的选择,但我认为这不是最好的主意
我创建了一个挂钩来使用确认对话框,这个挂钩为组件提供属性以像这样使用它们:
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 并传递函数以使用最新的函数状态更新状态,但我正在寻找一种方法来调用挂钩、声明道具而不触及对话框道具(如果不是)不需要。 欢迎大家回答,谢谢阅读
你真的应该考虑不这样做,这不是一个好的编码模式,这会使你的钩子不必要地复杂化,并可能导致难以调试的问题。这也违背了“单一事实来源”的原则。我的意思是像下面这样的情况
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 个字段),所以我真的建议不要这样做,而只是让调用者useConfirmDialog
有自己可以改变的状态。或者你可以首先从 useConfirmDialog
中删除 props 并强制用户始终将它们传递给 showConfirmDialog
,尽管在这种情况下这个钩子变得有点无用了。也许你根本不需要这个钩子,如果它只包含你在答案中实际显示的逻辑?看起来它所做的唯一一件事就是将 isDialogOpen
设置为 true
/false
。不管怎样,这是你的选择,但我认为这不是最好的主意