Event-driven approach in React? - reactjs

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?

There cannot be an alternative of event emitter because React hooks and use context is dependent on dom tree depth and have limited scope.
Is using EventEmitter with React (or React Native) considered to be a good practice?
A: Yes it is a good to approach when there is component deep in dom tree
I'm seeking event-driven approach in React. I'm happy with my solution now but can I achieve the same thing with React hooks?
A: If you are referring to component state, then hooks will not help you share it between components. Component state is local to the component. If your state lives in context, then useContext hook would be helpful.
For useContext we have to implement full context API with MyContext.Provider and MyContext.Consumer and have to wrap inside high order (HOC) component
Ref
so event emitter is best.
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>
}
}

Not sure why the EventEmitter has been downvoted, but here's my take:
When it comes to state management, I believe using a Flux-based approach is usually the way to go (Context/Redux and friends are all great). That said, I really don't see why an event-based approach would pose any problem - JS is event based and React is just a library after all, not even a framework, and I can't see why we would be forced to stay within its guidelines.
If your UI needs to know about the general state of your app and react to it, use reducers, update your store, then use Context/Redux/Flux/whatever - if you simply need to react to specific events, use an EventEmitter.
Using an EventEmitter will allow you to communicate between React and other libraries, e.g. a canvas (if you're not using React Three Fiber, I dare you to try and talk with ThreeJS/WebGL without events) without all the boilerplate. There are many cases where using Context is a nightmare, and we shouldn't feel restricted by React's API.
If it works for you, and it's scalable, just do it.
EDIT: here's an example using 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')
}
})
}
From there you trigger events wherever you want, subscribe to them, and act on it, pass some data around, do whatever you want really.

We had a similar problem and took inspiration from useSWR.
Here is a simplified version of what we implemented:
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 };
}
In a component we do:
const { dispatch, on, events } = useEvents();
useEffect(() => on('MyEvent', (data) => { ...do something...}));
This works nicely for a few reasons:
Unlike the window Event system, event data can be any kind of object. This saves having to stringify payloads and what not. It also means there is no chance of collision with any built-in browser events
The global cache (idea borrowed from SWR) means we can just useEvents wherever needed without having to pass the event list & dispatch/subscribe functions down component trees, or mess around with react context.
It is trivial to save the events to local storage, or replay/rewind them
The one headache we have is the use of the forceUpdate every time an event is dispatched means every component receiving the event list is re-rendered, even if they are not subscribed to that particular event. This is an issue in complex views.
We are actively looking for solutions to this...

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.

You can achieve this with any React's global state management.
In your store, have a useEffect for your event subscription, and a reducer for each of your event.
If you have 2 data sources, the subscription and the query, then initialize your state values with your query, then listen to the subscription.
Something like this
const reducer = (state, action) => {
switch(action.type) {
case 'SUBSCRIBE':
return action.payload
default:
return state
}
}
Assuming you are using https://github.com/dai-shi/use-reducer-async
const asyncActions = {
QUERY: ({ dispatch }) => async(action) => {
const data = await fetch(...)
dispatch({ type: 'query', payload: data })
}
}
You can also use middleware in 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>

Related

How to make react components communicate each others in large component tree

In my react app, I have some complex component tree.
In this component tree, I have a <Footer/> component with buttons. I also have <SomeComponent/> component elsewhere in the tree. This component is actually loaded from some dynamic code and is not always the same (similar to some widget engine, where the container is handled by the app engine, and the content is dynamically loaded). It means the context has no knowledge of what are actually the components.
In order to plug everything else, I have a custom react context that holds some fields and methods, which is exposed trough a custom useMyContext hook.
This is working quite well except one remaining issue :
In my <Footer /> I have a button that should call something inside the <SomeComponent/> component. As an example I may have a 'Refresh' button that should ask the component to get latest data.
Basically I have this react tree:
App
SomeContextProvider
Footer
RefreshButton
Deep/Nested/Component/Structure
SomeComponent
(contains a refresh function)
How can I call the refresh function in my component from the footer ?
I tried to play with forwarding refs and useImperativeHandler hook, which may work, but the deep nesting of component tree leads to a big mess of forwarding refs.
I also tried to extend the context provider, but I didn't found a way to "reverse" the callback (context can react to Refresh button action, but I cannot react to this in sibling branch of the component tree).
How could I handle this ?
PS: if it matters, I'm using react 16.13.1 and typescript 4.5
I think I have the start of a clean solution.
Basically, I can handle my scenario by implementing a subscribe/unsubscribe pattern hold by by app context.
This way I can emit some kind of event from my outer context, and let components in the tree subscribe and handle the events as needed.
Some repro : https://codesandbox.io/s/infallible-chaplygin-79c3gq?file=/src/App.tsx.
Relevant parts below:
Custom react context
type Subscribe = (cb: () => void) => () => void;
type AppContextData = {
subscribe: Subscribe;
onSubmit: () => void;
};
const AppContext = createContext<AppContextData | undefined>(undefined);
const useAppContext = (): AppContextData => {
const context = useContext(AppContext);
if (!context)
throw new Error(`useAppContext must be used within a AppContextProvider`);
return context;
};
Container component
const AppContextProvider: React.FC<PropsWithChildren<{}>> = ({ children }) => {
const subscribtions: (() => void)[] = [];
const subscribe: Subscribe = (cb) => {
subscribtions.push(cb);
return () => {
subscribtions.splice(subscribtions.indexOf(cb), 1);
};
};
const emitSubmit = () => {
subscribtions.forEach((cb) => cb());
};
const appContext: AppContextData = {
subscribe,
onSubmit: emitSubmit
};
return (
<AppContext.Provider value={appContext}>{children}</AppContext.Provider>
);
};
App
export default function App() {
return (
<AppContextProvider>
<div className="App">
<Main />
<Footer />
</div>
</AppContextProvider>
);
}
And finally, subscription and submission trigger:
Component with button
const Footer: React.VFC = () => {
const { onSubmit } = useAppContext();
return <button onClick={onSubmit}>Submit</button>;
};
Component that subscribes (and unsubscribe thanks to react effect)
const Main: React.VFC = () => {
const [myString, setMyString] = useState("initial");
const context = useAppContext();
useEffect(() => {
return context.subscribe(() => setMyString("from context"));
}, [context]);
return <p>{myString}</p>;
};

How to properly implement "subscription"-like fetches with React useEffect

I have a question about the "proper" (or most idiomatic) way to implement network fetch behavior in React based on a single changing property.
A simplified example of the functionality I'm building is below: I am looking to build a multi-page form that "auto-saves" a draft of form inputs as the user navigates back/forth between pages.
TL;DR - I thought useEffect hooks would be the right way to save a draft to the backend every time a url slug prop changes, but I'm running into issues, and wondering about suggestions for the "right" tool for this type of behavior.
Here is my attempt so far. My code is technically working how I want it to, but violates React's recommended hook dependency pattern (and breaks the exhaustive-deps ESLint rule).
import React from 'react';
const ALL_SLUGS = [
'apple',
'banana',
'coconut',
];
function randomUrlSlug() {
return ALL_SLUGS[Math.floor((Math.random() * ALL_SLUGS.length))];
}
// just resovles the same object passed in
const dummySaveDraftToBackend = (input) => {
return new Promise((resolve, _reject) => {
setTimeout(() => {
resolve(input);
}, 1000);
});
};
export function App() {
const [urlSlug, setUrlSlug] = React.useState(randomUrlSlug());
return (
<MyComponent urlSlug={urlSlug} setUrlSlug={setUrlSlug} />
);
}
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
// useCallback memoization is technically unnecessary as written here,
// but if i follow the linter's advice (listing handleSave as a dependency of the useEffect below), it also suggests memoizing here.
// However, complexState is also technically a dependency of this callback memo, which causes the fetch to trigger every time state changes.
//
// Similarly, moving all of this inside the effect hook, makes the hook dependent on `complexState`, which means the call to the backend happens every time a user changes input data.
const handleSave = React.useCallback(() => {
console.log('*** : start fetch');
setLoading(true);
dummySaveDraftToBackend(complexState).then((resp) => {
console.log('fetch response: ', resp);
// to keep this example simple, here we are just updating
// a dummy "responseCount", but in the actual implementation,
// I'm using a state reducer, and want to make some updates to form state based on error handling, backend validation, etc.
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState]);
// I know this triggers on mount and am aware of strategies to prevent that.
// Just leaving that behavior as-is for the simplified example.
React.useEffect(() => {
if (urlSlug) {
handleSave();
}
}, [urlSlug]); // <- React wants me to also include my memoized handleSave function here, whose reference changes every time state changes. If I include it, the fetch fires every time state changes.
return (
<div className="App">
<h2>the current slug is:</h2>
<h3>{urlSlug}</h3>
<div>the current state is:</div>
<pre>{JSON.stringify(complexState, null, 2)}</pre>
<div>
<h2>edit foo</h2>
<input value={complexState.foo} onChange={(e) => setComplexState((s) => ({ ...s, foo: e.target.value }))} disabled={loading} />
</div>
<div>
<h2>edit baz</h2>
<input value={complexState.baz} onChange={(e) => setComplexState((s) => ({ ...s, baz: e.target.value }))} disabled={loading} />
</div>
<div>
<button
type="button"
onClick={() => setUrlSlug(randomUrlSlug())}
disabled={loading}
>
click to change to a random URL slug
</button>
</div>
</div>
);
}
As written, this does what I want it to do, but I had to omit my handleSave function as a dependency of my useEffect to get it to work. If I list handleSave as a dependency, the hook then relies on complexState, which changes (and thus fires the effect) every time the user modifies input.
I'm concerned about violating React's guidance for not including dependencies. As-is, I would also need to manually prevent the effect from running on mount. But because of the warning, I'm wondering if I should not use a useEffect pattern for this, and if there's a better way.
I believe I could also manually read/write state to a ref to accomplish this, but haven't explored that in much depth yet. I have also explored using event listeners on browser popstate events, which is leading me down another rabbit hole of bugginess.
I know that useEffect hooks are typically intended to be used for side effects based on event behavior (e.g. trigger a fetch on a button click). In my use case however, I can't rely solely on user interactions with elements on the page, since I also want to trigger autosave behavior when the user navigates with their browser back/forward controls (I'm using react-router; current version of react-router has hooks for this behavior, but I'm unfortunately locked in to an old version for the project I'm working on).
Through this process, I realized my understanding might be a bit off on proper usage of hook dependencies, and would love some clarity on what the pitfalls of this current implementation could be. Specifically:
In my snippet above, could somebody clarify to me why ignoring the ESLint rule could be "bad"? Specifically, why might ignoring a dependency on some complex state can be problematic, especially since I dont want to trigger an effect when that state changes?
Is there a better pattern I could use here - instead of relying on a useEffect hook - that is more idiomatic? I basically want to implement a subscriber pattern, i.e. "do something every time a prop changes, and ONLY when that prop changes"
If all the "state" that is updated after saving it to backend is only a call count, declare this as a separate chunk of state. This eliminates creating a render loop on complexState.
Use a React ref to cache the current state value and reference the ref in the useEffect callback. This is to separate the concerns of updating the local form state from the action of saving it in the backend on a different schedule.
Ideally each useState hook's "state" should be closely related properties/values. The complexState appears to be your form data that is being saved in the backend while the responseCount is completely unrelated to the actual form data, but rather it is related to how many times the data has been synchronized.
Example:
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow' });
const [responseCount, setResponseCount] = React.useState(0);
const complexStateRef = React.useRef();
React.useEffect(() => {
complexStateRef.current = complexState;
}, [complexState]);
React.useEffect(() => {
const handleSave = async (complexState) => {
console.log('*** : start fetch');
setLoading(true);
try {
const resp = await dummySaveDraftToBackend(complexState);
console.log('fetch response: ', resp);
setResponseCount(count => count + 1);
} catch(error) {
// handle any rejected Promises, errors, etc...
} finally {
setLoading(false);
}
};
if (urlSlug) {
handleSave(complexStateRef.current);
}
}, [urlSlug]);
return (
...
);
}
This feels like a move in the wrong direction (towards more complexity), but introducing an additional state to determine if the urlSlug has changed seems to work.
export function MyComponent({ urlSlug, setUrlSlug }) {
const [slug, setSlug] = React.useState(urlSlug);
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
const handleSave = React.useCallback(() => {
if (urlSlug === slug) return // only when slug changes and not on mount
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState, urlSlug, slug]);
React.useEffect(() => {
if (urlSlug) {
handleSave();
setSlug(urlSlug)
}
}, [urlSlug, handleSave]);
Or move handleSave inside the useEffect (with additional slug check)
Updated with better semantics
export function MyComponent({ urlSlug, setUrlSlug }) {
const [autoSave, setAutoSave] = React.useState(false); // false for not on mount
React.useEffect(() => {
setAutoSave(true)
}, [urlSlug])
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
React.useEffect(() => {
const handleSave = () => {
if(!autoSave) return
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}
if (urlSlug) {
handleSave();
setAutoSave(false)
}
}, [autoSave, complexState]);

How can we get the updated props in the same function, in a react functional component?

I'm using redux to manage my state. After making an API call, I would update my redux store, the component would receive the updated props from redux and I would handle update the state based on the props.
With class components I currently have a method that does this:
onEdit = async () => {
if(!this.props.item) {
await this.props.fetchItem();
}
this.setState({
item: this.props.item
});
}
The updated props would be used in the setState.
Here is an example of something similar with a functional component:
const Component = (item) => {
...
const onEdit = async () => {
if(!item) {
await this.props.fetchItem();
}
setState(item) // this doesn't work
};
...
}
Obviously the above doesn't work since item uses the same props as before.
I recognize that useEffect is probably the solution most people would go for, but I was just wondering if there was a similar solution to the class component method above, since the syntax is very nice.
Put the item into state instead of props, and use props as the parameter to the Component instead of item:
const Component = (props) => {
const [item, setItem] = useState();
const onEdit = () => {
if(!item) {
props.fetchItem().then(setItem).catch(handleError);
}
};
// ...
};

Jest test case for UseEffect hooks in react JS

I am trying to write the Jest-enzyme test case for useEffect react hooks, and I am really lost, I want to write test case for 2 react hooks, one making the async call and another sorting the data and setting the data using usestate hooks, my file is here.
export const DatasetTable: React.FC<DatasetTableProps> = ({id, dataset, setDataset, datasetError, setDataSetError}) => {
const [sortedDataset, setSortedDataset] = useState<Dataset[]>();
useEffect(() => {
fetchRegistryFunction({
route:`/dataset/report?${constructQueryParams({id})}`,
setData: setDataset,
setError: setDataSetError
})();
}, [id, setDataset, setDataSetError]});
useEffect(() => {
if(dataset) {
const sortedDatasetVal = [...dataset];
sortedDatasetVal.sort(a, b) => {
const dateA: any = new Date(a.date);
const dateA: any = new Date(a.date);
return dataA - dateB;
}
setSortedDataset(sortedDatasetVal);
}
}, [dataset])
return (
<div>
<DatasetTable
origin="Datasets"
tableData={sortedDataset}
displayColumns={datasetColumns}
errorMessage={datasetError}
/>
</div>
);
}
Enzyme isn't the right library for this kind of testing.
https://react-hooks-testing-library.com/ is what you need.
In your case I would extract all the data fetching to a 'custom hook' and then test this independently from your UI presentation layer.
In doing so you have better separation of concerns and your custom hook can be used in other similar react components.
I managed to get enzyme to work with a data fetching useEffect hook. It does however require that you allow your dataFetching functions to be passed as props to the component.
Here's how I would go about testing your component, considering it now accepts fetchRegistryFunction as a prop:
const someDataSet = DataSet[] // mock your response object here.
describe('DatasetTable', () => {
let fetchRegistryFunction;
let wrapper;
beforeEach(async () => {
fetchRegistryFunction = jest.fn()
.mockImplementation(() => Promise.resolve(someDataSet));
await act(async () => {
wrapper = mount(
<DatasetTable
fetchRegistryFunction={fetchRegistryFunction}
// ... other props here
/>,
);
});
// The wrapper.update call changes everything,
// act seems to not automatically update the wrapper,
// which lets you validate your old rendered state
// before updating it.
wrapper.update();
});
afterEach(() => {
wrapper.unmount();
jest.restoreAllMocks();
});
it('should display fetched data', () => {
expect(wrapper.find(DatasetTable).props().tableData)
.toEqual(someDataSet);
});
});
Hope this helps!

Best practice to prevent state update warning for unmounted component from a handler

It is a common use-case to fetch and display the data from an external API (by using XHR requests) when a certain UI component (e.g. a <button />) is clicked. However, if the component was unmounted in the meantime, the following warning appears in the console:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
In fact, the most common solution (approved by #dan-abramov) to avoid the warning seems to keep track of the mount state of the component by using the return function of useEffect to cleanup.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
export default function PhotoList() {
const mounted = useRef(true);
const [photos, setPhotos] = useState(null);
useEffect(() => {
return () => {
mounted.current = false;
};
}, []);
function handleLoadPhotos() {
axios("https://jsonplaceholder.typicode.com/photos").then(res => {
if (mounted.current) {
setPhotos(res.data);
}
});
}
return (
<div>
<button onClick={handleLoadPhotos}>Load photos</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
However, this seems to cause unnecessary overhead to keep track of the mounting state and to check it before every state update. This becomes especially obvious when Observables (where you can unsubscribe) instead of Promises are used.
While you indeed can unsubscribe inside of the useEffect using the cleanup function in a very neat way:
useEffect(() => {
// getPhotos() returns an observable of the photo list
const photos$ = getPhotos().subscribe(setPhotos);
return () => photos$.unsubscribe();
}, []);
The same smart cleanup is not possible within a handler:
function handleLoadPhotos() {
const photos$ = getPhotos().subscribe(setPhotos);
// how to unsubscribe on unmounting?
}
Is there a best practice to avoid the warning without the ugly manual tracking of the mounting state with useRef()? Are there good approaches for that when using Observables?
Problem is that you are trying to fetch data in your component. This is not a good idea since the component could be unmounted and you would face many possible errors.
So that, you should look for other ways.
I always do async operations in redux thunks.
You should avoid your approach. Use redux and redux-thunk if you like. If not, try to find another solution to move async operations outside of your components.
In fact, you should be writing declarative ui components which renders for given props. So that, your data should be outside of your components logic too.
That's an awesome question! This is how I would do it:
First, define a helper function (it's not cheating because it really is a highly reusable function whenever you're dealing with React and observables combined):
import * as React from 'react';
import { Observable } from 'rxjs';
export const useObservable = <Value>(
arg: () => {
observable: Observable<Value>;
value: Value;
},
) => {
const { observable, value } = React.useMemo(arg, []);
const [state, setState] = React.useState<Value>(value);
React.useEffect(() => {
const subscription = observable.subscribe(value => setState(value));
return () => subscription.unsubscribe();
}, []);
return state;
};
Just to help illustrate what this function does, the following component will display the latest value emitted by myObservable:
() => {
const value = useObservable(() => ({
observable: myObservable,
value: 'Nothing emitted yet',
}));
return <span>{value}</span>;
};
Your component will then look like this:
export default function PhotoList() {
const clicksSubject = React.useMemo(() => new Subject<undefined>(), []);
const photos = useObservable(() => ({
observable: clicksSubject.pipe(
switchMap(() => axiosApiCallReturningAnObservable()),
map(res => res.data),
),
value: null,
}));
return (
<div>
<button
onClick={() => {
clicksSubject.next(undefined);
}}
>
Load photos
</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
When the component is dismounted, useObservable unsubs from the observable that was passed to it. This makes sure that we don't at a later point attempt to set the state, and that the data fetching API aborts (or at least gets a chance to abort) the HTTP request.

Resources