从没有引用的父级触发子方法



所以我知道这里有很多类似的问题,但所有的解决方案都涉及使用useRef,我宁愿不这样做。

我有以下情况(我已经简化了):

const Parent = () => {
return (
<div>
<button>Click</button>
<Child>
</div>
)
};

const Child = () => {
const doThing = () => {
console.log("I ran")
}
return (
<div></div>
)
};

事实上,我的父组件包含一个按钮,我的子组件包含文件夹,在这些文件夹中我有文件。我想通过单击父级中的按钮来折叠所有这些文件夹。(同样,根据组件的布局方式,将按钮移动到子项中是没有意义的)

为了在没有裁判的情况下实现这一点,我知道我可以做以下事情:

const Parent = () => {
const [wasTriggered, setWasTriggered] = useState(false)
const clickFunction = () => {
setWasTriggered(true)
}
const changeBackToFalse = () => {
setWasTriggered(false)
}
return (
<div>
<button onClick={clickFunction} >Click</button>
<Child wasTriggered = {wasTriggered} changeBackToFalse={changeBackToFalse}>
</div>
)
};

const Child = ({wasTriggered, changeBackToFalse}) => {
const doThing = () => {
console.log("I ran")
}
useEffect(()=>{
if (wasTriggered) {
doThing()
changeBackToFalse()
}
},[wasTriggered])
return (
<div></div>
)
};

但这很乏味,似乎我只是为了实现我想要的东西而来回传递。

我想要的是从父级触发子级内的方法的某种方式。如果这非常简单或不可能,我深表歉意,但由于我对React的了解有限,我不确定是哪一个,谢谢。

TLDR:如果有时间,重构Children组件并提升它们的状态,否则您的解决方案是可以的


详细信息

这感觉乏味,正是因为它违背了";React";做事的方式。建议您看到当前方法有问题:)

状态指示React中的所有内容。如果您无法访问状态进行修改,则必须将其抬起,以便进行修改。

我假设子文件夹文件组件包含某种状态。在使用Parent按钮进行更改之前,我们有一个"真理的单一来源";(组件的局部状态)。更改后,我们现在有两个状态需要保持同步(同步并不理想):

  • 标志必须指定子状态
  • 在children状态更改时,我们必须重置标志

我很抱歉,但唯一的"好">(从长远来看)的解决方案是控制子组件并处理父组件中的状态。

如果您负担不起重构Children组件的费用,那么您的解决方案就是次佳方案。

可能的解决方案包括

  1. 使用子组件的引用并从父组件调用所需的方法(理想解决方案)
  2. 发送您提到的道具(会很乏味)

这两种解决方案都适用于您的场景,并以React的方式工作。如果你不想使用这些解决方案,那么还有另一种解决方案并不是以React的方式,而是以普通的web应用程序的方式。我们可以使用窗口自定义事件

父组件将调度自定义事件,子组件将侦听该事件并对其执行操作

子组件

import { useEffect } from "react";
const Child = (props) => {
useEffect(() => {
document.addEventListener('trigger_child', (e) => doThing(e.detail)); //adding event listener when the component mounts. When dispatch is called, the doThing method gets called.
return () => {
document.removeEventListener('trigger_child', doThing);
}; //removing the listener when the component unmounts.
}, []);
const doThing = (data) => {
console.log(data); //process using the data sent by the parent component.
};
return (
<div>
Child
</div>
);
};
export default Child;

父组件

const Parent = (props) => {
const handleButtonClick = () => {
document.dispatchEvent(new CustomEvent('on_trigger_child', { detail: {  } })); //dispatch an event on button click. Send optional data along with the event. Child component will receive the data and process it.
};
return (
<div>
<button onClick={handleButtonClick}>Trigger</button>
<p>Parent</p>
</div>
);
};
export default Parent;

注意:您可以为文档访问方法编写一个包装器,并将这些包装器方法公开给您的组件

链接:

https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListenerhttps://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListenerhttps://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEventhttps://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent

我找到的一个解决方案(SO答案)是更改组件的key属性。useEffect()仅在组件加载时调用,在更改key属性之前,从Parent更改的任何动态都不会反映在Child组件中。

沙盒示例

Parent.js

import { useState } from "react";
import Child from "./Child";
const Parent = () => {
const [wasTriggered, setWasTriggered] = useState(false);
return (
<div>
<button onClick={() => setWasTriggered(false)}>Set FALSE</button>
<Child wasTriggered={wasTriggered} />
<Child wasTriggered={wasTriggered} />
<Child wasTriggered={wasTriggered} />
<button onClick={() => setWasTriggered(true)}>Set TRUE</button>
</div>
);
};
export default Parent;

Child.js

import { useEffect } from "react";
const Child = (props) => {
const doThing = () => {
console.log("I ran");
};
useEffect(() => {
doThing();
}, []);
return (
<div
key={props.wasTriggered}
style={{ color: props.wasTriggered ? "red" : "green" }}
>
Child
</div>
);
};
export default Child;

从父方法调用子方法有一个解决方案,可以实现我想要的一切。

@LukášNovák给出了我最满意的答案(其他一些人提出了这个建议,但这是我第一次看到)

可以将函数保存在useStates中。

所以我可以做以下事情:

母组件

const Parent = () => {
const [theSavedFunction, setTheSavedFunction] = useState()
return (
<div>
<button onClick={theSavedFunction} >Click</button>
<Child savedFunction={(f) => {setTheSavedFunction(f)}}>
</div>
)
};
.

子组件

const Child = ({savedFunction}) => {
const doThing = () => {
console.log("I ran")
}
useEffect(() => {
savedFunction(() => doThing);
}, []);

return (
<div></div>
)
};

现在,当我点击按钮时,它会触发doThing功能。(如果doThing包含可能更改的道具,则需要在useEffect中添加它们)

可以通过更改将变量传递给函数

savedFunction(() => doThing);

savedFunction(() => (thing) => doThing(thing));

如果有人知道为什么没有建议这样做,或者这是一种糟糕的做法,请告诉我。

您可以使用上下文来解决该问题。

核心概念是创建一个事件发射器或可订阅的东西,并通过React提供管理器。Context和Child组件订阅该管理器。

注意:我的例子都是用Typescript写的。

import React, { createContext, useCallback, useContext, useEffect, useMemo } from 'react';
interface Emitter {
emit: (type: string) => void;
// returns unsubscribe fn
on: (type: string, handler: () => void) => () => void;
}
const Context = createContext<Emitter>({} as any);
const Parent = () => {
const emitter = useMemo<Emitter>(() => {
// implement emitter here. Or just use https://github.com/primus/eventemitter3
return {} as any;
}, []);
return (
<Context.Provider value={emitter}>
<button onClick={() => emitter.emit('trigger')}>trigger</button>
<Child />
</Context.Provider>
);
};
const Child = () => {
const ctx = useContext(Context);
const doThing = useCallback(() => {}, []);
useEffect(() => {
return ctx.on('trigger', doThing);
}, [doThing, ctx]);
return null;
};

但是:

  1. 我强烈建议你不要做";触发从父到子的事件";,相反,我建议您将逻辑从孩子提升到家长。(原因与为什么除非必要,否则应该避免使用ImperativeHandle相同,你可以很容易地在任何地方找到答案)
  2. 根据官方文档的说法,我不建议使用useMemo来保存实例,因为react可能会忘记已经记住的内容https://reactjs.org/docs/hooks-faq.html#how-以存储计算

如果你真的想创建一个";实例";在组件中,试试这个:

export function useFactory<T>(factory: () => T, deps: any[] = []): T {
const prevDepsRef = useRef<any[]>(null as any);
const valueRef = useRef<T>(null as any);
const depsChanged = prevDepsRef.current === null || isDepsChanged(prevDepsRef.current, deps);
if (depsChanged) {
valueRef.current = factory();
}
prevDepsRef.current = deps;
return valueRef.current;
}
function isDepsChanged(a: any[] = [], b: any[] = []): boolean {
if (!a || !b) {
return true;
}
if (a.length !== b.length) {
return true;
}
if (a.length === 0 && b.length === 0) {
return false;
}
let [left, right] = [a, b];
if (left.length < right.length) {
[left, right] = [right, left];
}
return left.some((item, idx) => item !== right[idx]);
}