Сan I use the Mobx library to call a class method when data changes?
For example MyObject writes container['item'] = 10 and as a result the myaction method is called.
class MyElement extends Component<any> {
// modifiable data
container: any = [];
// method called when data (container) is modified
myaction() {
console.log('container was modified');
console.log(this.container);
}
render() {
<MyObject container = {this.container} />
}
}
decorate(MyElement, {
container: observable
} as any)
You could use reaction for example:
container = [];
componentDidMount() {
// save disposer function to use later inside componentWillUnmount
this.reactionDisposer = reaction(
() => this.container.length,
() => {
console.log('container was modified')
}
);
}
// Don't forget to dispose it when unmount
componentWillUnmount() {
this.reactionDisposer();
}
Codesandbox link: https://codesandbox.io/s/httpsstackoverflowcomquestions63864175-kjorh?file=/src/App.js
Also, technically you can do that with array container['item'] = 10, but I advise you not to use string keys with array. If you want to use string keys then you need to use object or a Map.
Other methods you could also use to achieve what you want:
Autorun - https://mobx.js.org/refguide/autorun.html
When (basically single use reaction) - https://mobx.js.org/refguide/when.html
Or lower level stuff like observe and intercept - https://mobx.js.org/refguide/observe.html
Related
Today I started using MobX and the first problem I ran into is how to execute a function in a React class component whenever an Observable updates.
I am under the impression this can be achieved using a reaction, but I'm not sure how to make it work.
class MissionLog {
private _missions: Array<IMissionItem> = [];
public get missions() {
return this._missions;
}
constructor() {
makeAutoObservable(this);
}
// Example of a method that modifies the _missions array
public receiveMission(mission: IMissionItem) {
this._missions.push(mission);
}
}
export const missionLog = new MissionLog();
// Example of modifying the missions array
missionLog.receiveMission(someMission);
export const ObserverTest = observer(class _ObserverTest extends React.Component {
constructor(props: any) {
super(props);
// Executes the console.log at the start,
// but not when missionLog.missions changes.
autorun(() => {
console.log("Autorun", missionLog.missions);
})
// Never executes the console.log
reaction(
() => missionLog.missions,
(mission) => {
console.log("Reaction");
}
)
}
render() {
return (
// Accessing missionLog.missions here
// gives me the correct, updated data,
// so my setup should be fine.
)
}
});
I also tried to use intercept and observe instead of reaction, but also no result.
I am accessing JSON file in ComponentDidMount in class A, i need to access that result outside class and need to use that in Class B
let test;
console.log(test);
class CustomerPage extends React.Component {
componentDidMount(): void {
$.getJSON("/api/LocaleStrings")
.done(results => {
let JsonString = JSON.parse(results);
test = new LocalizedStrings(JsonString);
})
.fail(console.log.bind(console));
}
}
Here, console.log(test) yields undefined.
It seems to me that your console.log(test) gets executed before the AJAX call returns, and at that point it will be uninitialized (undefined). Place your console.log inside the done function.
You could store your AJAX result in your component's state:
class CustomerPage extends React.Component {
constructor(props) {
super(props);
this.state = { test: null };
}
componentDidMount(): void {
$.getJSON("/api/LocaleStrings")
.done(results => {
let JsonString = JSON.parse(results);
this.setState({
test: new LocalizedStrings(JsonString);
});
})
.fail(console.log.bind(console));
}
}
You need to have an "event" that notifies anyone who is interested that test is available:
interface CustomerPageProps {
onLocaleStringsLoaded?: (test:object) => void
}
class CustomerPage extends React.Component<CustomerPageProps> {
static defaultProps {
onLocaleStringsLoaded: () => {} //nothing by default
}
componentDidMount(): void {
$.getJSON("/api/LocaleStrings")
.done(results => {
let JsonString = JSON.parse(results);
const test = new LocalizedStrings(JsonString);
this.props.onLocaleStringsLoaded(test);
}).fail(console.log.bind(console));
}
}
Then at some point in your code you could have:
<CustomerPage onLocaleStringsLoaded={window.console.log.bind(window.console)} />
which will print to the console once the result is available.
I recommend reading up a bit more on how React components share data. The component that needs the data can have an input defined, in which you can pass the test variable. Or using a redux store (which could potentially be a little too complex for your application). If you really want to continue this route. You can always use the window object to set a global variable: window.test = 'bla';. This is available anywhere in the application with console.log(window.test);.
You would have to update your code to:
window.test = new LocalizedStrings(JsonString);.
Verifying that it is set can be done with an interval:
setInterval(function() {
console.log(window.test);
}, 100);
Suppose I have a container component for handling app logic which has a lot of methods:
class ScreenContainer extends React.Component
{
state = {
inputs: { /* various properties for input values */ },
thingyActive: false,
someList: ["thing1", "thing2"],
// ...etc.
};
handleInputChange = e => {
const { name, value } = e.target;
this.setState(prevState => ({
inputs: { ...prevState.inputs, [name]: value }
}));
};
toggleThingy = () => this.setState(prevState => ({
thingyActive: !prevState.thingyActive
}));
coolMethod = () => { /* Do cool stuff */ };
boringMethod = () => { /* Do boring stuff */ };
// ...more methods...
}
I need ALL of these methods to be accessible to inner components. I'll use a Context provider in this example, and we'll just say that the context gets consumed by various nested presentational components making up a screen in the application.
const ScreenContext = React.createContext();
To pass methods either down to a child component or into a context provider value, it seems you always end up having to do something like below (note that I'm lifting the "actions" into state in this example per the advice given in the React documentation).
class ScreenContainer extends React.Component
{
constructor()
{
super();
this.state = {
// ...same state as before, plus:
actions: {
handleInputChange: this.handleInputChange,
toggleThingy: this.toggleThingy,
coolMethod: this.coolMethod,
boringMethod: this.boringMethod,
everySingleOtherMethod: this.everySingleOtherMethod,
// ...on and on
}
};
}
// ...same methods as before...
render()
{
return (
<ScreenContext.Provider value={this.state}>
{this.props.children}
</ScreenContext.Provider>
);
}
I was looking for a way to avoid passing them all one by one. A possible solution I found involves using a getter and looping through the class instance properties like so:
get allMethods()
{
let output = {};
for (var prop in this)
{
if (this.hasOwnProperty(prop) && typeof this[prop] === "function")
output[prop] = this[prop];
}
return output;
}
Then I can just do:
// (in ScreenContainer constructor)
this.state = {
// ...state,
actions: this.allMethods
};
The getter code could also be extracted out into a utility function for reuse in other container-type components if needed. Obviously, this is only worthwhile if there are a ton of methods to be passed down.
It seems simple enough and appears to work just fine as long as it's done in the contructor. Is there anything crazy about this? Is it bad practice in any way, or does it have any potential side effects I'm not aware of? Is there maybe a better way I'm missing?
EDIT
I've updated the example to be closer to my real code; it now shows what kinds of things the methods might do and uses a Context setup rather than passing the methods down as props to a single child component.
If a class doesn't maintain a state, and class methods are supposed to be used separately as helper functions, they shouldn't be a part of the class, let alone class component. A class acts as namespace in this case. In modern JavaScript, modules are used as namespaces. It can be:
export const coolMethod = () => { /* Do cool stuff */ };
export const coolerMethod = () => { /* Do even cooler stuff */ };
export const boringMethod = () => { /* Do boring but necessary stuff */ };
ScreenContainer component is an example of 'smart' container component. It's always preferable to list passed functions explicitly rather than pass them all automatically. ScreenContainer may get private methods at some point. And there should be a guarantee that lifecycle hooks won't be passed accidentally, too.
If it is supposed to have a single child, it can be applied as higher-order component:
const withScreen(Comp) => {
return class ScreenContainer extends React.Component {
...
render() {
return <Comp handleInputChange={this.handleInputChange} /* ... */ />;
}
}
}
In this particular case render can be distinguished from passed functions because the latter are instance methods (arrow functions). While this kind of magic generally isn't recommended because it may cause problems and won't work properly for private methods, it can be shortened to:
render() {
const fns = {};
for (const method of Object.keys(this)) {
if (typeof this[method] === 'function')
fns[method] = this[method];
}
return <Comp {...fns} {...this.props} />;
}
For multiple children, ScreenContainer children could be traversed to add props in a similar way.
For indirect children, context API can be used to pass functions.
While it's possible to pass ScreenContainer this to children, this isn't recommended because this breaks the encapsulation and contradicts the principle of least privilege.
One way I've done this is to instantiate a new instance in the constructor of the child component like this:
class ChildComponent extends Component {
constructor(props) {
super(props);
this.Container = new MyContainer();
}
Then you can use any methods like:
this.Container.coolMethod()
Edit
I misunderstood. I've only done this by creating a helper class that you instantiate, not a component. It is helpful when you have methods you want to use in multiple components without having to pass all your methods as props through the component tree.
I have a couple of React components that are all based of the same base class, these component have a couple of properties which I would like to read before I render the component. This has to do with some conditions that are used somewhere else.
Currently I am calling a method, with something like this in my Render function.
public getWidget(): JSX.Element {
let widget = null;
switch (widgetType) {
case 'widget1': {
widgetComponent = new Widget1(props); // private variable in my class
widget = (<Widget1 { ...props } ref = { some-ref });
}
case 'widget2': {
widgetComponent = new Widget2(props); // private variable in my class
widget = (<Widget2 { ...props } ref = { some-ref });
}
}
return widget;
}
This way I can ask the widget some stuff about it's default values and render the widget variable in my Render function, like this:
render() {
const widget = this.getWidget();
const somethingIWantToKnow = this.widgetComponent.someProperty;
return ({ widget });
}
From what I understand, the reference I set for my React Component is only available after I render? Otherwise I could just use that.
I also tried calling this.widgetComponent.render() in my own Render method, but this does not set up the component correctly (probably because of missing componentWillMount and componentDidMount calls.
I just can't believe this is the way to go, is there a way to render from this.widgetComponent in my Render method, or is there a way to get properties from the class behind a JSX.Element?
NULL checks and other stuff is all removed from these code snippets :)
Give your widget a ref,
widget = (<Widget1 { ...props } ref = { widget1 } />);
Then you can access your instantiated component in componentDidMount and use the ref to access the property,
componentDidMount(){
const somethingIWantToKnow = this.widget1.current.someProperty
}
I tried to push a value into a state array but I get an issue TypeError: Cannot read property 'state' of undefined at this.state.rows.push(a);
Know why? I am trying to push a new value into the array after i click a button.
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
constructor() {
super();
this.state = {
name: '',
rows: ['hello',<p>gfdfg</p>,'mello']
}
}
handle(e){
e.preventDefault();
var a = "h";
this.state.rows.push(a);
alert("hello");
}
render() {
return (
<div className="App">
Button<br/>
<input type="submit" id="black" onClick={this.handle}/><br/>
<p>{this.state.rows}</p>
</div>
);
}
}
export default App;
There are couple of things that are wrong here:
you should NEVER change the state directly:
This is a big No No:
this.state.rows.push(a);
instead you should do something like this:
this.setState({ rows : [...this.state.rows, a] })
or without ES6:
const newArray = this.state.rows.slice();
newArray.push(a);
this.setState({ rows: newArray })
You should always replace the state with a new one.
this in a react component is not what you think it is, in order to make it work you can do one of two things:
a. change your method to an arrow function:
handle = (e) => {
e.preventDefault();
var a = "h";
this.state.rows.push(a);
alert("hello");
}
b. bind this to the method:
constructor() {
super();
this.state = {
name: '',
rows: ['hello',<p>gfdfg</p>,'mello']
}
this.handle = this.handle.bind(this);
}
the method handle does not have access to the context of the class i.e this; consider writing it as a fat arrow function
// class definition
handle = () => {
e.preventDefault();
var a = "h";
this.state.rows.push(a);
alert("hello");
}
render() {
// render logic
}
Having said this, mutating the state is not a good idea, consider using setState if you want your component to re-render as a result of state change
handle = () => {
e.preventDefault();
let { rows } = this.state;
var a = "h";
rows.push(a);
this.setState({
rows,
});
}
You are doing wrong, you have to use setState() method to push the value in the array:
handle = (e) => {
e.preventDefault();
var a = "h";
let tempRows = [...this.state.rows];
tempRows.push(a)
this.setState({rows:tempRows})
alert("hello");
}
You have two problems.
Event handlers require 'this' to be bound: https://reactjs.org/docs/handling-events.html So following this, you must either write: this.handle = this.handle.bind(this) in your contructor, or change handler to arrow function, if your build process supports transpilation of class fields.
React component will only update if component props change, or component state changes. Which is done by comparing references. In your case, when you push to the array, you are mutating the state, so the new reference is never created, and component does not re-render. If you want to verify that, just put console.log(this.state.rows) below this.state.rows.push(a) and you'll see that the array has received the new value, but component does not represent it. You need to use this.setState to create a new reference for your state, like so: this.setState({ rows: [...this.state.rows, a] })
Another way of returning a new array from the current array with additional elements and then pushing to state is to use the concat method.
Example :
this.setState({ users: this.state.users.concat(<Additonal Items Here>)}