Why is my call to (mobx) extendObservable not triggering a re-render? - javascript

Here is the code -- pretty sure it is something about extendObservable that I just don't get, but been staring at it for quite a while now. When addSimpleProperty runs, it seems to update the object, but it doesn't trigger a render.
const {observable, action, extendObservable} = mobx;
const {observer} = mobxReact;
const {Component} = React;
class TestStore {
#observable mySimpleObject = {};
#action addSimpleProperty = (value) => {
extendObservable(this.mySimpleObject, {newProp: value});
}
}
#observer
class MyView extends Component {
constructor(props) {
super(props);
this.handleAddSimpleProperty = this.handleAddSimpleProperty.bind(this);
}
handleAddSimpleProperty(e) {
this.props.myStore.addSimpleProperty("newpropertyvalue");
}
render() {
var simpleObjectString =JSON.stringify(this.props.myStore.mySimpleObject);
return (<div>
<h3> Simple Object</h3>
{simpleObjectString}
<br/>
<button onClick={this.handleAddSimpleProperty}>Add Simple Property</button>
</div>);
}
}
const store = new TestStore();
ReactDOM.render(<MyView myStore={store} />, document.getElementById('mount'));
store.mySimpleObject = {prop1: "property1", prop2: "property2"};

This problem is brought up in the Common pitfalls & best practices section of the documentation:
MobX observable objects do not detect or react to property assignments
that weren't declared observable before. So MobX observable objects
act as records with predefined keys. You can use
extendObservable(target, props) to introduce new observable
properties to an object. However object iterators like for .. in or
Object.keys() won't react to this automatically. If you need a
dynamically keyed object, for example to store users by id, create
observable _map_s using
observable.map.
So instead of using extendObservable on an observable object, you could just add a new key to an observable map.
Example
const {observable, action} = mobx;
const {observer} = mobxReact;
const {Component} = React;
class TestStore {
mySimpleObject = observable.map({});
#action addSimpleProperty = (value) => {
this.mySimpleObject.set(value, {newProp: value});
}
}
#observer
class MyView extends Component {
constructor(props) {
super(props);
this.handleAddSimpleProperty = this.handleAddSimpleProperty.bind(this);
}
handleAddSimpleProperty(e) {
this.props.myStore.addSimpleProperty("newpropertyvalue");
}
render() {
var simpleObjectString = this.props.myStore.mySimpleObject.values();
return (
<div>
<h3> Simple Object</h3>
{simpleObjectString.map(e => e.newProp)}
<br/>
<button onClick={this.handleAddSimpleProperty}>Add Simple Property</button>
</div>
);
}
}
const store = new TestStore();
ReactDOM.render(<MyView myStore={store} />, document.getElementById('mount'));

Related

Change props value outsite react and reload component

As the title says, I want to change value of props and reload component in external js file.
<div data-group=""></div>
//external.js
const popupChat = document.getElementById('popupChatComponent');
popupChat.setAttribute('data-group', groupId);
//component.ts
export default class PopupChatRoot extends React.Component {
private readonly groupId: string;
constructor(props) {
super(props);
this.groupId = this.props.group;
}
render() {
return (
<div className="modal-body">
<p>{this.groupId}</p>
</div>
);
}
}
const component = document.getElementById('popupChatComponent');
if (component) {
const props = Object.assign({}, component!.dataset);
render(<PopupChatRoot {...props}/>, component);
}
How I can do this ?
What you can do is use a wrapper component or higher order component which provides those props to your component, and have that have that wrapper component integrated with your external javascript code.
Here is an HOC I use to do something similar:
export interface MyHocProps {
//the props you want to provide to your component
myProp: any;
}
export const withMyHOC = <T extends any>(params: any) =>
<P extends MyHocProps>(WrappedComponent: React.ComponentType<P>): React.ComponentClass<defs.Omit<P, keyof MyHocProps>> => {
return class extends React.PureComponent<defs.Omit<P, keyof MyHocProps>> {
//here you have access to params, which can contain anything you want
// maybe you can provide some sort of observable which causes this to re-render
render() {
return <WrappedComponent
{...this.props}
myProp={/*whatever*/}
/>;
}
}
};
From here, you would integrate this HOC with some kind of system to push changes to it. I recommend using an observable. Basically you want to have this HOC component subscribe to changes in some piece of observable data, and then force itself to re-render when it changes.
Alternatively, you can just expose some method on your component if it is just a singleton by doing something like window.reloadThisComponent = this.reload.bind(this);, but that should probably be considered a last resort.
It is just a generic example, it might help you to solve your problem. Actually I don't think you can change props of the root node.
// yourEventService.js
class YourEventService {
listeners = []
subscribe = listener => this.listeners.push(listener)
unsubscribe = listener => this.listeners = this.listeners.filter(item => item !== listener)
emit = message => listener.forEach(listener => listener(message))
}
export default new YourEventService() // singleton export
// someWhereElse.js
import yourEventService from './yourEventService'
window.addEventListener('click', () => yourEventService.emit('myNewGroup')) // it's just an event example
//component.js, sorry I don't know how to typescript well
import yourEventService from './yourEventService'
export default class PopupChatRoot extends React.Component {
state = {
groupId: this.props.group; // initial value is passed by props
}
componentDidMount() {
yourEventService.subscribe(this.handleMessage)
}
componentWillUnmount() {
yourEventService.unsubscribe(this.handleMessage)
}
handleMessage = message => {
this.setState({ groupId: message })
}
render() {
return (
<div className="modal-body">
<p>{this.state.groupId}</p>
</div>
);
}
}
const component = document.getElementById('popupChatComponent');
if (component) {
const props = Object.assign({}, component.dataset);
render(<PopupChatRoot {...props}/>, component);
}

How should I implement lazy behaviour with MobX?

The following code is intended to print a reversed list of users as soon as a new user is added, but it doesn't work. The autorun is listening to a lazy calculated var (_userArrayRev), but how to enable the recalculation of that var? The autorun is executed only once, while I expect it to be run three times
And, why does MobX allow me to modify the observable userArray var in AddUser() when enforceactions (useStrict) is set to true?
import { useStrict, configure, autorun } from 'mobx';
import { observable, action, computed } from 'mobx';
configure({ enforceActions: true });
class Test {
#observable _userArray = [];
#observable _userArrayRev = undefined;
userCount = 0;
addUser() {
console.log("Adduser");
this._userArray.push('user' + this.userCount);
this.invalidateCache();
}
// returns reversed array
getUsersArrayRev() {
if (this._userArrayRev == undefined) {
// console.log("recalculating userArray");
// TODO: should be reversed
this._userArrayRev = this._userArray;
}
return this._userArrayRev;
}
invalidateCache() {
this._usersArrayRev = undefined;
}
}
var t = new Test();
autorun(function test () {
console.log("users: ", t.getUsersArrayRev());
});
t.addUser();
t.addUser();
I recommend you use computed instead of autorun. computed is more suitable in the case when you want to create readonly lazy variable based on observable objects.
Notice: I use slice() to return a normal array. Observable array is an object rather than an array, be careful of that.
import React from 'react';
import { render } from 'react-dom';
import { observer } from 'mobx-react';
import { observable, action, computed } from 'mobx';
class Test {
#observable _userArray = [];
#computed get userCount() {
return this._userArray.length;
}
#computed get usersArrayRev() {
return this._userArray.slice().reverse();
}
#action
addUser() {
console.log("Adduser");
const id = this.userCount + 1;
this._userArray.push(`user${id}`);
console.log("Reversed users: ", this.usersArrayRev);
}
}
#observer
class App extends React.Component {
t = new Test();
render() {
return (
<div>
{this.t.usersArrayRev.map(user => <div key={user}>{user}</div>)}
<button onClick={() => { this.t.addUser(); }}>Add user</button>
</div>
);
}
}
Code demo here:

Pass data from react.js Store to Component in a clean way following flux pattern

following the Flux pattern I'm trying to update my component and pass some values (a string and a boolean in this specific case) via the store.
I could not find any non-hacky way to solve this yet i.e. using global vars in the Store and use a getter function in the Store which is called from the component on ComponentWillMount(), not a nice solution.
Here's a stripped down code example to show what im trying to achieve:
ExampleStore.js
import AppDispatcher from '../appDispatcher.jsx';
var displayimportError = false;
var importedID = '';
import axios from 'axios';
class ExampleStore extends EventEmitter {
constructor() {
super();
}
importId(id) {
let self = this;
// fetch data from BE
axios.get('foo.com').then(function(response) {
if (response.data && response.data.favoriteEntries) {
displayimportError = false;
}
self.emitChange();
}).catch(function(error) {
console.log(error);
displayimportError = true;
importedID = id;
self.emitChange();
// now update component and pass displayimportError and
// importedID.
// best would to component.receiveUpdateFromStore(param); but
// it's giving receiveUpdateFromStore is not function back
});
}
}
var favObj = new ExampleStore();
AppDispatcher.register(function(payload) {
var action = payload.action;
switch (action.actionType) {
case 'UPDATE_ID':
favObj.importId(action.data);
break;
}
return true;
});
export default favObj;
As mentioned in the Comment above the best solution in my eyes so far would be to call a function in the component from the store i.e component.receiveUpdateFromStore(param); and then update the component state within that function but even though they seem to be im/exported correctly to me it is returning receiveUpdateFromStore is undefined.
Any other idea how to solve this is appreciated.
//example component
import React from 'react';
import ReactDom from 'react-dom';
import ExampleStore from '../stores/ExampleStore.jsx';
class ExampleComponent extends React.Component {
constructor(props) {
super(props);
}
receiveUpdateFromStore(param) {
this.setState({'exampleText': param.text, 'exampleBoolean': param.bool});
}
render() {
return <div className="foo">bar</div;
}
}
export default ExampleComponent;
Any idea how to pass data from store to a component and update component state in a nice way?
I would hang your store state on the store class instance itself -- something like this.state.displayimportError = true -- and then have the component subscribe to the store:
import React from 'react';
import ExampleStore from '../stores/ExampleStore.jsx';
class ExampleComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
importError: ExampleStore.state.displayimportError,
};
}
componentWillMount() {
ExampleStore.on( 'change', this.updateState );
}
componentWillUnmount() {
ExampleStore.removeListener( 'change', this.updateState );
}
updateState = () => {
this.setState( state => ({
importError: ExampleStore.state.displayimportError,
})
}
render() {
return <div>{ this.state.importError }</div>
}
}
NOTE: Above code untested, and also using class properties/methods for binding updateState.

setState/use State in external function react

Considering this pseudocode:
component.js
...
import {someFunc} from "./common_functions.js"
export default class MyComp extends Component {
constructor(props) {
super(props);
this.someFunc = someFunc.bind(this);
this.state = {...};
}
_anotherFunc = () = > {
....
this.someFunc();
}
render() {
...
}
}
common_functions.js
export function someFunc() {
if(this.state.whatever) {...}
this.setState{...}
}
How would I bind the function someFunc() to the context of the Component? I use it in various Components, so it makes sense to collect them in one file. Right now, I get the error "Cannot read whatever of undefined". The context of this is unknown...
You can't setState outside of the component because it is component's local state. If you need to update state which is shared, create a store (redux store).
In your case, you can define someFunction at one place and pass it the specific state variable(s) or entire state. After you are done in someFunction, return the modified state and update it back in your component using setState.
export function someFunc(state) {
if(state.whatever) {...}
const newState = { ...state, newValue: whateverValue }
return newState
}
_anotherFunc = () = > {
....
const newState = this.someFunc(this.state);
this.setState({newValue: newState});
}
it's not a React practice and it may cause lot of problems/bugs, but js allows to do it:
Module A:
export function your_external_func(thisObj, name, val) {
thisObj.setSate((prevState) => { // prevState - previous state
// do something with prevState ...
const newState = { // new state object
someData: `This is updated data ${ val }`,
[name]: val,
};
return newState
});
}
Then use it in your react-app module:
import { your_external_func } from '.../your_file_with_functions';
class YourReactComponent extends React.Component {
constructor(props, context) {
super(props, context);
this.state={
someName: '',
someData: '',
};
}
handleChange = (e) => {
const { target } = event;
const { name } = target;
const value = target.type === 'checkbox' ? target.checked : target.value;
your_external_func(this, name, value);
}
render() {
return (<span>
{ this.state.someData }
<br />
<input
name='someName'
value={ this.state.someName }
onChange={ this.handleChange }
/>
</span>);
}
}
It's a stupid example :) just to show you how you can do it
The best would obviously to use some kind of external library that manages this. As others have suggested, Redux and MobX are good for this. Using a high-order component to wrap all your other components is also an option.
However, here's an alternative solution to the ones above:
You could use a standard javascript class (not a React component) and pass in this to the function that you are calling from that class.
It's rather simple. I've created a simple example below where the state is changed from a function of another class; take a look:
class MyApp extends React.Component {
constructor() {
super();
this.state = {number: 1};
}
double = () => {
Global.myFunc(this);
}
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.double}>Double up!</button>
</div>
);
}
}
class Global {
static myFunc = (t) => {
t.setState({number: t.state.number*2});
}
}
ReactDOM.render(<MyApp />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"><div>
There is a functional form of setState that can even be used outside of a component.
This is possible since the signature of setState is:
* #param {object|function} partialState Next partial state or function to
* produce next partial state to be merged with current state.
* #param {?function} callback Called after state is updated.
See Dan's tweet: https://twitter.com/dan_abramov/status/824308413559668744
This all depends on what you are trying to achieve. At first glance I can see 2 options for you. One create a child component and two: use redux as redux offers a singular state between all of your child components.
First option:
export default class parentClass extends Component {
state = {
param1: "hello".
};
render() {
return (
<Child param1={this.state.param1}/>
);
}
}
class Child extends Component {
render() {
console.log(this.props.param1);
return (
<h1>{this.props.param1}</h1>
);
}
}
Now the above child component will have the props.param1 defined from the props passed from it's parent render function.
The above would work but I can see you're trying to establish a 'common' set of functions. Option 2 sort of provides a way of doing that by creating a singular state for your app/project.
If you've haven't used redux before it's pretty simple to use once you've got the hang of it. I'll skip out the setup for now http://redux.js.org/docs/basics/UsageWithReact.html.
Make a reducer like so:
import * as config from './config';//I like to make a config file so it's easier to dispatch my actions etc
//const config.state = {param1: null}
//const config.SOME_FUNC = "test/SOME_FUNC";
export default function reducer(state = config.state, action = {}) {
switch(action.type) {
case config.SOME_FUNC:
return Object.assign({}, state, {
param1: action.param1,
});
break;
default:
return state;
}
}
}
Add that to your reducers for your store.
Wrap all your components in the Provider.
ReactDOM.render(
<Provider store={store} key="provider">
<App>
</Provider>,
element
);
Now you'll be able to use redux connect on all of the child components of the provider!
Like so:
import React, {Component} from 'react';
import {connect} from 'react-redux';
#connect(
state => (state),
dispatch => ({
someFunc: (param1) => dispatch({type: config.SOME_FUNC, param1: param1}),
})
)
export default class Child extends Component {
eventFunction = (event) => {
//if you wanted to update the store with a value from an input
this.props.someFunc(event.target.value);
}
render() {
return (
<h1>{this.props.test.param1}</h1>
);
}
}
When you get used to redux check this out https://github.com/redux-saga/redux-saga. This is your end goal! Sagas are great! If you get stuck let me know!
Parent component example where you define your callback and manage a global state :
export default class Parent extends Component {
constructor() {
super();
this.state = {
applyGlobalCss: false,
};
}
toggleCss() {
this.setState({ applyGlobalCss: !this.state.applyGlobalCss });
}
render() {
return (
<Child css={this.state.applyGlobalCss} onToggle={this.toggleCss} />
);
}
}
and then in child component you can use the props and callback like :
export default class Child extends Component {
render() {
console.log(this.props.css);
return (
<div onClick={this.props.onToggle}>
</div>
);
}
}
Child.propTypes = {
onToggle: PropTypes.func,
css: PropTypes.bool,
};
Well for your example I can see you can do this in a simpler way rather than passing anything.
Since you want to update the value of the state you can just return it from the function itself.
Just make the function you are using in your component async and wait for the function to return a value and set the state to that value.
import React from "react"
class MyApp extends React.Component {
constructor() {
super();
this.state = {number: 1};
}
theOnlyFunction = async() => {
const value = await someFunctionFromFile( // Pass Parameters );
if( value !== false ) // Just for your understanding I am writing this way
{
this.setState({ number: value })
}
}
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.double}>Double up!</button>
</div>
);
}
}
And in SomeOtherFile.js
function someFunctionFromFile ( // catch params) {
if( //nah don't wanna do anything ) return false;
// and the blahh blahh algorithm
}
you should use react Context
Context lets us pass a value deep into the component tree without explicitly threading it through every component.
here is a use case from react docs : create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we're passing "dark" as the current value.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// Assign a contextType to read the current theme context.
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is "dark".
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
resource: https://reactjs.org/docs/context.html

Reactjs, parent component, state and props

I m actually learning reactjs and I m actually developping a little TODO list, wrapped inside of a "parent component" called TODO.
Inside of this parent, I want to get the current state of the TODO from the concerned store, and then pass this state to child component as property.
The problem is that I dont know where to initialize my parent state values.
In fact, I m using ES6 syntax, and so, I dont have getInitialState() function. It's written in the documentation that I should use component constructor to initialize these state values.
The fact is that if I want to initialize the state inside of my constructor, the this.context (Fluxible Context) is undefined actually.
I decided to move the initialization inside of componentDidMount, but it seems to be an anti pattern, and I need another solution. Can you help me ?
Here's my actual code :
import React from 'react';
import TodoTable from './TodoTable';
import ListStore from '../stores/ListStore';
class Todo extends React.Component {
constructor(props){
super(props);
this.state = {listItem:[]};
this._onStoreChange = this._onStoreChange.bind(this);
}
static contextTypes = {
executeAction: React.PropTypes.func.isRequired,
getStore: React.PropTypes.func.isRequired
};
componentDidMount() {
this.setState(this.getStoreState()); // this is what I need to move inside of the constructor
this.context.getStore(ListStore).addChangeListener(this._onStoreChange);
}
componentWillUnmount() {
this.context.getStore(ListStore).removeChangeListener(this._onStoreChange);
}
_onStoreChange () {
this.setState(this.getStoreState());
}
getStoreState() {
return {
listItem: this.context.getStore(ListStore).getItems() // gives undefined
}
}
add(e){
this.context.executeAction(function (actionContext, payload, done) {
actionContext.dispatch('ADD_ITEM', {name:'toto', key:new Date().getTime()});
});
}
render() {
return (
<div>
<button className='waves-effect waves-light btn' onClick={this.add.bind(this)}>Add</button>
<TodoTable listItems={this.state.listItem}></TodoTable>
</div>
);
}
}
export default Todo;
As a Fluxible user you should benefit from Fluxible addons:
connectToStores.
The following example will listen to changes in FooStore and BarStore and pass foo and bar as props to the Component when it is instantiated.
class Component extends React.Component {
render() {
return (
<ul>
<li>{this.props.foo}</li>
<li>{this.props.bar}</li>
</ul>
);
}
}
Component = connectToStores(Component, [FooStore, BarStore], (context, props) => ({
foo: context.getStore(FooStore).getFoo(),
bar: context.getStore(BarStore).getBar()
}));
export default Component;
Look into fluxible example for more details. Code exсerpt:
var connectToStores = require('fluxible-addons-react/connectToStores');
var TodoStore = require('../stores/TodoStore');
...
TodoApp = connectToStores(TodoApp, [TodoStore], function (context, props) {
return {
items: context.getStore(TodoStore).getAll()
};
});
As a result you wouldn't need to call setState, all store data will be in component's props.

Categories

Resources