我正在使用应用程序中的通知功能(非常类似于Facebook通知)。
当我单击标题导航中的按钮时,下拉列表将打开并显示通知列表。通知中包含Link
(来自react-router
)。
我需要做的就是每当点击Link
时关闭下拉列表。
这里大致是我目前拥有的层次结构:
Header > Navigation > Button > Dropdown > List > Notification > Link
由于下拉功能的使用不止一次,因此我将其行为抽象为使用render prop的HOC:
export default function withDropDown(ClickableElement) {
return class ClickableDropdown extends PureComponent {
static propTypes = {
children: PropTypes.func.isRequired,
showOnInit: PropTypes.bool,
};
static defaultProps = {
showOnInit: false,
};
state = {
show: !!this.props.showOnInit,
};
domRef = createRef();
componentDidMount() {
document.addEventListener('mousedown', this.handleGlobalClick);
}
toggle = show => {
this.setState({ show });
};
handleClick = () => this.toggle(true);
handleGlobalClick = event => {
if (this.domRef.current && !this.domRef.current.contains(event.target)) {
this.toggle(false);
}
};
render() {
const { children, ...props } = this.props;
return (
<Fragment>
<ClickableElement {...props} onClick={this.handleClick} />
{this.state.show && children(this.domRef)}
</Fragment>
);
}
};
}
上面的HOC封闭了Button
组件,所以我有:
const ButtonWithDropdown = withDropdown(Button);
class NotificationsHeaderDropdown extends PureComponent {
static propTypes = {
data: PropTypes.arrayOf(notification),
load: PropTypes.func,
};
static defaultProps = {
data: [],
load: () => {},
};
componentDidMount() {
this.props.load();
}
renderDropdown = ref => (
<Dropdown ref={ref}>
{data.length > 0 && <List items={this.props.data} />}
{data.length === 0 && <EmptyList />}
</Dropdown>
);
render() {
return (
<ButtonWithDropdown count={this.props.data.length}>
{this.renderDropdown}
</ButtonWithDropdown>
);
}
}
List
和Notification
都是愚蠢的功能组件,因此我不在这里发布它们的代码。 Dropdown
几乎相同,只是使用的是ref forwarding。
我真正需要的是调用HOC创建的.toggle()
中的ClickableDropdown
方法,每当我单击列表中的Link
时都要调用该方法。
是否有任何方法可以将.toggle()
方法传递到Button > Dropdown > List > Notification > Link
子树下?
我正在使用redux
,但是我不确定这是我要放在商店里的东西。
还是我应该通过从handleGlobalClick
更改ClickableDropdown
的实现来使用DOM API来强制性地处理此问题?
我正在尝试命令式方法,因此我更改了handleGlobalClick
方法:
const DISMISS_KEY = 'dropdown';
function contains(current, element) {
if (!current) {
return false;
}
return current.contains(element);
}
function isDismisser(dismissKey, current, element) {
if (!element || !contains(current, element)) {
return false;
}
const shouldDismiss = element.dataset.dismiss === dismissKey;
return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}
// Then...
handleGlobalClick = event => {
const containsEventTarget = contains(this.domRef.current, event.target);
const shouldDismiss = isDismisser(
DISMISS_KEY,
this.domRef.current,
event.target
);
if (!containsEventTarget || shouldDismiss) {
this.toggle(false);
}
return true;
};
然后,我将Link
更改为包含data-dismiss
属性:
<Link
to={url}
data-dismiss="dropdown"
>
...
</Link>
现在该下拉列表已关闭,但我不再重定向到所提供的url
。
我尝试使用this.toggle(false)
和requestAnimationFrame
来推迟执行setTimeout
,但是它也不起作用。
根据@streletss bellow的回答,我提出了以下解决方案:
为了尽可能通用,我在shouldHideOnUpdate
下拉组件中创建了一个ClickableDropdown
道具,其Hindley-Milner-ish签名是:
shouldHideOnUpdate :: Props curr, Props prev => (curr, prev) -> Boolean
这是componentDidUpdate
的实现:
componentDidUpdate(prevProps) {
if (this.props.shouldHideOnUpdate(this.props, prevProps)) {
this.toggle(false);
}
}
这样,我不需要直接在withRouter
HOC中使用withDropdown
HOC。
因此,我取消了为隐藏向调用者隐藏下拉菜单的条件的职责,我的案例是Navigation
组件,在该组件中,我做了这样的事情:
const container = compose(withRouter, withDropdown);
const ButtonWithDropdown = container(Button);
function routeStateHasChanged(currentProps, prevProps) {
return currentProps.location.state !== prevProps.location.state;
}
// ... then
render() {
<ButtonWithDropdown shouldHideOnUpdate={routeStateHasChanged}>
{this.renderDropdown}
</ButtonWithDropdown>
}
答案 0 :(得分:1)
在问题中,您提到您正在使用redux。因此,我假设您将showOnInit存储在redux中。我们通常不将函数存储在redux中。在toggle函数中,我认为您应该调度CHANGE_SHOW操作来进行更改在redux中执行showOnInit,然后将show数据而不是函数传递给子组件。然后,在reducer调度后,react将自动更改“ show”。
switch (action.type) {
case CHANGE_SHOW:
return Object.assign({}, state, {
showOnInit: action.text
})
...
default:
return state
}
使用“链接到”属性,而不是“数据” ...中的属性。
<Link
to={{
pathname: url,
state:{dismiss:"dropdown"}
}}
/>
状态属性将在this.props.location中找到。
这可能会导致您的项目陷入不稳定以及其他一些问题。(https://reactjs.org/docs/context.html#classcontexttype)
首先,定义上下文
const MyContext = React.createContext(defaultValue);
第二次定义通过值
<MyContext.Provider value={this.toggle}>
然后,在嵌套组件中获取值
<div value={this.context} />
答案 1 :(得分:1)
似乎您可以简单地利用withRouter
HOC并检查this.props.location.pathname
时componentDidUpdate
是否已更改:
export default function withDropDown(ClickableElement) {
class ClickableDropdown extends Component {
// ...
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.toggle(false);
}
}
// ...
};
return withRouter(ClickableDropdown)
}