问题
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