V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hahaFck
V2EX  ›  React

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

  •  
  •   hahaFck · 15 天前 · 3888 次点击

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

    component

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

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

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

    60 条回复    2024-04-17 20:42:01 +08:00
    Histo
        1
    Histo  
       15 天前
    可以把 Grid 的 state 提升到 Page 中,或者直接在 Page 上包裹一个 Context.Provider ,用 context 管理状态
    leaflxh
        2
    leaflxh  
       15 天前
    另一种自用的办法是在 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<IToolbarDataChangeEvent>("toolar::handleDataChange", {detail: {msg: "a msg from grid"}});
    document.dispatchEvent(event)
    leaflxh
        3
    leaflxh  
       15 天前
    来自野路子前端,不确定是否是推荐的做法(
    zbinlin
        4
    zbinlin  
       15 天前   ❤️ 1
    这种情况一般用 Context
    shizhibuyu2023
        5
    shizhibuyu2023  
       15 天前
    要学会用 gpt
    我的话会直接用状态管理库
    Provide 给 Grid 控件去赋值,属于狗屎代码
    kneo
        6
    kneo  
       15 天前
    能处理的方法很多。比如把多个 callback 封装成一个,使用 children 传递子控件,使用 context/provider 。但是我想说,才 5 个 callback 有什么可怕的?搞后端没这么脆弱。
    lqm
        7
    lqm  
       15 天前
    直接上状态库,zustand ,早晚会上的
    huijiewei
        8
    huijiewei  
       15 天前
    用 context 跨级别传
    hahaFck
        9
    hahaFck  
    OP
       15 天前
    @Histo
    @zbinlin
    如果用 context 的话,useContext 的代码就要硬编码到 grid 组件中,这样这个组件也不通用了吧,放到另外页面的 context 里面又要重新写一份。
    wisetc
        10
    wisetc  
       15 天前 via iPhone
    这是 react 特别的地方,你说的对 react 组件是基于一般属性和回调属性的单向数据流,所有的子组件都可以由父组件自上而下赋值参数来控制,组件间也是提倡组合大于继承,不同的组件间传参共享数据是通过公共父类组件居中调度,这一点在某些清新下确实会不太方便,但是也在一定程度上避免了组件间的相互修改,而是给它什么就是什么显得很确定。若是例子中的情形也是可以通过设定属性的,react 是可以透传属性的,跨组件传递数据流确实会有一些不太方便就是,大应用可以用集中的数据管理作为公共的数据源获得其中的状态,然后转化为普通组件的属性
    orzorzorzorz
        11
    orzorzorzorz  
       15 天前   ❤️ 3
    是的,照文档看且不考虑状态管理,你只能一层层往上传。context 自己用还行,人多了就很乱。
    歪个楼。19 年那会我自己要开个项目,rva 选了半天,最后选了 a 。从结果看,只能说 angular 这种填空式开发是真的是在三年后等你,不太会有像 op 主楼里这样的心智负担。
    Ghrhrrv146
        12
    Ghrhrrv146  
       15 天前
    使用状态管理库最简单
    wu67
        13
    wu67  
       15 天前 via Android
    使用状态库,
    虽然项目早期时可能你会觉得烦琐,但是越往后越多需要共享状态的业务
    ChefIsAwesome
        14
    ChefIsAwesome  
       15 天前
    这就是典型的组件一对多的模式,要靠一个中间人来完成传递。这个中间人肯定是这几个组件的上一级。前面几层楼推荐的 context ,状态管理库,都是这么个中间人。你定义 customEvent ,在 windows 上 dispatch ,那也叫中间人,也能解决你的问题。
    你随便写就好了,不要想着非得符合“react”的路子才行。
    ztcaoll222
        15
    ztcaoll222  
       15 天前
    context 、状态库、或者 Grid 直接写 localStorage(笑
    Leviathann
        16
    Leviathann  
       15 天前
    @hahaFck 这是 explicity 的体现,不然你怎么知道这个组件有一个可以被捕获的事件

    组件和一个 context 的配合一起使用也是一个比较常见的 pattern
    darkengine
        17
    darkengine  
       15 天前
    状态上移,我会把所有数据和点击的响应函数都放在 Page 里,Toolbar 只负责通知 Page 某个 Button 被点击了,Layout, Some, Grid 只负责根据数据展示。
    maolon
        18
    maolon  
       15 天前 via Android
    状态上移+1 同时使用 children 的方法摊平组件
    <Page>
    <Toolbar/>
    <Layout>
    <some>
    <Grid {...props}>
    </some>
    <Laout>
    </Page>
    ymcz852
        19
    ymcz852  
       15 天前 via iPhone
    @orzorzorzorz 那么,请问 angular 是如何处理呢🤔
    ebushicao
        20
    ebushicao  
       15 天前
    最简单的方法就是把状态提升到需要的最上层,不过这样在组件层次过多的情况下既不利于维护,也会存在性能问题,尤其是中间传递某些组件不需要用到这个状态。
    然后就是用 Context API ,因为不推荐所以详情建议去看 React 官方文档。
    然后是使用状态管理库,把这种多个组件使用的状态交由状态管理库管理,算是最合适的处理了,喜欢稳定的就是 redux 和 mobx ,喜欢更现代的就看看 recoil ,zustand ,jotai
    ericgui
        21
    ericgui  
       14 天前
    @leaflxh #2 不错,这个思路也挺好
    ericgui
        22
    ericgui  
       14 天前
    1 楼和 2 楼都可以

    1 楼的又可以分为:context 和 state management library
    wcf
        23
    wcf  
       14 天前
    有这个 API useImperativeHandle 相当于 vue 的 ref
    orzorzorzorz
        24
    orzorzorzorz  
       14 天前
    @ymcz852 我只知道实例化调用。
    不过实际上就算在 react 里,我也没碰见过层级那么深的组件。真有 op 这个场景,那是要考虑组件抽象是不是出问题了。
    LandCruiser
        25
    LandCruiser  
       14 天前
    React 的官方文档看一遍吧,两种方法,一种是传递一个函数到 grid 组件里去,另一种是把状态提到 page 组件。
    gogozs
        26
    gogozs  
       14 天前
    @leaflxh 发送事件就是一对多了,楼主的例子更符合一对一的情况
    Baymaxbowen
        27
    Baymaxbowen  
       14 天前
    1 、通过一层层的回调
    2 、通过状态管理库或者 context
    3 、通过自定义事件,在 toolbar 发送事件,在 Grid 监听事件( https://mebtte.com/split_react_state_by_event
    lanlanye
        28
    lanlanye  
       14 天前
    推荐做法是把状态提到上层,毕竟你要在上层用,说明这个状态应该属于上层。
    如果跨多层或者分散导致不方便一层层传递的话可以使用 Provider 之类的东西,但这不改变你把状态提升到上层的事实。
    aliyun2017
        29
    aliyun2017  
       14 天前
    如果这值不用即时渲染到页面上,就直接写一个缓存对象;要即时渲染就上状态管理
    jguo
        30
    jguo  
       14 天前
    如果 toolbar 需要用到 grid 里的值,那这个值大概率不该属于 grid
    mouyase
        31
    mouyase  
       14 天前
    首先是上面各位大佬说过的问题。

    这个 state 如果 grid 组件并不需要为其生产数据,而只是消费数据的话,那可以把 state 提升到上层去生产他的组件里面。
    可以用 Provider+useContext 的形式跨级传入,也可以用 props 逐级传入。当然你用一些全局的状态管理也不是不行。

    如果这个值不涉及到渲染,也可以把这个值定义为 ref ,然后给 grid 组件使用 forwardRef 高阶函数包裹,同时使用 useImperativeHandle 把数值/函数暴露给父级,父级通过 ref.current.数值的形式获取。

    最主要的还是看这个数值的作用和来源是什么。
    ljpCN
        32
    ljpCN  
       14 天前 via iPhone
    大家只说了解决方案,但这个需求是否是合理的还得看具体场景,很多时候是设计问题,这个状态只需要 grid 自持就行。
    zbowen66
        33
    zbowen66  
       14 天前
    jutai, 跨组件的 useState
    xitler
        34
    xitler  
       14 天前
    状态提升,入门教程不是都有吗
    nzbin
        35
    nzbin  
       14 天前   ❤️ 1
    @ymcz852 在 angular 中最简单的方式就是通过 service 共享状态,本质就是 DI + rxjs
    xwwsxp
        36
    xwwsxp  
       14 天前
    https://easy-peasy.dev/ 如果会 Vue ,直接使用这个组件库
    douxc
        37
    douxc  
       14 天前
    如果只是父子的话,父组件用 useReducer ,把 dispatch 传给子组件
    douxc
        38
    douxc  
       14 天前
    @douxc #37 像题目这种跨多级的,在最近一个公共祖先节点用 context 维护状态,然后向下分发
    GeekGao
        39
    GeekGao  
       14 天前
    全局状态用 Jotai 库管理即可
    myl0204
        40
    myl0204  
       14 天前
    很多人说到了,状态提升。

    “我想在 toolbar 中的一个 button 点击事件中获取 Grid 控件的值”

    可以了解下受控组件的概念: https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components
    ZGame
        41
    ZGame  
       14 天前
    是我的话我可能会提 5 个 gridRef 出来, gridRef.getRow?.[0],比较符合直觉
    RRRSSS
        42
    RRRSSS  
       14 天前
    一定不要一层一层传,根本难以维护,更没有意义,这么多层写出来的一定是冗余代码。

    想要在 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
        43
    orzorzorzorz  
       14 天前
    @nzbin 学到了啊。我对 service 的刻板印象是处理外部数据的,配合 rxjs 控制对外状态确实骚到我了。
    ivslyyy
        44
    ivslyyy  
       14 天前
    自定义一个 hooks ,在其中用 useState ,
    可以在其他组件中使用 hook ,
    madao199
        45
    madao199  
       14 天前
    可以用 ref useimperativehandle 把组件的状态暴露出来 向下渗透 ref 也能拿到值
    superedlimited
        46
    superedlimited  
       14 天前 via iPhone
    react 是种哲学,学习 declarative ui ,一定要先摒弃 imperative ui 的 get value 、set text 等等思想。数据在 react 中是单向流动的,就像水一样,如果上游发生了变化,那么自然而然,下流必然发生变化。
    jinliming2
        47
    jinliming2  
       14 天前 via iPhone
    子组件获取祖先组件的数据,这个逻辑没有问题,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
        48
    hahaFck  
    OP
       14 天前
    @superedlimited
    @jinliming2
    是的,我的思路还是停留在以前那种对象的写法,就是组件自己提供 api 给外面调用,而 react 的思想是通过输入(prop)来产生输出,数据应该是通过父组件来传过来的。
    hahaFck
        49
    hahaFck  
    OP
       14 天前
    @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
        50
    monster1priest  
       14 天前
    1. useContext 共享全局变量
    2. useImperativeHandle 层层上传,获取 Grid 对象
    3. callback 层层向下传

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

    初学前端的话,我推荐你使用第一种 useContext ,先试一试,推荐去找个教程看看,可以把 Context 部分抽象成一个 Provider 组件。
    Lesenelir
        51
    Lesenelir  
       14 天前
    @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
        52
    darkengine  
       14 天前
    @RRRSSS 我们的项目里用到 Context 的场景是搞了个 AccountContext 存放当前登录账号的数据
    Lesenelir
        53
    Lesenelir  
       14 天前
    @hahaFck

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

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

    useEffect 的心智模型是 Synchronize react component 与 external system
    hahaFck
        56
    hahaFck  
    OP
       13 天前
    @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
        57
    hahaFck  
    OP
       13 天前
    @Lesenelir 我在看看你的博客理解一下
    shunia
        58
    shunia  
       13 天前
    这图都画的这么详细了,一眼看过去就应该知道接下来只能再在外面加一个数据层来进行数据管理啊?毕竟组件和组件之间的关系已经是完备的了。

    用 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
        59
    RRRSSS  
       12 天前
    @hahaFck 我觉得在写 React 不用太考虑 re-render 问题,遇到了再解决就好。

    和后端一样,在没有遇到性能问题的时候,先别考虑加缓存什么的。
    hahaFck
        60
    hahaFck  
    OP
       12 天前
    @RRRSSS 嗯嗯,是的,考虑的太多就把事情弄复杂了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1246 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 23:29 · PVG 07:29 · LAX 16:29 · JFK 19:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.