How to track nested object in a MobX store - javascript

Let's stay I have this myObject loaded via an API call:
myObject = {
fieldA: { details: 'OK', message: 'HELLO' },
fieldB: { details: 'NOT_OK', message: 'ERROR' },
}
Only details and message of each field can change. I want this object to be observable in a MobX store (which properties? to be defined below). I have a simple React component which reads the two fields from the store:
#observer
class App extends Component {
store = new Store();
componentWillMount() {
this.store.load();
}
render() {
return (
<div>
{this.store.fieldA && <p>{this.store.fieldA.details}</p>}
{this.store.fieldB && <p>{this.store.fieldB.details}</p>}
</div>
);
}
}
I read this page trying to understand what MobX reacts to, but still didn't get a clear idea. Specifically, which of the 4 stores below would work, and why?
1/
class Store1 = {
#observable myObject = {};
#action setMyObject = object => {
this.myObject = object;
}
load = () => someAsyncStuff().then(this.setMyObject);
}
2/
class Store2 = {
#observable myObject = {};
#action setMyObject = object => {
this.myObject.fieldA = object.fieldA;
this.myObject.fieldB = object.fieldB;
}
load = () => someAsyncStuff().then(this.setMyObject);
}
3/
class Store3 = {
#observable myObject = { fieldA: {}, fieldB: {} };
#action setMyObject = object => {
this.myObject = object;
}
load = () => someAsyncStuff().then(this.setMyObject);
}
4/
class Store4 = {
#observable myObject = { fieldA: {}, fieldB: {} };
#action setMyObject = object => {
this.myObject.fieldA = object.fieldA;
this.myObject.fieldB = object.fieldB;
}
load = () => someAsyncStuff().then(this.setMyObject);
}

All of the above will work, except for solution 2.
That because as described in Mobx docs about objects:
When passing objects through observable, only the properties that
exist at the time of making the object observable will be observable.
Properties that are added to the object at a later time won't become
observable, unless extendObservable is used.
In the first solution you re-assign the object again with the properties already exist in the returned object.
In 3 and 4 you initialized the object with those 2 properties so it works.
Also I think in your component example you meant to use it like this (otherwise, it won't work in any way):
render() {
const { myObject } = this.store;
return (
<div>
{myObject && myObject.fieldA && <p>{myObject.fieldA.details}</p>}
{myObject && myObject.fieldB && <p>{myObject.fieldB.details}</p>}
</div>
);
}

Related

Giving Typescript classes access to React hooks state

I'm trying to make a game using React to display the UI elements and using Typescript classes to represent the state of the game.
Here are a few examples of the classes I'm using to represent my data:
export class Place extends Entity {
items: Item[];
npcs: NPC[];
location: LatLng | null;
onEnter: (...args: any[]) => any = () => {};
constructor(
name: string,
description: string,
location?: LatLng,
onEnter: (...args: any[]) => any = () => {},
items: Item[] = [],
npcs: NPC[] = []
) {
super(name, description);
this.items = items;
this.npcs = npcs;
this.location = location ? location : null;
this.onEnter = onEnter;
}
export class Item extends Entity {
url: string;
constructor(
name: string,
description: string,
actions = {},
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Question_mark_%28black%29.svg/1920px-Question_mark_%28black%29.svg.png"
) {
super(name, description);
this.url = url;
this.actions = actions;
}
}
export class NPC {
name: string;
description: string;
messages: Message[];
url: string;
timesTalkedTo = 0;
constructor(
name: string,
description: string,
url = "https://cdn.icon-icons.com/icons2/1378/PNG/512/avatardefault_92824.png",
messages: Message[] = []
) {
this.name = name;
this.description = description;
this.messages = messages;
this.url = url;
}
getMsg() {
console.log(this.messages);
if (this.messages.length > 1) {
for (var i = 1; i < this.messages.length; i++) {
const msg = this.messages[i];
if (msg["cond"] && msg["cond"]()) {
this.timesTalkedTo += 1;
return msg;
}
}
}
this.timesTalkedTo += 1;
return this.messages[0];
}
}
Later on, I store instances of these classes in hooks so I can display them using other components I've defined:
function UI() {
const [places, setPlaces] = useState({});
const [inventory, setInventory] = useState([]);
const [playerPlace, setPlayerPlace] = useState(outside);
const [playerLocation, setPlayerLocation] = useState(L.latLng([0, 0]));
...
My problem is that I wanted to define a class and functions like this inside my UI component, so I would be able to access the setState hooks and use the "drop" and "pick up" actions on any item I've defined as Droppable:
class Droppable extends Item {
dropped;
constructor(
name,
description,
actions = {},
dropped = true,
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Question_mark_%28black%29.svg/1920px-Question_mark_%28black%29.svg.png"
) {
super(name, description, actions, url);
this.dropped = dropped;
const drop = () => {
addToPlace(removeFromInventory(this));
this.dropped = true;
this.actions["pick up"] = pickUp;
delete this.actions["drop"];
};
const pickUp = () => {
addToInventory(removeFromPlace(this));
this.dropped = false;
this.actions["drop"] = drop;
delete this.actions["pick up"];
};
if (dropped) {
this.actions["pick up"] = pickUp;
} else {
this.actions["drop"] = drop;
}
}
}
const addToInventory = useCallback(
(item) => {
setInventory((inv) => [...inv, item]);
return item;
},
[setInventory]
);
const removeFromInventory = useCallback(
(item) => {
setInventory((inv) => inv.filter((i) => i !== item));
return item;
},
[setInventory]
);
const addToPlace = useCallback(
(item) => {
setPlaces((places) => {
return {
...places,
[playerPlace.name]: {
...playerPlace,
items: [...playerPlace.items, item],
},
};
});
return item;
},
[setPlaces, playerPlace]
);
const removeFromPlace = useCallback(
(item) => {
setPlaces((places) => {
const newPlace = { ...places[playerPlace.name] };
newPlace.items = newPlace.items.filter((i) => i !== item);
const newPlaces = [...places];
newPlaces[playerPlace.name] = newPlace;
return newPlaces;
});
return item;
},
[setPlaces, playerPlace]
);
However, when I try removing an item from the place it's in and adding it to the player's inventory (the "pick up" action), I find that, while it is successfully added to the inventory, it cannot be removed from the place, because the playerPlace state variable is stale. Even though setPlayerPlace had been called successfully and set the playerPlace to a place containing items, the value is still set to its initial empty Place, so there is an error when trying to access the items of that Place.
My guess is that these callbacks are not being updated properly according to the state because they are used inside the class that I defined, but I can't think of any other way to give methods inside the class easy access to the state variables.
Is it a bad idea to be using ordinary classes alongside React in this way? If so, what would be a better way to structure my app. If not, how can I give my classes access to the state inside my React components?
I would recommend moving the class outside of the component and then passing the setters and data to the class as parameters if you really want to use classes. You can also use a third-party state management library for this and then hook it together, but I don't think it's really worth it. Generally speaking, using classes for your state in react is an antipattern IMHO. Usually what I would do is just write types and then utility functions for those types if I need to encapsulate functionality. This has many benefits aside from working with react such as easily being able to serialize the data to JSON (they are now POJOs).

Javascript, in a React application assign to {} in a function component, code review

I have this code in a friend of mine React application and I need to understand what this code does explicitly:
const Component = ()=> (
<QueryFetcher>
{({ data }) => {
const { user: { profile = {} } = {} } = data
return (
<div>
{profile.username && profile.username}
</div>
)
}}
</QueryFetcher>
)
What is this line for?
const { user: { profile = {} } = {} } = data
Is it correct to assign something to {} using { user: { profile = {} } = {} } in this functional component? Or in a render() hook of a stateful component in React?
const { user: { profile = {} } = {} } = data basically means that your retrieving the user profile.
const means that you are creating a new variable
{ user: { profile } } } means that you are retrieving profile inside of user
= {} means that if the object is undefined, use an empty object so it will not fail because doing user.profile will throw an error if user is undefined.
= data means that you retrieving this info from the data variable
So, this line means, from the variable data, go take the user, if the user is undefined, use an empty object. Then, go take the profile, if the profile is undefined, use an empty object. Then create a variable called profile with the result. This is like doing this:
const user = data.user === undefined ? {} : data.user;
const profile = user.profile === undefined ? {} : user.profile;
What is this line for?
const { user: { profile = {} } = {} } = data
It's basically just chained ES6 object-destructuring with default values.
What this line does in words:
Read "user" from "data", if "user" is undefined, assign {} as a default value
Read "profile" from "user", if "profile" is undefined, assign {} as a default value
Is it correct
It is mostly a short-hand syntax used to remove repetitive stuff. So instead of accessing multiple object props separately e.g.
this.props.prop1, this.props.prop2, ...
you can use
const { prop1, prop2 } = this.props;
It also helps other readers later quickly understanding what variables are used in a method if all necessary props are destructured at the start.

React to nested state change in Angular and NgRx

Please consider the example below
// Example state
let exampleState = {
counter: 0;
modules: {
authentication: Object,
geotools: Object
};
};
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>){
this.counter = store.select('counter');
}
}
Here in the MyAppComponent we react on changes that occur to the counter property of the state. But what if we want to react on nested properties of the state, for example modules.geotools? Seems like there should be a possibility to call a store.select('modules.geotools'), as putting everything on the first level of the global state seems not to be good for overall state structure.
Update
The answer by #cartant is surely correct, but the NgRx version that is used in the Angular 5 requires a little bit different way of state querying. The idea is that we can not just provide the key to the store.select() call, we need to provide a function that returns the specific state branch. Let us call it the stateGetter and write it to accept any number of arguments (i.e. depth of querying).
// The stateGetter implementation
const getUnderlyingProperty = (currentStateLevel, properties: Array<any>) => {
if (properties.length === 0) {
throw 'Unable to get the underlying property';
} else if (properties.length === 1) {
const key = properties.shift();
return currentStateLevel[key];
} else {
const key = properties.shift();
return getUnderlyingProperty(currentStateLevel[key], properties);
}
}
export const stateGetter = (...args) => {
return (state: AppState) => {
let argsCopy = args.slice();
return getUnderlyingProperty(state['state'], argsCopy);
};
};
// Using the stateGetter
...
store.select(storeGetter('root', 'bigbranch', 'mediumbranch', 'smallbranch', 'leaf')).subscribe(data => {});
...
select takes nested keys as separate strings, so your select call should be:
store.select('modules', 'geotools')

JS immutable objects keep the same object type

Let's say I have a class.
class Test {
constructor() {
this.name = 'name';
this.arr = [];
}
}
And I need to create multiple instances from this class.
const entities = {
1: new Test(),
2: new Test()
}
Now, I need to update one of the properties in a shallow clone manner.
const newEntities = {
...entities,
[1]: {
...entities[1],
name: 'changed'
}
}
console.log(newEntities[1].arr === entities[1].arr) <=== true
That's works, but the problem is that [1] is a simple object and not instance of Test anymore.
How can I fix that?
You can't keep instances using object destructuring so you'll need implement this behaviour.
First example, set the new properties in the constructor:
class Test {
constructor(props) {
props = props == null ? {} : props;
this.name = 'name';
this.arr = [];
Object.assign(this, props);
}
}
const newEntities = {
...entities,
[1]: new Test({ ...entities[1], name: 'changed' })
}
Sencod example, use a custom method:
class Test {
constructor() {
this.name = 'name';
this.arr = [];
}
assign(props) {
props = props == null ? {} : props;
const instance = new Test();
Object.assign(instance, this, props);
return instance;
}
}
const newEntities = {
...entities,
[1]: entities[1].assign({ name: 'changed' })
}
You can use Object.setPrototypeOf on your [1] object.
As result, it will be:
Object.setPrototypeOf(newEntities[1], Test.prototype);

MobX not setting observable properly in my class

I'm trying to make my react app as dry as possible, for common things like consuming a rest api, I've created classes that act as stores with predefined actions to make it easy to modify it.
Behold, big code:
import {autorun, action, observable} from 'mobx'
export function getResourceMethods(name) {
let lname = name.toLowerCase()
let obj = {
methods: {
plural: (lname + 's'),
add: ('add' + name),
addPlural: ('add' + name + 's'),
rename: ('rename' + name),
},
refMethods: {
add: ('add' + name + 'ByRef'),
addPlural: ('add' + name + 'sByRef'),
rename: ('rename' + name + 'ByRef'),
setRef: ('set' + name + 'Ref'),
},
fetchMethods: {
pending: (lname + 'Pending'),
fulfilled: (lname + 'Fulfilled'),
rejected: (lname + 'Rejected'),
}
}
return obj
}
class ResourceItem {
#observable data;
#observable fetched = false;
#observable stats = 'pending';
#observable error = null;
constructor(data) {
this.data = data;
}
}
class ResourceList {
#observable items = [];
#observable fetched = false;
#observable status = 'pending';
constructor(name) {
this['add' + name + 's'] = action((items) => {
items.forEach((item, iterator) => {
this.items.push(item.id)
})
})
}
}
class ResourceStore {
constructor(name, resourceItem, middleware) {
let {methods} = getResourceMethods(name)
this.middleware = middleware || []
let items = methods.plural.toLowerCase()
this[items] = observable({}) // <--------------- THIS DOES NOT WORK!
// Add resource item
this[methods.add] = action((id, resource) => {
let item = this[items][id], data;
if (item && item.fetched) {
data = item.data
} else {
data = resource || {}
}
this[items][id] = new resourceItem(data)
this.runMiddleware(this[items][id])
})
// Add several resource items
this[methods.addPlural] = action((resources) => {
resources.forEach((resource, iterator) => {
this[methods.add](resource.id, resource)
})
})
// Rename resource item
this[methods.rename] = action((oldId, newId) => {
let item = this[items][oldId]
this[items][newId] = item
if (oldId !== newId) {
delete this[items][oldId]
}
})
// Constructor ends here
}
runMiddleware(item) {
let result = item;
this.middleware.map(fn => {
result = fn(item)
})
return result
}
}
class ReferencedResourceStore extends ResourceStore {
#observable references = {}
constructor(name, resource, middleware) {
super(name, resource, middleware)
let {methods, refMethods, fetchMethods} = getResourceMethods(name)
let getReference = (reference) => {
return this.references[reference] || reference
}
this[refMethods.setRef] = action((ref, id) => {
this.references[ref] = id
})
this[refMethods.add] = action((ref, data) => {
this[methods.add](getReference(ref), data)
this[refMethods.setRef](ref, getReference(ref))
})
this[refMethods.rename] = action((ref, id) => {
this[methods.rename](getReference(ref), id)
this[refMethods.setRef](ref, id)
})
// *** Fetch *** //
// Resource pending
this[fetchMethods.pending] = action((ref) => {
this[refMethods.add](ref)
})
// Resource fulfilled
this[fetchMethods.fulfilled] = action((ref, data) => {
this[refMethods.add](ref, data)
this[refMethods.rename](ref, data.id)
let item = this[methods.plural][data.id];
item.fetched = true
item.status = 'fulfilled'
})
}
}
export {ResourceItem, ResourceList, ResourceStore, ReferencedResourceStore}
Now I'm just creating a simple user store:
class UserResource extends ResourceItem {
constructor(data) {
super(data)
}
#observable posts = new ResourceList('Posts')
#observable comments = new ResourceList('Comment')
}
// Create store
class UserStore extends ReferencedResourceStore {}
let store = new UserStore('User', UserResource)
And mobx-react connects just fine to the store, can read it as well. BUT, whenever I do any changes to the items (users in this case, the name of the property is dynamic) property, there are no reactions. I also noticed that in chrome, the object property does not have a "invoke property getter" in the tree view:
Didn't read the entire gist, but if you want to declare a new observable property on an existing object, use extendObservable, observable creates just a boxed observable, so you have an observable value now, but not yet an observable property. In other words:
this[items] = observable({}) // <--------------- THIS DOES NOT WORK!
should be:
extendObservable(this, {
[items] : {}
})
N.b. if you can't use the above ES6 syntax, it desugars to:
const newProps = {}
newProps[items] = {}
extendObservable(this, newProps)
to grok this: https://mobxjs.github.io/mobx/best/react.html
Edit: oops misread, you already did that, it is not hacky but the correct solution, just make sure the extend is done before the property is ever used :)
I found a hacky solution:
First off, use extendObservable instead (this is the correct solution) and then use a fresh version of the object and set it as the property.
let items = methods.plural.toLowerCase()
extendObservable(this, {
[items]: {}
})
// Add resource item
this[methods.add] = action((id, resource) => {
let item = this[items][id], data;
if (item && item.fetched) {
data = item.data
} else {
data = resource || {}
}
this[items][id] = new resourceItem(data)
this.runMiddleware(this[items][id])
this[items] = {...this[items]}
})
This works, not sure if there's a better solution.
Your options are using extendObservable or using an observable map.
For reference see the documentation of observable and specifically:
To create dynamically keyed objects use the asMap modifier! Only initially existing properties on an object will be made observable, although new ones can be added using extendObservable.

Categories

Resources