React中的事件驱动方法?

21

我想在一个组件中触发一个事件,让其他组件“订阅”该事件并在React中执行一些工作。

例如,在典型的React项目中,我有一个模型,从服务器获取数据并使用该数据呈现多个组件。

interface Model {
   id: number;
   value: number;
}

const [data, setData] = useState<Model[]>([]);
useEffect(() => {
   fetchDataFromServer().then((resp) => setData(resp.data));
}, []);

<Root>
   <TopTab>
     <Text>Model with large value count:  {data.filter(m => m.value > 5).length}</Text>
   </TobTab>
   <Content>
      <View>
         {data.map(itemData: model, index: number) => (
            <Item key={itemData.id} itemData={itemData} />
         )}
      </View>
   </Content>
   <BottomTab data={data} />
</Root>

在一个子组件中,可以编辑和保存模型。

const [editItem, setEditItem] = useState<Model|null>(null);
<Root>
   <TopTab>
     <Text>Model with large value count:  {data.filter(m => m.value > 5).length}</Text>
   </TobTab>
   <ListScreen>
      {data.map(itemData: model, index: number) => (
          <Item 
             key={itemData.id} 
             itemData={itemData} 
             onClick={() => setEditItem(itemData)}
          />
      )}
   </ListScreen>
   {!!editItem && (
       <EditScreen itemData={editItem} />
   )}
   <BottomTab data={data} />
</Root>

假设它是EditScreen:

const [model, setModel] = useState(props.itemData);

<Input 
   value={model.value}
   onChange={(value) => setModel({...model, Number(value)})}
/>
<Button 
   onClick={() => {
       callSaveApi(model).then((resp) => {
           setModel(resp.data);
           // let other components know that this model is updated
       })
   }}
/>

应用程序必须让 TopTabBottomTabListScreen 组件更新数据

  • 而不需要 再次调用服务器的 API(因为 EditScreen.updateData 已经从服务器获取了更新的数据);以及
  • 不需要将 updateData 函数作为 props 传递(因为在大多数实际情况下,组件结构太复杂,无法将所有函数作为 props 传递)

为了有效地解决上述问题,我想触发一个事件(例如“model-update”),并带有一个参数(已更改的模型),让其他组件订阅该事件并更改其数据,例如:

// in EditScreen
updateData().then(resp => {
   const newModel = resp.data;
   setModel(newModel);
   Event.emit("model-updated", newModel);
});

// in any other components
useEffect(() => {
   // subscribe model change event
   Event.on("model-updated", (newModel) => {
      doSomething(newModel);
   });
   // unsubscribe events on destroy
   return () => {
     Event.off("model-updated");
   }
}, []);

// in another component
useEffect(() => {
   // subscribe model change event
   Event.on("model-updated", (newModel) => {
      doSomethingDifferent(newModel);
   });
   // unsubscribe events on destroy
   return () => {
     Event.off("model-updated");
   }
}, []);

使用React hooks是否可能?

如何在React hooks中实现事件驱动方法?


1
好的,你可以使用 redux 来解决这个问题。并且你可以使用 'useSelector' 钩子。根据文档,useSelector() 也会订阅 Redux store,并在每次 action 被分发时运行你的 selector。 - Naresh
@Naresh 我不知道...它是事件驱动的吗?你能举个例子吗? - glinda93
Redux在reducers中使用actions,类似于事件,但是是纯函数式的。您也可以在普通的React中使用useReducer来获得类似的功能,但它不如Redux强大。最后,您可以使用合成事件处理程序,这在React中是最典型的。 - Nick McCurdy
1
看一下这个:https://codesandbox.io/s/9on71rvnyo?file=/src/components/Todo.js 它没有完全使用hooks实现,但你可以通过遵循这个文档来更新它。 - Naresh
5个回答

12

事件发射器没有替代品,因为React钩子和使用上下文取决于dom树深度并且范围有限。

在React(或React Native)中使用EventEmitter被认为是一种好的做法吗?

A:是的,在dom树中存在组件时,这是一种好的方法。

我正在寻找React中的事件驱动方法。我现在很满意我的解决方案,但是我是否可以使用React hooks实现相同的功能?

A:如果您指的是组件状态,则hooks将无法帮助您在组件之间共享它。组件状态局限于该组件。如果您的状态存在于上下文中,则useContext hook将会有所帮助。 对于useContext,我们必须使用MyContext.ProviderMyContext.Consumer实现完整的上下文API,并将其包装在高阶组件(HOC)中 Ref

因此,事件发射器是最佳选择。

在React Native中,您可以使用react-native-event-listeners软件包。

yarn add react-native-event-listeners

发送器组件

import { EventRegister } from 'react-native-event-listeners'

const Sender = (props) => (
    <TouchableHighlight
        onPress={() => {
            EventRegister.emit('myCustomEvent', 'it works!!!')
        })
    ><Text>Send Event</Text></TouchableHighlight>
)

接收器组件

class Receiver extends PureComponent {
    constructor(props) {
        super(props)
        
        this.state = {
            data: 'no data',
        }
    }
    
    componentWillMount() {
        this.listener = EventRegister.addEventListener('myCustomEvent', (data) => {
            this.setState({
                data,
            })
        })
    }
    
    componentWillUnmount() {
        EventRegister.removeEventListener(this.listener)
    }
    
    render() {
        return <Text>{this.state.data}</Text>
    }
}

const Event = new EventEmitter(); 在React Native中无法工作,但在React JS中可以。对于Expo或React Native,您可以使用此软件包。 - Muhammad Numan
react-native-event-listeners 这个包适用于所有版本的 React Native 和 Expo。 - Muhammad Numan
1
对于useContext,我们必须使用MyContext.ProviderMyContext.Consumer实现完整的上下文API,并将其包装在高阶组件中。 - Muhammad Numan
@bravemaster,为什么您没有接受我的答案? - Muhammad Numan
这就是为什么我仍然更喜欢在大多数应用程序中使用Vue。对于高度自定义和/或大规模应用程序,它确实存在缺点,但我认为其API涵盖了现代Web开发的重要要点,并以直观的方式实现了它们。 - GHOST-34
显示剩余14条评论

9
我不确定为什么 EventEmitter 被投票反对,但这是我的看法:
当涉及到状态管理时,我认为使用基于 Flux 的方法通常是比较好的选择(Context/Redux 等都很棒)。话虽如此,我真的不明白为什么基于事件的方法会有任何问题 – JS 是基于事件的,React 只是一个库而已,甚至不是一个框架,我不明白为什么我们会被迫遵循它的指南。
如果您的 UI 需要了解应用程序的总体状态并做出响应,请使用 reducers,更新 store,然后使用 Context/Redux/Flux/whatever - 如果您只需要响应特定事件,请使用 EventEmitter。
使用 EventEmitter 将允许您在 React 和其他库之间进行通信,例如 canvas (如果您不使用 React Three Fiber,我敢打赌,在没有事件的情况下尝试与 ThreeJS/WebGL 交流会很困难),而不需要所有样板文件。有许多情况下使用 Context 是一场噩梦,我们不应该感到受 React API 的限制。
如果适合您,并且可扩展,请放心实施。
编辑:以下是使用 eventemitter3 的示例:
./emitter.ts
import EventEmitter from 'eventemitter3';

const eventEmitter = new EventEmitter();

const Emitter = {
  on: (event, fn) => eventEmitter.on(event, fn),
  once: (event, fn) => eventEmitter.once(event, fn),
  off: (event, fn) => eventEmitter.off(event, fn),
  emit: (event, payload) => eventEmitter.emit(event, payload)
}

Object.freeze(Emitter);

export default Emitter;

./some-component.ts

import Emitter from '.emitter';

export const SomeComponent = () => {
  useEffect(() => {
    // you can also use `.once()` to only trigger it ... once
    Emitter.on('SOME_EVENT', () => do what you want here)
    return () => {
      Emitter.off('SOME_EVENT')
    }
  })
}

从那里,你可以在任何你想要的地方触发事件,订阅它们并对其进行操作,传递一些数据,真正做任何你想做的事情。


我了解到,当状态是对象时,EventEmitters与React状态不兼容,尤其是在使用React时。但是,您可以使用createRef来避免这个问题。 - glinda93
当然,因为状态是异步的。我的观点是,有时你想要在没有附加数据或非常少量数据的情况下传递事件,这在开发更“创意”的应用程序时非常有用。但是,如果您需要在使用EventEmitter时交换数据,则应使用引用。 - Joel Beaudon
2
简单明了,比Redux和Context容易得多。我不知道为什么不在任何地方都使用这种方法。 只是想知道为什么要创建一个新对象并将其冻结。导出eventEmitter也可以正常工作。 - Eduardo
我喜欢冻结导出的对象以防止它们被操纵。虽然深度冻结可能更有用 :) - Joel Beaudon
3
全局事件总线模式感觉像是欺骗——没错。但为什么欺骗感觉如此美好呢?这是因为Flux模式就像宗教正统,而你是个异端;你很美丽,就是那样。但是说真的,在像Figma这样的应用程序中,您最终需要与不共享状态的组件进行通信。如果感觉良好,就这样做吧。(嘘...感觉真的很好) https://www.pluralsight.com/guides/how-to-communicate-between-independent-components-in-reactjs - MeatFlavourDev

4
我们遇到了类似的问题,并从useSWR中获得了启发。
以下是我们实现的简化版本:
const events = [];
const callbacks = {};

function useForceUpdate() {
   const [, setState] = useState(null);
   return useCallback(() => setState({}), []);
}

function useEvents() {

    const forceUpdate = useForceUpdate();
    const runCallbacks = (callbackList, data) => {
       if (callbackList) {
          callbackList.forEach(cb => cb(data));
          forceUpdate();
       }
     
    }

    const dispatch = (event, data) => {
        events.push({ event, data, created: Date.now() });
        runCallbacks(callbacks[event], data);
    }

    const on = (event, cb) => {
        if (callbacks[event]) {
           callbacks[event].push(cb);
        } else {
          callbacks[event] = [cb];
        }

        // Return a cleanup function to unbind event
        return () => callbacks[event] = callbacks[event].filter(i => i !== cb);
    }

    return { dispatch, on, events };
}

在一个组件中,我们执行以下操作:
const { dispatch, on, events } = useEvents();

useEffect(() => on('MyEvent', (data) => { ...do something...}));

这个方法之所以很好用,有以下几点原因:
  1. 与窗口事件系统不同,事件数据可以是任何类型的对象。这样可以避免对载荷进行字符串化等操作,并且不会与任何内置浏览器事件发生冲突。
  2. 全局缓存(从SWR借鉴的思路)意味着我们可以在需要时仅使用useEvents,而无需将事件列表和调度/订阅函数传递到组件树中或在react上下文中处理。
  3. 将事件保存到本地存储或重放/倒带它们是微不足道的。

我们唯一的麻烦是每次分派事件时都使用forceUpdate,这意味着接收事件列表的每个组件都会重新渲染,即使它们没有订阅该特定事件也是如此。这在复杂的视图中是一个问题。 我们正在积极寻找解决方案...


0

你可以使用React的全局状态管理来实现这个功能。

在你的store中,为你的事件订阅添加一个useEffect,并为每个事件添加一个reducer。

如果你有两个数据源,即订阅和查询,则使用查询初始化你的状态值,然后监听订阅。

类似于这样:

const reducer = (state, action) => {
   switch(action.type) {
     case 'SUBSCRIBE':
        return action.payload
     default:
        return state
   }
}

假设您正在使用 https://github.com/dai-shi/use-reducer-async
const asyncActions = {
   QUERY: ({ dispatch }) => async(action) => {
     const data = await fetch(...)
     dispatch({ type: 'query', payload: data })
   }
}

你也可以在Redux中使用中间件

const [state, dispatch] = useReducer(reducer, initialValues, asyncActions)

useEffect(() => {
   dispatch({ type: 'QUERY' })
   Event.on((data) => {
      dispatch({ type: 'SUBSCRIBE', payload: data })
   })
   return () => Event.off()
}, [])

return <Provider value={state}>{children}</Provider>

0

你可以在 App.js 中使用useContext创建上下文,然后在子组件中使用其中的值并更新上下文。一旦上下文被更新,其他子组件中使用的值也会被更新,无需传递 props。


是的,但是你如何使用useContext执行不同的函数呢? - glinda93

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接