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
Related
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.
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}
/>
})
}
....
Let's say that I have two forms, each related to a seperate mobx store. One form is for Client info (first name, last name etc), and the other for Employee info. Each form obviously has multiple inputs that update the observables in the related store.
In this example I have an action in each store that takes an event and based on the name, updates the value:
#action handleInputChange = (e) => {
this[e.target.name] = e.target.value
}
Is there a way to abstract this action into a helper file, something that would contain common actions, instead of retyping this again and again?
Thanks in advance, I'm pretty new to this as you can imagine.
There are several ways to handle the question. In my project, I just wrote an HOC(Higher-Order Component) to do that.
export default function asForm(MyComponent, formDataProp) {
return #observer class Form extends Component {
// constructor, etc.
updateProperty(key, value) {
this.props[formDataProp][key] = value;
}
// some other functions like double click prevention, etc.
render() {
return (
<MyComponent
{...this.props}
updateProperty={this.updateProperty}
// some other props
/>
);
}
};
}
Then use the HOC like this:
#observer
class UserForm extends Component {
render() {
const { updateProperty, userInfo } = this.props;
return (
<div className="form-wrapper">
<YourInputComponent
name="name"
updateProperty={updateProperty}
value={userInfo.name}
// other props
/>
</div>
);
}
}
UserForm.propTypes = {
userInfo: PropTypes.instanceOf(UserInfo),
updateProperty: PropTypes.func.isRequired,
};
export default asForm(UserForm, 'userInfo');
I am not sure if this solution violates the rule that you should not assign values to props.
I'm just getting started with Mobx in a react-native project and am having trouble understanding how to perform changes on a observed object.
Changing the object reference via the setWorkingObject action function in my store properly renders the UI, however if I just want to change a single property within this object, how do I cause a render?
My "store":
export default class MyStore {
constructor() {
extendObservable(this, {
workingObject: null
});
}
}
My "container":
class Container extends Component {
render() {
return (
<Provider store={new MyStore()}>
<App />
</Provider>
);
}
}
and my "component", which uses a simple custom input component (think of it like Checkbox) to perform changes to a property of my workingObject
class MyClass extends Component {
...
render() {
const {store} = this.props;
return
<View>
...
<RadioGroup
options={[
{ title: "One", value: 1 },
{ title: "Two", value: 2 }
]}
onPress={option => {
store.workingObject.numberProperty = option.value;
}}
selectedValue={store.workingObject.numberProperty}
/>
...
</View>
}
}
export default inject("store")(observer(MyClass));
I can't figure out why this doesn't work, in fact it looks very similar to the approach used in this example
Any other tips/critique on how I've implemented mobx welcome
The problem is that only existing properties are made observable at the time the workingObject is first assigned.
The solution is to declare future properties at the time of assignment, ie:
// some time prior to render
store.workingObject = { numberProperty:undefined };
First, you don't want to set initial value to null. Second, adding properties to observable object after it was created will not make added properties observable. You need to use extendObservable() instead of assigning new properties directly to observable object. Another solution is to use observable map instead.
in your store:
extendObservable(this, {
workingObject: {}
});
in your component:
extendObservable(store.workingObject, {numberProperty: option.value});
I recommend using Map in this case:
extendObservable(this, {workingObject: new Map()});
in your component:
store.workingObject.set(numberProperty, option.value);
I don't know how to pass a reference to the TripList instance below to the AddTrip component. I need to do something like that to signal to TripList to refresh the data after adding a new trip.
In my render() method, inside <Navigator> I have:
if (route.index === 1) {
return <TripList
title={route.title}
onForward={ () => {
navigator.push({
title: 'Add New Trip',
index: 2,
});
}}
onBack={() => {
if (route.index > 0) {
navigator.pop();
}
}}
/>
} else {
return <AddTrip
styles={tripStyles}
title={route.title}
onBack={() => { navigator.pop(); }}
/>
}
However, when I call onBack() in AddTrip, after adding a trip, I want to call refresh() on TripList so the new trip is displayed. How best can I structure things to do that? I'm guessing I need to pass TripList somehow to AddTrip and then I can call refresh() there easily right before calling onBack().
This is not how React works. You don't pass instances of a component around, rather you pass the data to your component via props. And your component AddTrip should receive another props which is a function to call when adding a trip.
Let me illustrate this with a code example, this is not how your code should be in the end, but it'll illustrate how to contain the data outside of your components.
// Placed at the top of the file, not in a class or function.
let allTrips = [];
// Your navigator code.
if (route.index === 1) {
return <TripList
trips={allTrips}
title={route.title}
onForward={ () => {
navigator.push({
title: 'Add New Trip',
index: 2,
});
}}
onBack={() => {
if (route.index > 0) {
navigator.pop();
}
}} />
} else {
return <AddTrip
styles={tripStyles}
title={route.title}
onAdd={(tripData) => {
allTrips = [...allTrips, tripData];
}}
onBack={() => { navigator.pop(); }} />
}
As you can see, the logic about adding and finding the trips comes from the parent component, which is the navigator in this case. You will also note that we are reconstructing the content of allTrips, this is important as React is based on the concept of immutability.
You must have heard of Redux which is a system allowing all your components to discuss with a global store from which you fetch and save all your application state. It's a bit more complex that's why I did not use it as an example it.
I'll almost forget the most important! You will not need to signal to to your component that it needs refreshing, the magic of React should take care of it by itself!