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

我想在 toolbar 中的一个 button 点击事件中获取 Grid 控件的值,该用什么方法实现呢。
按照以往非组件化的思路应该是直接获取到这个 grid 对象,在调用对应的 getValue 方法就可以了。
虽然组件支持传递回调函数,在 grid 内部,state 变化时调用回调函数,但是这样的话回调函数就要在 page 里面从 layout 传到 grid ,感觉这种思路太不符合直觉了,因为 layout 和 some 组件根本就不应改有 callback 的 prop ,如果 some 下面有 5 个组件,那就要传递 5 个 callback ,太可怕了吧,并且多加一个组件就要去修改 some 的代码也是不对的。
后段 coder 最近在学前端,没用 react 做过项目,不知道遇到这种情况改怎么解决,有什么好的方法或者 lib 能够优雅的解决这个问题么。

可以把 Grid 的 state 提升到 Page 中,或者直接在 Page 上包裹一个 Context.Provider ,用 context 管理状态

另一种自用的办法是在 Toolbar 这个组件里的 useEffect([])里,挂一个事件监听://定义通信格式interface IToolbarDataChangeEvent{msg:string}//处理函数const handleDatachange = (detail: IToolbarDataChangeEvent) = > {console.log(detail.msg)}//注册事件document.addEventListener("toolar::handleDataChange", handleDataChange)//本组件卸载时的清除return () => {document.removeEventListener("toolar::handleDataChange", handleDataChange)}然后 Grid 更新数据的时候发一个事件过去:const event = new CustomEvent("toolar::handleDataChange", {detail: {msg: "a msg from grid"}});document.dispatchEvent(event)

来自野路子前端,不确定是否是推荐的做法(

这种情况一般用 Context

要学会用 gpt我的话会直接用状态管理库Provide 给 Grid 控件去赋值,属于狗屎代码

能处理的方法很多。比如把多个 callback 封装成一个,使用 children 传递子控件,使用 context/provider 。但是我想说,才 5 个 callback 有什么可怕的?搞后端没这么脆弱。

直接上状态库,zustand ,早晚会上的

用 context 跨级别传

如果用 context 的话,useContext 的代码就要硬编码到 grid 组件中,这样这个组件也不通用了吧,放到另外页面的 context 里面又要重新写一份。

这是 react 特别的地方,你说的对 react 组件是基于一般属性和回调属性的单向数据流,所有的子组件都可以由父组件自上而下赋值参数来控制,组件间也是提倡组合大于继承,不同的组件间传参共享数据是通过公共父类组件居中调度,这一点在某些清新下确实会不太方便,但是也在一定程度上避免了组件间的相互修改,而是给它什么就是什么显得很确定。若是例子中的情形也是可以通过设定属性的,react 是可以透传属性的,跨组件传递数据流确实会有一些不太方便就是,大应用可以用集中的数据管理作为公共的数据源获得其中的状态,然后转化为普通组件的属性

是的,照文档看且不考虑状态管理,你只能一层层往上传。context 自己用还行,人多了就很乱。歪个楼。19 年那会我自己要开个项目,rva 选了半天,最后选了 a 。从结果看,只能说 angular 这种填空式开发是真的是在三年后等你,不太会有像 op 主楼里这样的心智负担。

使用状态管理库最简单

使用状态库,虽然项目早期时可能你会觉得烦琐,但是越往后越多需要共享状态的业务

这就是典型的组件一对多的模式,要靠一个中间人来完成传递。这个中间人肯定是这几个组件的上一级。前面几层楼推荐的 context ,状态管理库,都是这么个中间人。你定义 customEvent ,在 windows 上 dispatch ,那也叫中间人,也能解决你的问题。你随便写就好了,不要想着非得符合“react”的路子才行。

context 、状态库、或者 Grid 直接写 localStorage(笑

这是 explicity 的体现,不然你怎么知道这个组件有一个可以被捕获的事件组件和一个 context 的配合一起使用也是一个比较常见的 pattern

状态上移,我会把所有数据和点击的响应函数都放在 Page 里,Toolbar 只负责通知 Page 某个 Button 被点击了,Layout, Some, Grid 只负责根据数据展示。

状态上移+1 同时使用 children 的方法摊平组件 <Toolbar/>

那么,请问 angular 是如何处理呢🤔

最简单的方法就是把状态提升到需要的最上层,不过这样在组件层次过多的情况下既不利于维护,也会存在性能问题,尤其是中间传递某些组件不需要用到这个状态。然后就是用 Context API ,因为不推荐所以详情建议去看 React 官方文档。然后是使用状态管理库,把这种多个组件使用的状态交由状态管理库管理,算是最合适的处理了,喜欢稳定的就是 redux 和 mobx ,喜欢更现代的就看看 recoil ,zustand ,jotai

#2 不错,这个思路也挺好

1 楼和 2 楼都可以1 楼的又可以分为:context 和 state management library

有这个 API useImperativeHandle 相当于 vue 的 ref

我只知道实例化调用。不过实际上就算在 react 里,我也没碰见过层级那么深的组件。真有 op 这个场景,那是要考虑组件抽象是不是出问题了。

React 的官方文档看一遍吧,两种方法,一种是传递一个函数到 grid 组件里去,另一种是把状态提到 page 组件。

发送事件就是一对多了,楼主的例子更符合一对一的情况

1 、通过一层层的回调2 、通过状态管理库或者 context3 、通过自定义事件,在 toolbar 发送事件,在 Grid 监听事件( mebtte.com/split_react_state_by_event )

推荐做法是把状态提到上层,毕竟你要在上层用,说明这个状态应该属于上层。如果跨多层或者分散导致不方便一层层传递的话可以使用 Provider 之类的东西,但这不改变你把状态提升到上层的事实。

如果这值不用即时渲染到页面上,就直接写一个缓存对象;要即时渲染就上状态管理

如果 toolbar 需要用到 grid 里的值,那这个值大概率不该属于 grid

首先是上面各位大佬说过的问题。这个 state 如果 grid 组件并不需要为其生产数据,而只是消费数据的话,那可以把 state 提升到上层去生产他的组件里面。可以用 Provider+useContext 的形式跨级传入,也可以用 props 逐级传入。当然你用一些全局的状态管理也不是不行。如果这个值不涉及到渲染,也可以把这个值定义为 ref ,然后给 grid 组件使用 forwardRef 高阶函数包裹,同时使用 useImperativeHandle 把数值/函数暴露给父级,父级通过 ref.current.数值的形式获取。最主要的还是看这个数值的作用和来源是什么。

大家只说了解决方案,但这个需求是否是合理的还得看具体场景,很多时候是设计问题,这个状态只需要 grid 自持就行。

jutai, 跨组件的 useState

状态提升,入门教程不是都有吗

在 angular 中最简单的方式就是通过 service 共享状态,本质就是 DI + rxjs

easy-peasy.dev/ 如果会 Vue ,直接使用这个组件库

如果只是父子的话,父组件用 useReducer ,把 dispatch 传给子组件

#37 像题目这种跨多级的,在最近一个公共祖先节点用 context 维护状态,然后向下分发

全局状态用 Jotai 库管理即可

很多人说到了,状态提升。“我想在 toolbar 中的一个 button 点击事件中获取 Grid 控件的值”可以了解下受控组件的概念: react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components

是我的话我可能会提 5 个 gridRef 出来, gridRef.getRow?.[0],比较符合直觉

一定不要一层一层传,根本难以维护,更没有意义,这么多层写出来的一定是冗余代码。想要在 Grid 和 Page 之间状态共享,使用 zustand / jotai / redux 这种全局状态库就行了,写出来代码都差不多(这里以 jotai 为例):// 定义 atomexport const dataAtom = atom('')// Grid ,想要使用 data 或 设置 data 值都可以const [data, setDataA] = useAtom(dataAtom)// 同理在 Page 也是一样的:const [data, setDataA] = useAtom(dataAtom)这样写,省去了一层一层的 props ,代码简单多了。更重要的是:你只要知道你在组件里需要什么 atom ,然后使用就行了,没有心智负担。hook 本来就是干这事的。另外,这样的需求,不建议使用 context 。实际上,我在任何情况下都不建议别人使用 context 来做业务代码,context 最常见的是场景其实是封装组件。

学到了啊。我对 service 的刻板印象是处理外部数据的,配合 rxjs 控制对外状态确实骚到我了。

自定义一个 hooks ,在其中用 useState ,可以在其他组件中使用 hook ,

可以用 ref useimperativehandle 把组件的状态暴露出来 向下渗透 ref 也能拿到值

react 是种哲学,学习 declarative ui ,一定要先摒弃 imperative ui 的 get value 、set text 等等思想。数据在 react 中是单向流动的,就像水一样,如果上游发生了变化,那么自然而然,下流必然发生变化。

子组件获取祖先组件的数据,这个逻辑没有问题,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 实例,一个组件更新,一个组件监听,相当于一个小型的受限的广播系统,会比全局的广播好一些。

是的,我的思路还是停留在以前那种对象的写法,就是组件自己提供 api 给外面调用,而 react 的思想是通过输入(prop)来产生输出,数据应该是通过父组件来传过来的。

看了你提供的代码我有几点疑问,因为我没用前端工程化方式做过项目,还是以前那种 jquery 的开发经验,所以我想问:1. 对象作用域// 定义 atomexport const dataAtom = atom('')这个应该是在单独一个文件定义的对象并导出,然后我在 page 和 grid 等不同的文件导入的时候,应该导入的是同一个对象吧?是不是可以理解 dataAtom 是一个类似于定义在 window 对象的单例,在整个浏览器页面访问他都是同一个对象。2.grid 共享数据Grid 组件我是想通过传递一个 URL 和 List给 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 。

  1. useContext 共享全局变量2. useImperativeHandle 层层上传,获取 Grid 对象3. callback 层层向下传React 由于历史包袱的原因,在一些语言设计上是有问题的,一般会更推荐使用现代状态管理库,比如 Redux ,Jotai 等等。初学前端的话,我推荐你使用第一种 useContext ,先试一试,推荐去找个教程看看,可以把 Context 部分抽象成一个 Provider 组件。

    我帮他回答吧。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 。具体的原因你可以看: jotai.org/docs/guides/core-internals#first-version 3. useAtom 本质是一个自定义 hooks ,它内部有 useState 进行处理。你可以理解为 data 是 grid 里的 data ,但也只是值是相同的,因为它其实是属于组件内的 state 。你要知道共享的只是 状态值,但状态是没有共享的,还是由组件内部维护的。如果你对 jotai 感兴趣,可以我看的一篇博客: lesenelir.me/posts/jotai

    我们的项目里用到 Context 的场景是搞了个 AccountContext 存放当前登录账号的数据

    OP 您自己上面一个的回答,还是有些问题。react 作为一个 ui 库,如果说输出的是 ui 的话,它的思想并不是通过 prop 来产生输出的,而是通过 state 来映射出一个 ui 。state 并不等于 data 数据,state > data 。state 其实和用户交互息息相关,你写多就会发现了,您说的数据只是 state 的一个小分支。

    首先在 useEffect 去通过 url 来请求数据本身就不是一个很好的 pattern -----想请教下为啥这样做不合理,我的项目里基本上都是 useEffect 里拉接口 。。。

    分情况把 这个场景就是可用的 react.dev/learn/you-might-not-need-an-effect#fetching-datauseEffect 的心智模型是 Synchronize react component 与 external system

    你的回答我看了,如果在 Page 里面调用同一个 atom ,能够拿到 grid 的数据,但是 grid 在 re-render 时候也会导致 page 整个组件的 re-render ,那比较好奇的是刷新是谁导致的,是 grid 在调用 setatom 的时候导致了 page 的 re-render ,那么 page 在 re-render 时不会再一次导致 grid re-render 吧,这样 grid 就导致了 2 次 render

    我在看看你的博客理解一下

这图都画的这么详细了,一眼看过去就应该知道接下来只能再在外面加一个数据层来进行数据管理啊?毕竟组件和组件之间的关系已经是完备的了。用 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 内部总是有关联的,试论哪种解构方式都无法脱离耦合,只是高低的区别而已。