Can mobX tell me which elements in an observable array or object were changed? - mobx

I am using mobX with React and Three.js. The Three.js scene is rendered in a React component. I use the mobX store for information rendered both in normal React components as well as the 3D scene (this is not a graphics or React question though).
To update the ThreeJS scene when relevant changes happen in the mobX store, I have a custom reaction:
import { reaction } from 'mobx';
import { observer, inject } from 'mobx-react';
// ...
#inject('matchStore')
#observer
class MapViewport extends React.Component {
// Update the scene when the map state changes
reactToSceneUpdate = reaction(
() => return this.props.matchStore.territories,
() => this.updateScene()
);
// ...
That reaction is correctly updating the scene when matchStore.territories changes. matchStore. matchStore.territories is an array computed from an observable object:
class MatchStore {
#observable territoriesById = {};
#computed get territories() {
return Object.values(this.territoriesById);
}
// ...
The problem is that matchStore.territories is a very large array, and the updateScene function has to iterate through every member of the array to check if it needs graphics updates. I've noticed that this takes some time and has a noticeable lag.
Is there a way that mobX lets me get the specific member(s) of the array that triggered the change?
Alternatively, can I use the observable object in the reaction (matchStore.territoriesById) and somehow get the member(s) of the object that were changed?
My best idea so far is to set a flag on the updated members that's checked in the updateScene loop (and members not in need of updating are instantly skipped over), but I was hoping for a more implicit solution.

To get separate information about updates to individual array elements, define a Territory class with its own #observable properties, and create an instance of that class to be each element of the array. Set up a separate reaction for each element. That way, if its data changes you would make specific updates to the scene relating to that element.

Related

Re-render React useState without updating the state

I need to re-render this state manually.
const [person] = React.useState(new Person());
I have methods inside the Person class to update it (e.g. person.setName('Tom')).
When I update person using a method from itself, it does not trigger a re-render on the person state.
const carouselData = React.useMemo(() => {
// Doesn't re-render when the fields on the person class update
}, [person]);
Is there a way to force this state to re-render without using a setState function?
Ideally, is there a way to call that re-render from inside the Person class itself?
Or is this totally misusing the useState functionality? Would there be a better React hook to connect this to?
Thanks!
Person could be a prop or in context instead. First create your instance outside of your component.
const person = new Person([]);
Then pass person as a prop.
function App({person}) {
const [personName, setPersonName] = useState(person.personName);
function handleNameChange(txt) {
person.addTodo(txt);
setPersonName(person.personName);
}
function handleSubmit(txt) {
handleNameChange(txt);
}
return (...)
}
you are indeed miss using it, react relies heavy on functional programming. you should be doing something like:
const [person, setPerson] = useState(new Person());
...
setPerson(setPersonName(person, 'Tom'));
that's just a silly example but you get the gist. react will only re-render if state is changed, it won't monitor if the state object is mutating or anything like angularjs used to do

Change state from other component (without passing setState function)

I have a quite decent background in android but now I am starting digging into react native and I am really confused with the ways to change the state of a component through hooks and set state function.
To the point, I have my main screen and I have many small components which change visibility. This is done by letting the user change some filter settings within dialogs. So the suggested way to do that is by having a hook in my main screen with a list that holds the values for the visibility of each component. But since I change the visibility of the components from inside the modals, every time I want to show a modal I will have to pass in a different function(for example setComponentEnable or setComponentDisabled) to set the state for each component. So my main screen will be polluted from all these small functions. Also I should not forget to mention that my modals are consisted from many smaller components and I will have to pass as deep as it goes the proper function to match the user action.
So my question is, is there a way to do this thing without polluting my main with all these small functions and make it possible for the main screen to update every time the user change the filters within the modals?
I already read about context but the docs say:
Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.
So I dont think that this should be a great case for context use.
What I am trying to do now is create a hook with a list
const [isibility, setVisibility] = useState([]);
create visibility handler functions
const setVisibilityEnable = () => {
...
}
and pass it into my modal.
<MyModal
visibilityHandler={setVisibilityEnable}/>
Is there a way to manipulate the state without passing all these callbacks to the modals? Or maybe is there anyone that can suggest a better and clean solution to avoid end up having a really huge main screen?
you can include all the settings in one object and pass that object to all the components. Then each component will then modify that object accordingly.
const defaultVisibility = {
childComponentOne: true,
childComponentTwo: true,
};
const [visibilityObject, setVisibilityObject] = useState(defaultVisibility);
pass both the function and the object into your child components:
<ChildComponentOne visibilityObject={visibilityObject} setVisibilityObject={setVisibilityObject} />
Then in your child component, you set the visibility like so:
setVisibilityObject({...visibilityObject, childComponentOne: false});
Why you don't just pass a property to your modal and check if changed in oncomponentdidchange method?
componentDidUpdate(prevProps) {
if (this.props.yourPoperty!== prevProps.yourPoperty) {
//do your visibility stuff
}
}
Alternatively you can do it with redux when you connect your components to the store.

React functions: changing state after useState

I have a functional component that manages a question that can come from two sources.
A props value comes in indicating the source.
When the source changes, I want to create a new question model object with the new source.
Since I'm doing something like this:
const [questionModel, setQuestionModel ] = useState(new QuestionModel(questionSource));
For some reasons it thinks, "Oh, I've already got one of those questionModel's. I don't need to make a new one".
So all the old stuff stays there.
If I try to do something like:
setQuestionModel(new QuestionModel(questionSource));
Then it complains about:
Invariant Violation: Too many re-renders. React limits the number of
renders to prevent an infinite loop.
I get that infinite loops are bad, but I'm not sure how to make this work in ReactJS functions with hooks.
Back when I was using classes, I could specify something once in the constructor and then adjust it again in the render(). But now that the render is mixed in with the other code for the function, how do I re-render it with the new source?
Is there a force re-render when the props change? I thought it would do that if a prop changed ... but it doesn't.
I don't know how your props changes but I saw sometimes, that the following misunderstanding creates sometimes problems, where the developer thinks "I changed the object, so why my component doesn't rerender?":
When you create a new object like:
const user = { name: "john" };
You created an object that, has a property that points to the value "john" like this:
user -> { } -- name --> "john"
user points on an object and when you make name, point to a different value by:
user.name = "bob"
than user still points to the same object and to react it's the same object
but when you do
user = { ...user, name: "bob" };
then you would assign a new object and now it's a different object.
Look at using useEffect with a dependency of the prop that is being passed in. Within the effect, set the new question type in local state.
https://reactjs.org/docs/hooks-effect.html
Like this...
interface Props {
source: QuestionSource
}
export function QuestionsModal(props: Props) {
const [questionModel, setQuestionModel] = useState<QuestionModel>(new QuestionModel(questionSource))
useEffect(() => {
setQuestionModel(new QuestionModel(questionSource))
}, props.source)
}

Any way to make a React presentational component react to MobX store changes

I have a React table component that gets its data via a prop called TableStore. This prop is a high-level abstraction for getting row data:
interface TableStore<RowType> {
getRowIds: () => Array<RowId>;
getRow: (rowId: RowId) => RowType | undefined;
}
interface MaterialTableProps<RowType> {
tableStore: TableStore<RowType>;
}
function MaterialTable<RowType>(props: MaterialTableProps<RowType>) {
...
}
As you can see MaterialTable is not a MobX observer. It is part of a component library that is not dependent on MobX.
When I use this component in my app, I supply it a MobX-based TableStore. I would like the table component to re-render whenever the MobX-based store changes:
<MaterialTable tableStore={orderStore} />
However that does not happen because the table component is not a MobX observer. Is there any way to force the table component to re-render? For example, I am able to force a re-render by dereferencing the store in the parent component (using a simple console.log()). But this feels like a hack. Is there a better way?
Answering my own question....
I looked at several options but all of them were kludgy. I finally decided to rework the props of the table component to pass in an array instead of an abstract TableStore interface (which the table component can't react to). This allowed me to refrain from adding MobX as a dependency to the table component library while still leverage MobX in the parent component. In summary, the parent component now watches the MobX store, reacts to changes by creating a new array and passing it to the table component.
Here's the new interface for the table component:
export interface MaterialTableProps<T extends Entity> extends TableProps {
entityList: Array<T>;
}
export function MaterialTable<T extends Entity>(props: MaterialTableProps<T>) {
...
}

Large components as sections in VirtualizedList/etc?

If I want to display a bunch of heterogenous data in a virtualized list, it seems like the default way to do it is have the parent component gather up all the data so that it can create the sections to supply to the list component.
Is there any way to avoid requiring the parent component from doing this? I'd like to decouple the parent component from the data gather part, so that all it has to do is declare it has such and such components, and then those components would be responsible for gathering the data.
This would be exceedingly simple if it were a ScrollView:
<ScrollView>
<SectionA>
<SectionB>
<SectionC>
</ScrollView>
However, to leverage the performance gains of a VirtualizedList, if each section is large, I would need to pass in the individual data of each section into the VirtualizedList. I'm not sure how to do this or if it's possible.
Not sure if this is idiomatic or a gross React anti-pattern, but the way I solved it was to implement each section as purely headless data Component.
export type SectionDataComponentProps = {
onDataChanged?: () => void, // call this whenever the data updates.
}
export class SectionDataComponent<P : SectionDataComponentProps, S, ItemT> extends React.PureComponent<P, S> {
// Implemented by subclasses
getSectionData() : Array<SectionT<ItemT>> {
// returns an array of sections for a SectionList...
}
render() {
// business logic component only.
return null;
}
}
The parent component keeps track of them through the use of ref, and then calls getSectionData() as needed.