Event-driven approach in React?

泄露秘密 提交于 2020-07-18 11:48:28

问题


I'd like to "fire an event" in one component, and let other components "subscribe" to that event and do some work in React.

For example, here is a typical React project.

I have a model, fetch data from server and several components are rendered with that data.

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>

In one child component, a model can be edited and saved.

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>

Let's assume it's 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
       })
   }}
/>

App must let TopTab, BottomTab and ListScreen component to update data

  • without calling API from server again (because EditScreen.updateData already fetched updated data from server) and
  • not passing updateData function as props (because in most real cases, components structure is too complex to pass all functions as props)

In order to solve above problem effectively, I'd like to fire an event (e.g. "model-update") with an argument (changed model) and let other components subscribe to that event and change their data, e.g.:

// 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");
   }
}, []);

Is it possible using React hooks?

How to implement event-driven approach in React hooks?


回答1:


in react native you can use react-native-event-listeners package

yarn add react-native-event-listeners

SENDER COMPONENT

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

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

RECEIVER COMPONENT

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>
    }
}



回答2:


EventEmitter goes against the fundamentals of the Flux architecture where data only flows down. There must never be a case where a state change in a component affects a sibling component.

The way to go is to use a global state management library, such as Redux. You can use the useSelector() hook to watch for a particular value, then use that value as the dependency of an effect.

const model = useSelector(store => store.model)

useEffect(() => doSomething(model), [model])

One thing to watch out when using this approach is that the effect will also run on component mount, even if model didn't change.




回答3:


You can create use context in App.js using useContext, and then in you child component you can use values from it and update the context as soon as the context get updated it will update the values being used in other child component, no need to pass props.




回答4:


I ended up using EventEmitter.

Javascript is an event-driven language after all.

Here is a working example: link

I tried to embed it in SO code snippet but EventEmitter does not work in browser.

// console.log("This example does not work in snippet because EventEmitter does not work in browser.\n You can see working example here: https://codesandbox.io/s/eventemitter-and-react-84eey?file=/src/Title.js");
const { useState, useEffect, useReducer } = React;
const DEFAULT_APP_STATE = "Initial state";
const Event = new EventEmitter();
const appReducer = (state = DEFAULT_APP_STATE, action) => {
  switch (action.type) {
    case "update":
      return action.payload;
    default:
      return action.payload;
  }
};

const Title = () => {
  const [reducerState, dispatch] = useReducer(
    appReducer,
    DEFAULT_APP_STATE
  );
  const [state, setState] = useState(reducerState);
  useEffect(() => {
    console.log("new state by Reducer", reducerState);
    setState(reducerState);
  }, [reducerState]);

  useEffect(() => {
    Event.addListener("update", newState => {
      console.log("new state by EventEmitter", newState);
      setState(newState);
    });
    return () => {
      Event.removeListener("update");
    };
  }, []);

  return <h1>Title state: {state}</h1>;
};

const Description = () => {
  const [reducerState, dispatch] = useReducer(
    appReducer,
    DEFAULT_APP_STATE
  );
  const [state, setState] = useState(reducerState);
  return (
    <React.Fragment>
      <p>Description State: {state}</p>
      <p>
        <button
          onClick={() => {
            const newState = "State updated by useReducer";
            setState(newState);
            dispatch({
              type: "update",
              payload: "State updated by useReducer"
            });
          }}
        >
          Dispatch using Reducer
        </button>
      </p>
      <p>
        <button
          onClick={() => {
            const newState = "State updated by EventEmitter";
            setState(newState);
            Event.emit("update", newState);
          }}
        >
          Emit by EventEmitter
        </button>
      </p>
      <p>
        <button
          onClick={() => {
            location.reload();
          }}
        >
          Refresh
        </button>
      </p>
    </React.Fragment>
  );
};

const App = () => {
  return (
    <div className="App">
      <Title />
      <Description />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  rootElement
);
<html>

<body>
  <div id="root">
     <p>This example does not work in snippet because EventEmitter does not work in browser.</p>
     <p>You can see working example <a target="_blank" href="https://codesandbox.io/s/eventemitter-and-react-84eey?file=/src/Title.js">here</a></p>
  </div>
  <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

</body>

</html>


来源:https://stackoverflow.com/questions/62827419/event-driven-approach-in-react

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!