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.
Related
I'm currently refactoring a FreeCodeCamp repo as way to learn mobx. What I'm trying to do is calculate this.store.studentCount as you can see in the StudentModal Component. Take a look here:
This is my /client/src/components/students/Modal.js
#observer
#inject('StudentModal')
export default class StudentModal extends Component {
store = new this.props.StudentModal()
renderStudentCount() {
let message
if (this.store.studentCount > 1) {
message = `${this.store.studentCount} students`
} else {
message = `${this.store.studentCount} student`
}
return <div id="student-count">{message}</div>
}
render() {
return (
<div className="AddStudentForm">
<div className="class-table-button-container">
<Button
onClick={this.open}
>
Add Student
</Button>
{this.renderStudentCount()}
</div>
.....
)
}
}
Taking a look at my models for the Modal component you can see I need to fetch a service to get the length of this but for whatever reason I cannot set the studentCount to a new value.
This is my /client/src/models/students/Modal.js
import { observable, action } from 'mobx'
import StudentsService from '../../services/StudentsService'
export default class StudentModal {
#observable open = true
#observable studentCount = 0
#action
fetchStudents() {
StudentsService.fetchStudents().then(response => {
const studentCount = response.body
this.studentCount = studentCount.length
})
}
}
You can take a look at the full source code here: https://github.com/imcodingideas/classroom-mode/tree/mobx-migration which I should remind you that this is open-source.
Am I doing this correctly? Do you have any feedback for me?
There seem to be some minor things:
Identical class names
This might lead to problems since, both your store and react component are called StudentModal
decorator order
as #Joseph suggested swap the order around your class:
#inject("StudentModal")
#observer
export default class StudentModal
State management
store = new this.props.StudentModal()
On creation of every StudentModal you seem to create a new state store. Normally te store is instantiated once (unless you really want seperate stores per modal) inside your entry point and then used later on:
import { render } from "react-dom";
import { Provider } from "mobx-react";
var stores = { StudentModal: new StudanModalStore() }
render(
<Provider {...stores}>
<StudentModal />
</Provider>,
rootEl,
);
#observer
#inject('StudentModal')
export default class StudentModal extends Component {
//used getter instead of setting once
// no longer use `new` but directly reference instance of the store.
get store (): StudentModalStore { return this.props.StudentModalas; }
}
above code is in typescript.
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
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);
Consider I have a dump component, use for display user's information.
There're 2 ways to pass user information to component:
1):
class UserItem extends PureComponent {
// pass user's properties
const {
userName, userEmail,
} = this.props;
// same
render() {
return (
<View>
<Text>{userName}</Text>
<Text>{userEmail}</Text>
</View>
);
}
}
2):
class UserItem extends PureComponent {
// pass a user object
const {
userName, userEmail,
} = this.props.user;
// same
render() {
return (
<View>
<Text>{userName}</Text>
<Text>{userEmail}</Text>
</View>
);
}
}
So which way I should use? I will list down some pros & cons that I know in 2 ways:
1)
pros:
the logic is very easy to understand, there're no "magic" things happen, the component display whatever you pass in.
can use the power of PureComponent by shallow compare state & props (https://reactjs.org/docs/shallow-compare.html) so that the component only get re-render whend needed
cons:
I have to typing many parameters passing, like <UserItem userName={user.name} userEmail={user.email}> (unless you use spreading operator ...user, but you will pass all the object properties)
I cannot use object method inside the component. for example, my User model has a method user.totalMoneys(), because there're some lazy calculated properties in my object.
2)
pros:
passing looks simple: <UserItem user={user}>
can use object method inside UserItem
cons:
I cannot use the benefit of PureComponent, I have to compare my own,
I thing the best to go is the 1, i often see the spread operator used for this, btw you can do this in the parent to avoid passing useless props :
class UserParent extends Component {
const { uselessProp1, uselessProp2, ...usefullProps } = this.props;
render() {
return (
<View>
<UserItem {...usefullProps} />
</View>
);
}
}
How can I access variable bvar in the code below? Also, when would I declare variables as:
a) state
b) between constructor() and render()
c) inside render() - my understanding is that I'd set them here if a variable can change and I'd like to set it each time a component renders. So if I know something is not changing at all, it'd be a const and where would I set it?
import React, {Component} from 'react';
export default class App extends Component {
constructor(props) {
super();
// Set the initial grid in
this.state = {
value: 4,
xsquares: 10,
ysquares: 10
};
var bvar = "cat";
}
render() {
var avar = [
"Hydrogen",
"Helium",
"Lithium",
"BerylÂlium"
];
let cvar = "dog";
return (
// Add your component markup and other subcomponent references here.
<div>
<h1>Hello, World! {this.state.value}</h1>
<h2>{this.state.xsquares}</h2>
<h3>{avar[0]}</h3>
<h4>{this.bvar}</h4>
<h3>{cvar}</h3>
</div>
);
}
}
All variables display apart from bvar.
bvar declared inside your constructor is not accessible inside render() method. It is out of scope. As answered by Caleb, you would need to use instance variable: this.bvar = "cat"
When would I declare variables as:
a) state
Use state if changes in data should affect the view (e.g. store user location in state so that current temperature can be established and rendered based on this location). Also, state can be used in the logic found in other methods of your component (e.g. fetch background image based on user's current location).
b) between constructor() and render()
Variables declared inside other methods of your component are often used to temporarily store data coming, for example, from the state, props, input fields etc. These variables are only accessible within those methods, e.g.
constructor() {
...
}
onInputText() {
var accountNumber = this.refs.inputField.value;
this.props.handleInputText(accountNumber);
}
render() {
...
}
c) inside render()
Variables are often declared inside render() to temporarily store data held in state or props. These variables are only accessible inside render(), e.g.
class WelcomeScreen extends React.Component {
render() {
var userName = this.props.userName;
return (
<div>
Hello, { userName }!
</div>
);
}
}
To define bvar within the constructor you would need to declare it as
this.bvar = "cat"
import React, {Component} from 'react';
export default class App extends Component {
constructor(props) {
super();
// Set the initial grid in
this.state = {
value: 4,
xsquares: 10,
ysquares: 10
};
this.bvar = "cat";
}
render() {
var avar = [
"Hydrogen",
"Helium",
"Lithium",
"BerylÂlium"
];
let cvar = "dog";
return (
// Add your component markup and other subcomponent references here.
<div>
<h1>Hello, World! {this.state.value}</h1>
<h2>{this.state.xsquares}</h2>
<h3>{avar[0]}</h3>
<h4>{this.bvar}</h4>
<h3>{cvar}</h3>
</div>
);
}
}