react 这种组件化设计框架,怎么能拿到子组件的值呢?

248 天前
 hahaFck

最近在学 react,遇到一个问题请教一下,假如组件都是用 function 定义的,如果页面的结构是这样的:

我想在 toolbar 中的一个 button 点击事件中获取 Grid 控件的值,该用什么方法实现呢。 按照以往非组件化的思路应该是直接获取到这个 grid 对象,在调用对应的 getValue 方法就可以了。

虽然组件支持传递回调函数,在 grid 内部,state 变化时调用回调函数,但是这样的话回调函数就要在 page 里面从 layout 传到 grid ,感觉这种思路太不符合直觉了,因为 layout 和 some 组件根本就不应改有 callback 的 prop ,如果 some 下面有 5 个组件,那就要传递 5 个 callback ,太可怕了吧,并且多加一个组件就要去修改 some 的代码也是不对的。

后段 coder 最近在学前端,没用 react 做过项目,不知道遇到这种情况改怎么解决,有什么好的方法或者 lib 能够优雅的解决这个问题么。

4878 次点击
所在节点    React
60 条回复
ZGame
247 天前
是我的话我可能会提 5 个 gridRef 出来, gridRef.getRow?.[0],比较符合直觉
RRRSSS
247 天前
一定不要一层一层传,根本难以维护,更没有意义,这么多层写出来的一定是冗余代码。

想要在 Grid 和 Page 之间状态共享,使用 zustand / jotai / redux 这种全局状态库就行了,写出来代码都差不多(这里以 jotai 为例):

// 定义 atom
export const dataAtom = atom('')

// Grid ,想要使用 `data` 或 设置 `data` 值都可以
const [data, setDataA] = useAtom(dataAtom)

// 同理在 Page 也是一样的:
const [data, setDataA] = useAtom(dataAtom)

这样写,省去了一层一层的 props ,代码简单多了。更重要的是:你只要知道你在组件里需要什么 atom ,然后使用就行了,没有心智负担。hook 本来就是干这事的。

另外,这样的需求,不建议使用 context 。实际上,我在任何情况下都不建议别人使用 context 来做业务代码,context 最常见的是场景其实是封装组件。
orzorzorzorz
247 天前
@nzbin 学到了啊。我对 service 的刻板印象是处理外部数据的,配合 rxjs 控制对外状态确实骚到我了。
ivslyyy
247 天前
自定义一个 hooks ,在其中用 useState ,
可以在其他组件中使用 hook ,
madao199
247 天前
可以用 ref useimperativehandle 把组件的状态暴露出来 向下渗透 ref 也能拿到值
superedlimited
247 天前
react 是种哲学,学习 declarative ui ,一定要先摒弃 imperative ui 的 get value 、set text 等等思想。数据在 react 中是单向流动的,就像水一样,如果上游发生了变化,那么自然而然,下流必然发生变化。
jinliming2
247 天前
子组件获取祖先组件的数据,这个逻辑没有问题,props/context 往下传都行。子组件存在的前提肯定是所有祖先组件都挂载存在了。
但是祖先组件获取子组件的数据、兄弟组件之间获取数据,这个有一个问题是,你要获取数据的那个组件可能不存在。
而你现在这个问题,综合了获取兄弟组件、子组件数据的情况。
你提到了“按照以往非组件化的思路应该是直接获取到这个 grid 对象,在调用对应的 getValue 方法就可以了”,这个在 React 中对应的就是 ref ,ref 上暴露 getValue 方法就是你说的这个了。函数组件没有对象实例可以用 React 提供的 forwardRef 。但是你也得要考虑一个事情是,这个实例引用变量存放在哪里,怎么去获取。因为这个 ref 的持有人默认只有挂载这个子组件的那个父组件,你又得要想办法把这个 ref 传给祖先、兄弟。
所以,这个 ref 的最佳存放位置就是共同祖先上,然后通过 context 往下传。但既然到了共同祖先这一步,那么就不要存 ref 了嘛,直接存数据就好?这就是楼上提到的数据存在共同祖先上,然后下面用 context 来读写数据。
祖先上不管是存数据还是存 ref 都是有自己的实际场景的。如果要调用对应组件的 API ,就还是得存 ref 。
然后另一个方案,全局状态,这个实际上也是把数据存在祖先上,只不过是存在根祖先上,子孙组件通过封装过的 context 读写数据。

不管是用全局状态还是自己写 context ,本质上都是数据存在祖先上,你在读取的时候不需要关心目标组件是否已经挂载存在,没挂载存在的话,你读到的就是个默认值。

或者楼上也有提到全局的通知广播,但这个一旦滥用就不好控制了。React 18 里有个 API ,useSyncExternalStore ,实际上也可以实现跨组件的共享,因为本质上数据是脱离 React 存在的,一个 store 实例,一个组件更新,一个组件监听,相当于一个小型的受限的广播系统,会比全局的广播好一些。
hahaFck
247 天前
@superedlimited
@jinliming2
是的,我的思路还是停留在以前那种对象的写法,就是组件自己提供 api 给外面调用,而 react 的思想是通过输入(prop)来产生输出,数据应该是通过父组件来传过来的。
hahaFck
247 天前
@RRRSSS 看了你提供的代码我有几点疑问,因为我没用前端工程化方式做过项目,还是以前那种 jquery 的开发经验,所以我想问:
1. 对象作用域
// 定义 atom
export const dataAtom = atom('')
这个应该是在单独一个文件定义的对象并导出,然后我在 page 和 grid 等不同的文件导入的时候,应该导入的是同一个对象吧?是不是可以理解 dataAtom 是一个类似于定义在 window 对象的单例,在整个浏览器页面访问他都是同一个对象。

2.grid 共享数据
Grid 组件我是想通过传递一个 URL 和 List<Column>给 Grid 组件,同时 grid 的 list 数据是通过你提供的那个方法( const [data, setDataA] = useAtom(dataAtom))定义的 data 变量,组件在挂载后通过 useEffect 函数,请求 url 来获取 data(List 数据),并通过 setDataA 来更新 grid 的 data 变量,组件重新渲染,那么在调用 setDataA 的时候,Page 组件也会更新么?也会重新渲染么,直观理解应该是只有 grid 重新渲染,展示列表数据。

3.Page 获取数据
page 里面通过 const [data, setDataA] = useAtom(dataAtom),拿到的这个 data 应该也是 grid 里面的 data ,是不是可以理解无论在哪个文件调用[data, setDataA] = useAtom(dataAtom)这段代码时,拿到都是同一个 data 和 setDataA 。
monster1priest
247 天前
1. useContext 共享全局变量
2. useImperativeHandle 层层上传,获取 Grid 对象
3. callback 层层向下传

React 由于历史包袱的原因,在一些语言设计上是有问题的,一般会更推荐使用现代状态管理库,比如 Redux ,Jotai 等等。

初学前端的话,我推荐你使用第一种 useContext ,先试一试,推荐去找个教程看看,可以把 Context 部分抽象成一个 Provider 组件。
Lesenelir
247 天前
@hahaFck 我帮他回答吧。

1. 是的。用一个单独以 .ts 结尾的文件定义 atoms ,每一个 atom 本质都是一个对象。如果你组件树只有一个顶层的 Provider ,我觉得是可以理解为全局的单例。

2. 首先在 useEffect 去通过 url 来请求数据本身就不是一个很好的 pattern 。其次,你最后的理解是错的。如果你在 Grid 组件中 使用了 useAtom(dataAtom) 后,请求并更新了数据,Page 是否更新取决于 「 Page 组件是否使用了你当前的 atom ,即 Page 组件中是否有 useAtom(dataAtom) useAtomValue(dataAtom) useSetAtom(dataAtom) 中的任意一个」。如果 Page 组件中使用了你的 atom ,则会触发你 Page 组件的 re-render ,而又因为你的 Page 组件在顶层,所以你 Page 组件下的所有子组件都会 re-render 。但如果你的 Page 组件中没有使用这个 atom ,那就不会触发 Page 组件 re-render 。具体的原因你可以看: https://jotai.org/docs/guides/core-internals#first-version

3. useAtom 本质是一个自定义 hooks ,它内部有 useState 进行处理。你可以理解为 data 是 grid 里的 data ,但也只是值是相同的,因为它其实是属于组件内的 state 。你要知道共享的只是 状态值,但状态是没有共享的,还是由组件内部维护的。

如果你对 jotai 感兴趣,可以我看的一篇博客: https://lesenelir.me/posts/jotai
darkengine
247 天前
@RRRSSS 我们的项目里用到 Context 的场景是搞了个 AccountContext 存放当前登录账号的数据
Lesenelir
247 天前
@hahaFck

OP 您自己上面一个的回答,还是有些问题。react 作为一个 ui 库,如果说输出的是 ui 的话,它的思想并不是通过 prop 来产生输出的,而是通过 state 来映射出一个 ui 。state 并不等于 data 数据,state > data 。state 其实和用户交互息息相关,你写多就会发现了,您说的数据只是 state 的一个小分支。
darkengine
247 天前
@Lesenelir 首先在 useEffect 去通过 url 来请求数据本身就不是一个很好的 pattern
-----
想请教下为啥这样做不合理,我的项目里基本上都是 useEffect 里拉接口 。。。
Leviathann
246 天前
@darkengine

分情况把 这个场景就是可用的 https://react.dev/learn/you-might-not-need-an-effect#fetching-data

useEffect 的心智模型是 Synchronize react component 与 external system
hahaFck
246 天前
@Lesenelir 你的回答我看了,如果在 Page 里面调用同一个 atom ,能够拿到 grid 的数据,但是 grid 在 re-render 时候也会导致 page 整个组件的 re-render ,那比较好奇的是刷新是谁导致的,是 grid 在调用 setatom 的时候导致了 page 的 re-render ,那么 page 在 re-render 时不会再一次导致 grid re-render 吧,这样 grid 就导致了 2 次 render
hahaFck
246 天前
@Lesenelir 我在看看你的博客理解一下
shunia
246 天前
这图都画的这么详细了,一眼看过去就应该知道接下来只能再在外面加一个数据层来进行数据管理啊?毕竟组件和组件之间的关系已经是完备的了。

用 context 确实会限制数据只能在 react 组件之间流转,但是你也没意识到一个问题,当你选择了用 react 去实现一个组件,它就已经不再是一个”通用“组件了,它本身就只能在 react 框架里流转。

如果你能把 Grid 实现为一个独立的组件,那你就必然要为它设计 API 用来向外传递数据,也就意味着你使用 Grid 的地方一定要能捕获 Grid 的输出并且把它集成到你的 App 的数据流里(如果你需要的话),也就意味着你的 App 必须要有一个数据层。此时使用 context 是完全合理的。

如果你还是觉得生理不适,必须要使用一个无依赖的数据层,react 生态里很多这种东西,比如 zustand ,但是我觉得增加的额外心智负担其实不如使用 context 。

最后再说一下那个鼓吹 Angular 的,包括说 service+rxjs 的,其实和 context 是一个性质的东西甚至更为不如。react 好歹数据层的生命周期管理的让人毫无心智负担,rxjs 则难以管理而且还要显式的销毁,否则会引发内存泄漏。service+rxjs 又完全不独立,和代码是紧密关联的,也并不符合 OP 的要求。就试问一下使用 Angular 实现和使用 React 实现,代码路径有什么区别?独立一个状态管理类,所有组件从中直接或者间接进行引用,还能有其他方法?而且早期的 Angular 没有 Inject 注解,甚至要在构造函数里传递 service 实例(至今官方示例代码依然优先采用这种写法),一旦遇到需要继承的情况就构造函数爆炸,蠢的很。而且直到最新的 standalone 组件之前,所有组件和 service 之类的都要显式的声明依赖和引用关系,傻的爆炸。组件编译出来默认 Shadow Dom alike 无法直接外部干涉,还需要每个组件显式声明不采用 Shadow Dom ,无语到让人喊娘。

最最后再提一下,脱藕 html/dom 和脱藕 React 组件不是一码事,不要想着希望自己最终产生的 html/dom 是脱藕的,就期盼着 JS/TS 代码内部也完全脱藕,不存在这种可能性呀,毕竟你的 App 内部总是有关联的,试论哪种解构方式都无法脱离耦合,只是高低的区别而已。
RRRSSS
245 天前
@hahaFck 我觉得在写 React 不用太考虑 re-render 问题,遇到了再解决就好。

和后端一样,在没有遇到性能问题的时候,先别考虑加缓存什么的。
hahaFck
245 天前
@RRRSSS 嗯嗯,是的,考虑的太多就把事情弄复杂了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1032409

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX