React 中的事件驱动方法?

IT技术 reactjs react-native react-hooks event-driven
2021-05-04 22:46:08

我想在一个组件中“触发一个事件”,让其他组件“订阅”该事件并在 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 传递)

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

// 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 钩子?

如何在 React 钩子中实现事件驱动的方法?

4个回答

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

将 EventEmitter 与 React(或 React Native)一起使用是否被认为是一种好习惯?

A: 是的,当 dom 树中有很深的组件时,这是一个很好的处理方式

我正在 React 中寻求事件驱动的方法。我现在对我的解决方案很满意,但我可以用 React 钩子实现同样的目标吗?

A:如果您指的是组件状态,那么钩子将无法帮助您在组件之间共享它。组件状态是组件本地的。如果您的状态存在于上下文中,那么 useContext 钩子会有所帮助。对于 useContext,我们必须使用MyContext.Provider实现完整的上下文 API,MyContext.Consumer并且必须包装在高阶 ( HOC ) 组件 Ref 中

所以事件发射器是最好的。

在本机react中,您可以使用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>
    }
}

不知道为什么 EventEmitter 被否决了,但这是我的看法:

说到状态管理,我相信使用基于 Flux 的方法通常是可行的方法(Context/Redux 和朋友都很棒)。也就是说,我真的不明白为什么基于事件的方法会带来任何问题——JS基于事件的,而 React 毕竟只是一个库,甚至不是一个框架,我不明白为什么我们会被迫遵守其准则。

如果您的 UI 需要了解应用程序的一般状态并对其做出react,请使用 reducer,更新您的商店,然后使用 Context/Redux/Flux/whatever - 如果您只需要对特定事件做出react,请使用 EventEmitter。

使用 EventEmitter 将允许您在 React 和其他库之间进行通信,例如画布(如果您不使用 React Three Fiber,我敢于尝试在没有事件的情况下与 ThreeJS/WebGL 交谈)而无需所有样板。在很多情况下,使用 Context 是一场噩梦,我们不应该受到 React API 的限制。

如果它适合你,并且它是可扩展的,那就去做吧。

编辑:这是一个使用 eventemitter3 的例子:

./发射器.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')
    }
  })
}

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

您可以使用useContext在 App.js 中创建使用上下文,然后在您的子组件中,您可以使用它的值并在上下文更新后立即更新上下文,它将更新其他子组件中正在使用的值,无需传props。

我们遇到了类似的问题,并从 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. 与窗口Event系统不同,事件数据可以是任何类型的对象。这节省了必须的stringify有效载荷和其他什么。这也意味着不会与任何内置浏览器事件发生冲突
  2. 全局缓存(从 SWR 借来的想法)意味着我们可以在useEvents任何需要的地方使用,而无需将事件列表和分派/订阅功能向下传递到组件树,或者搞乱 React 上下文。
  3. 将事件保存到本地存储或重放/倒带它们是微不足道的

我们forceUpdate遇到的一个令人头疼的问题是每次调度事件时都使用 意味着接收事件列表的每个组件都会重新呈现,即使它们没有订阅该特定事件。这是复杂视图中的一个问题。我们正在积极寻找解决方案...