How do you dispose a reaction defined in a class constructor? - mobx

In some of my models I need to define a reaction in the constructor like this:
constructor() {
//...some code
const dispose = reaction(
() => this.items.length,
count => {
this.setItemCount(count);
}
);
}
I am using a reaction rather than a computed (#computed get itemCount()) because loading items into state is an expensive (lots of data over network) operation and so I need to persist the most recent value so that it can be used throughout the app. The reaction is to update the value if the count changes when the items are loaded into state.
So, with the above in mind, I'm wondering when/how I would dispose of the reaction? I want to avoid memory leaks. I'm open to alternative ways of accomplishing what I need although I would prefer a reactive vs imperative approach.

Three ways to go about this.
Dispose of reaction after it is done its job.
constructor() {
const dispose = reaction(
() => this.items.length,
(count, reaction) => {
this.setItemCount(count);
if(count === 100) reaction.dispose()
}
);
}
Some other action disposes it for you. Like a button click. Or another reaction. Whatever you need it to be, actually.
class myStore {
disposer
constructor() {
this.disposer = reaction(
() => this.items.length,
(count) => this.setItemCount(count)
);
}
myButtonClick = () => {
this.disposer()
}
}
Create a "deconstructor" method in your class that is meant to be called when you don't "need" this class/store anymore. You can use this method for dumping in anything that needs a cleanup before safely passing things to garbage collector.
class myStore {
disposers = []
constructor () {
this.disposers.push(reaction(
() => this.items.length,
(count, reaction) => {
this.setItemCount(count);
if(count === 100) reaction.dispose()
}
))
}
deconstructor() {
this.disposers.forEach((disposer) => disposer())
}
}
You are responsible for calling this deconstructor too. Typically you will be calling it on component unmount. Hook example below:
function Example() {
const [store] = useState(() => new myStore())
useEffect(() => {
return () => store.deconstructor()
}, [])
return <App/>
}
If store is global/context, you can call destructor in a frame component (a component that is always mounted in app's lifecycle), so it is run when user exits the app. I am not so sure, however, how needed is this step specifically for Mobx disposables, maybe someone can comment on this. Does not hurt to do it, though.
NB. Actually you should be doing number 3 at all times anyway, because it could be that due to some reasons, condition at 1. (or 2.) might not manage to trigger and you are left with unneeded reaction ticking in the background.

I'm using an array of disposables + specific method to dispose them.
It's looking like:
class MyClass {
...
disposables = [];
...
constructor () {
// constructor stuff
this.disposables.push(reaction(
() => this.items.length,
count => {
this.setItemCount(count);
}
))
}
...
disposeAll = () => {
this.disposables.forEach(dispose => dispose());
}
}
This method is not useful if you want to dispose specific reaction. But in this case you can you map instead of an array.

Related

Mobx observe only specific objects in array

I have a store with events
export class EventStore {
#observable
events: Event[] = [];
#action
addEvent(event: Event) {
this.events = [...this.events, event]
};
};
My Event look like this :
export class Event{
classType: string;
}
I want to observe change on events properties of store BUT only of a specific classType
For Eg :
I have Event with classType "AddToCart" and "Order", and I want to observe only "Order" added, removed from events
I want to use import { observer } from "mobx-react"
Question :
Is there some magic trick to do in the EventStore or I have to handle it in my component (with some if) ?
My unperfect solution
Meanwhile, I'm doing somehting like this :
reaction(
() => eventStore.lastEvent,
event => {
if (event.classType === "Order")
this.newEvent = { ...event }
}
)
You can add computed property
#computed
get orderEvents() {
// Add whatever filtering you want
return this.events.filter(event => event.classType === 'Order')
};
If you need to pass arguments you could you computedFn from mobx-utils:
filteredEvents = computedFn(function (classType) {
return this.events.filter(event => event.classType === classType)
})
Note: don't use arrow functions as the this would be incorrect.
https://github.com/mobxjs/mobx-utils#computedfn
https://mobx.js.org/refguide/computed-decorator.html#computeds-with-arguments

Fetching data as reaction to observable array change in MobX

Suppose we have an observable main object array, and observable data about that array (e.g. suppose we have selectedReports and reportParameters) . Now suppose we emit action to either add report to the array or remove report from that array. How do we run an action to fetch the data for reportParameters, as reaction?
Thus far, my attempt, which isn't working, looks like this:
// report parameters stuff
async fetchAllReportParameters() {
reaction(
() => this.selectedReports,
async (reports) => {
// reset the report parameters
this.reportParameters = {}
// fetch the parameters for all the reports
await reports
.forEach((report) => {
this.fetchReportParameters(report.Id)
})
}
)
}
/**
* fetches report parameters for a reportId
* #param {number} reportId
*/
fetchReportParameters = (reportId) => {
this.reportParameters[reportId] = []
const onSuccess = (reportParameters) => {
this.reportParameters[reportId] = reportParameters
}
this.api.GetReportParameters(reportId)
.then(onSuccess, this.fetchReportParametersError)
}
fetchReportParametersError = (error) => {
// TODO: output some error here
}
Are you ever actually calling fetchAllReportParameters? If you don't, the reaction will never be created. You may instead like to create the reaction from the constructor, assuming you always want it to be run. One example:
class SomeStore {
constructor() {
this.disposeReportsReaction = reaction(
() => this.selectedReports.slice(),
reports => {
// ...
}
)
}
}
Call storeInstanceName.disposeReaction() whenever you're done with the reaction.
Notice that I've used .slice() here. This is because if you simply pass the array reference, the reaction will never be called. See reaction docs: you have to actually use the value in some way.
You also need to tweak the async code a bit. This:
async (reports) => {
await reports.forEach((report) => {
// ...
})
}
won't do what you hope, because forEach returns undefined. Even if you shift the async keyword to the forEach callback, all the API requests will be sent in quick succession. Consider using something like this instead, depending on whether you want to wait for the preceding request before sending the next one:
try {
for (const report of reports) {
await this.fetchReportParameters(report.id)
}
} catch (e) {
// handle error
}
This isn't always the right answer: sometimes it's fine to send a bunch of requests in quick succession (perhaps especially if it's a small batch, and/or in the context of HTTP/2). If that's ok with you, you could use:
reports => {
// ...
reports.forEach(report => this.fetchReportParameters(report.id))
}

Store result from api call React native

How can I store the recipe name from this api call into a state variable or some other variable to be used further down in the render
{
this.props.recipeList.recipeList.filter((recipe) => recipe.recipeName === search).map(recipe => {
return <Recipe
name={recipe.recipeName}
numberOfServings={recipe.numberOfServings}
key={'recipe-' + recipe.recipeId}
/>
})
}
As this is performed within render, avoid modifying the component state.
I would recommend declaring a variable at the top of your render method and then assigning it in your map call. This, of course, assumes that filter will only ever return a single result.
Something like:
render() {
let name;
....
{
this.props.recipeList.recipeList.filter((recipe) => recipe.recipeName === search).map(recipe => {
name = recipe.recipeName; // this bit
return <Recipe
name={recipe.recipeName}
numberOfServings={recipe.numberOfServings}
key={'recipe-' + recipe.recipeId}
/>
})
}
....

Mobx Observable not triggering changes

I've been working to implement MobX into one of my classes, and I believe I'm close to have it working but I'd really appreciate if someone can point out where I'm going wrong here.
Essentially when the refreshJobs() function runs, I'd like the render() function to execute again. From my understanding, if I updated the observable object jobs, the computed functions (renderSubmittedJobs(), renderInProgressJobs()) would run again producing new values, then the render function would run again since those values have been updated.
However, what happens with this code is that it updates this.jobs (wrapped in an action), but neither of the computed functions execute - and I believe that is why render isn't ran again either.
Does anyone know what might be causing this issue? I really appreciate any direction with this.
#observer
export default class Jobs extends React.Component<ScreenProps<>> {
#observable jobs = {};
#computed get renderInProgressJobs() {
inProgressJobs = [];
for (key in this.jobs) {
if (jobs[key].status === "in progress") {
inProgressJobs.push(this.jobs[key]);
}
}
return this.renderJobComponents(inProgressJobs);
}
#computed get renderSubmittedJobs() {
submittedJobs = [];
for (key in this.jobs) {
console.log(key)
if (this.jobs[key].status !== "in progress") {
submittedJobs.push(this.jobs[key]);
}
}
return this.renderJobComponents(submittedJobs);
}
renderJobComponents(jobList: Array) {
return jobList.map((jobInfo, key) => {
return (
...
);
});
}
#observer
async refreshJobs() {
jobs = await grabClientJobs(refresh=true);
await runInAction("Updating Jobs", async () => {
this.jobs = jobs;
});
}
#observer
async componentWillMount() {
jobs = await grabClientJobs();
runInAction("Updating Jobs", async () => {
this.jobs = jobs;
});
}
#observer
render(): React.Node {
console.log('in jobs now');
return <BaseContainer title="Jobs" navigation={this.props.navigation} scrollable refresh={this.refreshJobs}>
<Tabs renderTabBar={()=> <ScrollableTab />} tabBarUnderlineStyle={style.secondaryBackground}>
<Tab heading="In Progress" textStyle={style.tabTextStyle} activeTextStyle={style.activeTabTextStyle}>
{ this.renderInProgressJobs }
<Button full style={[style.secondaryBackground, style.newJob]}>
<Text>CREATE NEW JOB</Text>
</Button>
</Tab>
<Tab heading="Submitted" textStyle={style.tabTextStyle} activeTextStyle={style.activeTabTextStyle}>
{ this.renderSubmittedJobs }
</Tab>
</Tabs>
</BaseContainer>;
}
}
few mistakes here:
you can't re-assign another value to the observable variable, it'll destroy the observable. You need to mutate the observable. For example, you can directly assign values to existing properties or use extendObservable for assigning new properties to observable object.
If you use MobX < 4, adding new properties the observable object will not trigger changes because the properties are set when the observable object was created. extendObservable may work but it's also limited. Use observable Map instead.
#observer should be used for component class (or SFC), not member functions inside of the class

How to make observable from multiple event in Rxjs?

How are you. I am newbie of Rxjs. I am not sure how to merge observable from different event. I integrated Rxjs with Vue.js
export default {
name: 'useraside',
data: function () {
return {
searchKey: '',
isPublic: true
}
},
components: {
User
},
subscriptions () {
return {
// this is the example in RxJS's readme.
raps: this.$watchAsObservable('searchKey')
.pluck('newValue')
// .filter(text => text.length > 1)
.debounceTime(500)
.distinctUntilChanged()
.switchMap(terms => fetchRaps(terms, this.userdata._id, this.isPublic))
.map(formatResult)
}
}
}
Now event comes from searchKey changes, now I would like to subscribe same observable when isPublic value change.
So I would like to get raps whenever searchKey changes or isPublic changes.
Thanks.
You could use the merge operator and keep using the this.isPublic in your switchMap, as Maxime suggested in the comment.
But I'd rather go with a nice a pure dataflow where you listen for the two values and consume them in your handlers. Something like
Rx.Observable.combineLatest(
this.$watchAsObservable('searchKey').pluck('newValue'),
this.$watchAsObservable('isPublic').pluch('newValue'),
([searchKey, isPublic]) => ({ searchKey, isPublic })
)
.dedounceTime(500)
.distinctUntilChanged()
.switchMap(({ searchTerm, isPublic }) => fetchRaps(searchTerm, this.userdata._id, isPublic))
Or event better is you can change the initial data structure to something like :
data: function () {
return {
searchConfig: {
searchKey: '',
isPublic: true
}
}
},
you can then remove the combineLatest and only watch the searchConfig property.
The benefit of this implementation is that your dataflow is pure and doesn't depend on any external context (no need for the this.isPublic). Every dependency is explicitly declared at the beginning of the dataflow.
If you want to go even further, you can also watch the userdata and explicitly pass it down the dataflow :)