Mobx store update when React props change - reactjs

I'm creating a generic react component, I'm using mobx internally to control the component state. What I need to achieve is besides to keep all business logic in Store, when the user change the showSomething prop, the store should know it so fetchSomeStuffs runs and change anotherThing.
// Application using the component
#observer
class MyApplication extends React.Component {
#observable
showSomething = false;
changeThings = () => {
this.showSomething = true;
};
render() {
return (
<React.Fragment>
<button onClick={this.changeThings}>Change Show Something</button>
<MyComponent showSomething={showSomething} />
</React.Fragment>
);
}
}
class Store {
#observable
showSomething = false;
#observable
anotherThing = [];
#action
setShowSomething = value => {
this.showSomething = value;
};
// I'll dispose this later...
fetchSomeStuffs = autorun(() => {
const { showSomething } = this;
// Update some other stuffs
if (showSomething) {
this.anotherThing = [1, 2, 3];
} else {
this.anotherThing = [];
}
});
}
#observer
class MyComponent extends React.Component {
static propTypes = {
showSomething: PropTypes.bool
};
constructor() {
super();
this.store = new Store();
}
componentDidMount() {
const { setShowSomething } = this.store;
this.setSomethingDispose = autorun(() =>
setShowSomething(this.props.showSomething)
);
}
componentWillUnmount() {
this.setSomethingDispose();
}
render() {
return (
<Provider store={this.store}>
<MySubComponent />
</Provider>
);
}
}
#inject("store")
#observer
class MySubComponent extends React.Component {
render() {
const { showSomething, anotherThing } = this.props.store;
return (
<div>
MySubComponent
{showSomething && "Something is finally showing"}
{anotherThing.map((r, i) => {
return <div key={i}>{r}</div>;
})}
</div>
);
}
}
This is the way i found to achieve it, all the logic is in the Store and I use an autorun in the componentDidMount of my main component to always keep the showSomething variable of the store the same as the prop.
My doubt here is if this is a good practice or if there are better ways to do it?

Yes, there is a better way, using computed values.
One pattern is to use a private variable to hold the values and a computed value to expose what you want.
using this pattern you can implement list filters and all sorts of dynamic computations with a lot of agility, since MobX optimizes all computed values.
Just remember that to access a computed value you just have to read it as a property, not a function.
Ex:
// good
store.anotherThing
// bad
store.anotherThing()
class Store {
#observable
showSomething = false;
#observable
_anotherThing = [];
#action
setShowSomething = value => {
this.showSomething = value;
};
#computed get anotherThing() {
const { showSomething } = this;
// Update some other stuffs
if (showSomething) {
this._anotherThing = [1, 2, 3];
} else {
this._anotherThing = [];
}
}
}

Related

How can I chain asynchronous Firebase updates in my React app?

React & Firebase newbie here. I have a React component that needs to look up some stuff in Firebase before rendering. My database design requires first getting the correct doohick ids and subsequently looking up the doohick details, but I'm not sure how to do that with the asynchronous nature of Firebase database access. This doesn't work:
class Widget extends React.Component {
componentDidMount() {
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
});
this.state.doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
render() {
if (this.state.doohick-ids) {
return null;
} else {
// render the Doohick subcomponents
}
}
}
I can think of a few solutions here, but none that I like. What's the recommended way to chain together Firebase calls, or perhaps redesign this to eliminate the problem?
I think you should split one component Widget to two WidgetList and WidgetItem.
WidgetItem
subscribe and unsubscribe to firebase.database().ref(/doohick-details/${id})
class WidgetItem extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
}
constructor(props) {
super(props);
this.state = {};
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
const { id } = this.props;
this.dbRef = firebase.database().ref(`/doohick-details/${id}`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
// update state
this.setState(dataSnapshot);
}
render() {
return (
<pre>{JSON.stringify(this.state, null, 2)}</pre>
);
}
}
WidgetList
subscribe and unsubscribe to firebase.database().ref(/users/${username}/doohick-ids)
class WidgetItem extends React.Component {
constructor(props) {
super(props);
this.state = { doohick_ids: [] };
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
// Note: I've just copied your example. `username` is undefined.
this.dbRef = firebase.database().ref(`/users/${username}/doohick-ids`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
this.setState({ doohick_ids: dataSnapshot });
}
render() {
const { doohick_ids } = this.state;
if (doohick_ids.length === 0) {
return 'Loading...';
}
return (
<React.Fragment>
{doohick_ids.map(id => <WidgetItem key={id} id={id} />)}
</React.Fragment>
);
}
}
And code that requires the data from the database needs to be inside the callback that is invoked when that data is available. Code outside of the callback is not going to have the right data.
So:
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
});
There's many optimizations possible here, but they all boil down to the code being inside the callback and updating the state when a value comes from the database.

Call a function of one of a list of children components of the parent component using reference

For my website I want to include a feature that helps users randomly click a link programatically. The event happens in the parent component called StreamingPlaza, and its has a list of children components called StreamingCard, each containing a streaming link. Below is my code:
StreamingPlaza
class StreamingPlaza extends Component {
state = {
......
}
roomclicks = [];
componentDidMount() {
//Approach 1//
this.roomclicks[0].current.handleClick();
//Approach 2//
this.roomclicks[0].props.click = true;
......
}
setRef = (ref) => {
this.roomclicks.push(ref);
}
renderRoom = (room) => {
return <StreamingCard info={room} ref={this.setRef} click={false}></StreamingCard>;
}
render () {
const rooms = this.props.rooms;
return (
{ rooms && rooms.map (room => {
return this.renderRoom(room);
})
}
);
}
StreamingCard
class StreamingCard extends Component {
constructor(props){
super(props);
this.state = {
......
}
}
handleClick = () => {
document.getElementById("link").click();
}
render() {
return (
✔️ Streaming Link: <a id="link" href=......></a>
);
}
Regarding Approach 1, the console reported the error Cannot read property handClick of undefined. After I removed "current", it said that this.roomclicks[0].handleClick is not a function. Regarding Approach 2, I was not able to modify the props in this way, as the console reported that "click" is read-only.
Approach 1 is basically how its need to be done, but with React API.
See React.createRef
class StreamingPlaza extends Component {
roomclicks = React.createRef([]);
componentDidMount() {
// 0 is room.id
this.roomclicks.current[0].handleClick();
}
renderRoom = (room) => {
return (
<StreamingCard
info={room}
ref={(ref) => (this.roomclicks.current[room.id] = ref)}
click={false}
></StreamingCard>
);
};
render() {
const rooms = this.props.rooms;
return rooms.map((room) => {
return this.renderRoom(room);
});
}
}

static props evaluation resulting in exception

I am calling the below SomeComponent like following. SomeComponent.render is happening fine. But When i call show method I get undefined is not an object 'arr[index].something'
<SomeComponent arr={arr} />
export default class SomeComponent extends Component {
static propTypes = {
arr: PropTypes.arrayOf(PropTypes.any).isRequired,
};
state = {
x: 1,
x: 2,
};
show(index) {
const {arr} = this.props;
if(arr[index].something){ // Here i get the issue. Seems likes arr is undefined.
}
}
render() {
const {arr} = this.props;
return(
<Viewer images={arr} show={currentIndex => this.show(currentIndex)} />
)
}
}
show = (index) =>{
const {arr} = this.props;
if(arr[index].something){ // Here i get the issue. Seems likes arr is undefined.
}
}
change this function to this way
this.show = this.show.bind(this)
or add this line to constructor,(this.props) instance of component can't accessible in function without binding function

React - Call wrapped component callback from higher order function

I have a higher order function that wraps the service calls. The data is streamed on a callback which I have to pass to the wrapped components. I have written the code below currently, where the child assigns handleChange to an empty object passed by the HOC. The wrapped component is a regular JS grid and hence I have to call the api to add data than pass it as a prop.
Is there a cleaner way of doing this?
function withSubscription(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.handler = {};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange(row) {
if (typeof this.handler.handleChange === "function") {
this.handler.handleChange(row);
}
}
render() {
return <WrappedComponent serviceHandler={this.handler} {...this.props} />;
}
};
}
class MyGrid extends React.Component {
constructor(props) {
super(props);
if (props.serviceHandler !== undefined) {
props.serviceHandler.handleChange = this.handleChange.bind(this);
}
this.onReady = this.onReady.bind(this);
}
onReady(evt) {
this.gridApi = evt.api;
}
handleChange(row) {
this.gridApi.addRow(row);
}
render() {
return <NonReactGrid onReady={this.onReady} />;
}
}
const GridWithSubscription = withSubscription(MyGrid);
That wrapped component should be aware of handler.handleChange looks awkward.
If withSubscription can be limited to work with stateful components only, a component may expose changeHandler hook:
function withSubscription(WrappedComponent) {
return class extends React.Component {
...
wrappedRef = React.createRef();
handleChange = (row) => {
if (typeof this.wrappedRef.current.changeHandler === "function") {
this.wrappedRef.current.changeHandler(row);
}
}
render() {
return <WrappedComponent ref={this.wrappedRef}{...this.props} />;
}
};
}
class MyGrid extends React.Component {
changeHandler = (row) => {
...
}
}
const GridWithSubscription = withSubscription(MyGrid);
To work with stateful and stateless components withSubscription should be made more generalized to interact with wrapped component via props, i.e. register a callback:
function withSubscription(WrappedComponent) {
return class extends React.Component {
handleChange = (row) => {
if (typeof this.changeHandler === "function") {
this.changeHandler(row);
}
}
registerChangeHandler = (cb) => {
this.changeHandler = cb;
}
render() {
return <WrappedComponent
registerChangeHandler={this.registerChangeHandler}
{...this.props}
/>;
}
};
}
class MyGrid extends React.Component {
constructor(props) {
super(props);
props.registerChangeHandler(this.changeHandler);
}
changeHandler = (row) => {
...
}
}
In case the application already uses some form of event emitters like RxJS subjects, they can be used instead of handler.handleChange to interact between a parent and a child:
function withSubscription(WrappedComponent) {
return class extends React.Component {
changeEmitter = new Rx.Subject();
handleChange = (row) => {
this.changeEmitter.next(row);
}
render() {
return <WrappedComponent
changeEmitter={this.changeEmitter}
{...this.props}
/>;
}
};
}
class MyGrid extends React.Component {
constructor(props) {
super(props);
this.props.changeEmitter.subscribe(this.changeHandler);
}
componentWillUnmount() {
this.props.changeEmitter.unsubscribe();
}
changeHandler = (row) => {
...
}
}
Passing subjects/event emitters for this purpose is common in Angular because the dependency on RxJS is already imposed by the framework.

MobX observable with dynamic data

I have the following class
export default class BaseStore {
#observable model ;
#action updateStore(propertyName, newValue) {
this.model[propertyName] = newValue;
}
}
In child classes I add layers to the observable model, such as :
model.payment.type = 'credit card'
My react component doesn't render automatically when this happen, it does however, if I has a top level data such as:
model.Type = 'CreditCard'
I am new to MobX, and read that I need to make use of map() but I am unable to find a decent example that explain how to use it.
If you know all the keys that the model will have, you can just initialize them with a null value, and the observer components will re-render.
Example (JSBin)
class BaseStore {
#observable model = {
type: null
};
#action updateStore(propertyName, newValue) {
this.model[propertyName] = newValue;
}
}
const baseStore = new BaseStore();
#observer
class App extends Component {
componentDidMount() {
setTimeout(() => baseStore.model.type = 'CreditCard', 2000);
}
render() {
return <div> { baseStore.model.type } </div>;
}
}
If you don't know all the keys of model beforehand, you can use a map like you said:
Example (JSBin)
class BaseStore {
model = observable.map({});
#action updateStore(propertyName, newValue) {
this.model.set(propertyName, newValue);
}
}
const baseStore = new BaseStore();
#observer
class App extends Component {
componentDidMount() {
this.interval = setInterval(() => {
const key = Math.random().toString(36).substring(7);
const val = Math.random().toString(36).substring(7);
baseStore.updateStore(key, val);
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <div>
{ baseStore.model.entries().map(e => <div> {`${e[0]} ${e[1]}` } </div>) }
</div>;
}
}

Resources