Using a model object in React (without Redux) - reactjs

I'm building a text editor app in React. My basic structure is
model
class TextLine {
text: string
folded: boolean
indentLevel: number
index: number
model: TextEntryModel
fold: () => this.model.foldLineAtIndex(this.index)
}
class TextModel {
lines: TextLine[]
setText: (text: string) => void // splits the text, creates TextLine objects and sets each line's model to this (line.model = this)
foldLineAtIndex = (index: number) => {
// we're actually folding all the "children" of this line.
const startingLine = this.getLineAtIndex(index);
let previousIndentLevel = startingLine.indentLevel;
// start with the next line
for (let i = index + 1; i < this.lines.length; i++) {
const thisLine = this.lines[i];
if (thisLine.indentLevel > previousIndentLevel) {
thisLine.folded = true;
} else {
return; // if we've reached a line with the same indent level, we're done.
}
}
};
}
basic component structure
<App> // state = { model: new TextEntryModel() };
{ model.lines.map(line =>
<Line
line={line}
onClick={line.fold}
/>
)}
</App>
Something like that, that's a big reduction. When you click the Line's accessory (not in my sketch) it calls fold on the line.
Clicking the line does indeed change the lines in the model, I can confirm this. But the lines don't rerender with their new folded value. At first I just had model as an instance property of the App class, and I thought maybe moving it to the App's state would help, but no dice.
I guess React's question of "have the props changed?" is not affirmative when a property of a property of the state has changed. (I would hope that when model changed it would pass Line a new line prop but maybe model isn't being registered as changing).
So, I could do this with Redux, possibly with a reducer whose type is just { model: TextModel } and use selectors to drill down in each Line, or possibly with a reducer with the simple type TextLine[] (or {lines: TextLine[]}).
But I like the idea of using a real model-ass model. I think this is how I'd do it, correctly, in say Swift or Angular, but it doesn't seem to fit the React paradigm as far as I can tell, which seems weird. So I imagine I'm missing something.
How do I go about this?

Related

How to render and update a list of React components dynamically in a performant way?

I am trying to build a simple form builder, with which you can add questions to a list, and each kind of question has a specific component to create the question, and one to set an answer.
These components are contained in an object like this
{
longText:{
create: (initialData, setNewData)=>{...},
answer: (initialData, setNewData)=>{...}
},
multiple:{
create: (initialData, setNewData)=>{...},
answer: (initialData, setNewData)=>{...}
}
...
}
When a kind of question is selected from a dropdown, the right component should render, showing the initialData.
right now the only way I have found that works is something like this:
{(() => {
return React.createElement(components[questionKindName].create,{
initialData: data,
setNewData: (v) => setTheData(v),
});
})()}
But this has very poor performance,
as I am updating the data, on every change,
and it triggers a new render of the whole list of forms each time.
this is the code that keeps track of the list of questions.
const [questions, setQuestions] = useState<Question[]>([]);
const addNewQuestion = (question: Question) => setQuestions([...questions, question]);
// this is the function invoked to set the new data
const editQuestion = (
questionIndex: Number,
key: keyof Question,
value: string
) =>
setQuestions(
questions.map((s, i) => {
if (s && questionIndex === i) {
s[key] = value;
}
return s;
})
);
how can I make it work nicely?
What is that I am missing here?
Should I put a throttle around the IIFE?
I am learning about lazy and Suspense, might that help?
I know the question could have been clearer,
if you can help, i will be happy to improve it.
Thanks.
PS:
I have tried saving and updating the selected component with a useState and a useEffect hook,
but it just doesn't work.
I always got an error like.. "cannot read "InitialData" on "_ref" as it is undefined" that indicates that it was invoking the function, but not passing the props.

useEffect not triggering when object property in dependence array

I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}

Why does Object.keys(this.refs) not return all keys?

Hi,
so I've redacted some sensitive information from the screen shot, but you can see enough to see my problem.
Now, I'm trying to build the UI for a site that gets data from a weather station.
I'm trying to use react-google-maps' InfoBox, which disables mouse events by default.
It seems that to enable mouse events, you must wait until the DOM is loaded, and then add the event handlers.
react-google-maps' InfoBox fires an onDomReady event (perhaps even upon adding more divs) but seems to never fire an onContentChanged event (I've looked in the node_modules code).
The content I'm putting in the InfoBox is basically a div with a string ref for each type of weather data. Sometimes there comes along a new type of weather data so I want to put that in also, and have the ref be available / usable.
However, immediately after the new divs have been added (and the DOM has been updated to show them), when I try to console log the DOM nodes (the refs refer to the nodes because they are divs and not a custom built component) the latest added ones are undefined.
They do become a div (not undefined) a few renders later.
I've contemplated that this may be because
1) the DOM is not being updated before I'm trying to access the refs, but indeed the UI shows the new divs,
2) string refs are deprecated (React 16.5),
but they work for the divs in comonentDidMount and eventually for new divs in componentDidUpdate,
3) executing the code within the return value of render may be run asynchronously with componentDidMount, but I also tried setTimeout with 3000 ms to the same effect,
4) of something to do with enumerable properties, but getOwnProperties behaves the same way.
In the end I decided I'll console log this.refs and Object.keys(this.refs) within the same few lines of code (shown in the screen shot), and you can see that within one console log statement (where Object.keys was used in the previous line) that while this.refs is an object with 8 keys, the two most recently added refs don't appear in Object.keys(this.refs).
This is probably a super complex interaction between react-google-maps' InfoBox, React's refs, and JavaScript's Object.keys, but it seems like it should be simple and confuses me to a loss.
Can anyone shed some light on why this might be happening??
The code looks something alike:
class SensorInfoWindow extends React.Component {
handleIconClick = () => {
// do stuff here
}
componentDidMount() {
this.addClickHandlers();
}
componentDidUpdate() {
this.addClickHandlers();
}
addClickHandlers = () => {
const keys = Object.keys(this.refs);
for(let i=0; i<keys.length; i++) {
const key = keys[i];
let element = this.refs[key];
if (element !== undefined)
element.addEventListener('click', this.handleIconClick);
}
}
render() {
const { thissensor, allsensors } = this.props;
let divsToAddHandlersTo = [];
const sensorkeys = Object.keys(allsensors);
for (let i=0; i<sensorkeys.length; i++) {
divsToAddHandlersTo.push(
<div
ref={'stringref' + i}
/>
{/* children here, using InfoBox */}
</div>
);
}
return (
<div>
{divsToAddHandlersTo}
</div>
);
}
}
This is, in essence, the component.

What is Reacts function for checking if a property applies?

Based off this Q&A:
React wrapper: React does not recognize the `staticContext` prop on a DOM element
The answer is not great for my scenario, I have a lot of props and really dislike copy-pasting with hopes whoever touches the code next updates both.
So, what I think might work is just re-purposing whatever function it is that React uses to check if a property fits to conditionally remove properties before submitting.
Something like this:
import { imaginaryIsDomAttributeFn } from "react"
...
render() {
const tooManyProps = this.props;
const justTheRightProps = {} as any;
Object.keys(tooManyProps).forEach((key) => {
if (imaginaryIsDomAttributeFn(key) === false) { return; }
justTheRightProps[key] = tooManyProps[key];
});
return <div {...justTheRightProps} />
}
I have found the DOMAttributes and HTMLAttributes in Reacts index.t.ts, and could potentially turn them into a massive array of strings to check the keys against, but... I'd rather have that as a last resort.
So, How does React do the check? And can I reuse their code for it?
The following isn't meant to be a complete answer, but something helpful for you in case I forget to come back to this post. The following code is working so far.
// reacts special properties
const SPECIAL_PROPS = [
"key",
"children",
"dangerouslySetInnerHTML",
];
// test if the property exists on a div in either given case, or lower case
// eg (onClick vs onclick)
const testDiv = document.createElement("div");
function isDomElementProp(propName: string) {
return (propName in testDiv) || (propName.toLowerCase() in testDiv) || SPECIAL_PROPS.includes(propName);
}
The React internal function to validate property names is located here: https://github.com/facebook/react/blob/master/packages/react-dom/src/shared/ReactDOMUnknownPropertyHook.js
The main thing it checks the properties against is a "possibleStandardNames" property-list here: https://github.com/facebook/react/blob/master/packages/react-dom/src/shared/possibleStandardNames.js
So to reuse their code, you can copy the property-list in possibleStandardNames.js into your project, then use it to filter out properties that aren't listed there.

How do I integrate rxjs observables with a plain React Component?

I am new to Rxjs and am trying to learn how to integrate it with a simple React component without any external wrapper/library. I got this working here:
const counter = new Subject()
class App extends Component {
state = {
number: 0
}
componentDidMount() {
counter.subscribe(
val => {
this.setState({ number: this.state.number + val })
}
)
}
increment = () => {
counter.next(+1)
}
decrement = () => {
counter.next(-1)
}
render() {
return (
<div style={styles}>
Current number {this.state.number}
<br /> <br />
<button onClick={this.increment}>Plus</button>
<button onClick={this.decrement}>Minus</button>
</div>
)
}
https://codesandbox.io/s/02j7qm2xw
I trouble is that this uses Subjects which is a known anti-pattern according to experts like Ben Lesh:
https://medium.com/#benlesh/on-the-subject-of-subjects-in-rxjs-2b08b7198b93
I tried doing this:
var counter = Observable.create(function (observer) {
// Yield a single value and complete
observer.next(0);
// Any cleanup logic might go here
return function () {
console.log('disposed');
}
});
class App extends Component {
state = {
number: 0
}
componentDidMount() {
counter.subscribe(
val => {
this.setState({ number: this.state.number + val })
}
)
}
increment = () => {
counter.next(+1)
}
decrement = () => {
counter.next(-1)
}
// - render
}
But this fails with the error: counter.next is not a function
So How would I use new Observable() or Observable.create()and use it to setState with a plain React component?
Because .next() is an Observer's method, NOT Observables.
The reason why Subject works simply because Subject itself is both an observer and an observable. When you call subject.next(), you are simply just updating the observable part, and notify all the observers about the change.
It can be quite confusing sometimes when comes to Observable and Observers. To make it simple, think of this way: Observable is someone who produces the data, a.k.a. data producers; while Observer is someone who consume the data, a.k.a. data consumer. In a simple analogy, consumer eats what is produced. For the same token, Observer(consumer) observes(eats) the observable (produced).
In your context (or at least React/Redux paradigm), Subject works better. That is because Subject has state. It keep tracks of the value over the production of data (job of the Observable). Every time the observable (the one inside Subject) changes, or update, any observers that subscribes to the Subject will get notified. See the pattern similar to redux here? Every time your redux store is updated, your view gets notified (and hence updated). In fact, if you are very used to reactive programming, you can eliminate the use of redux store completely, and fully replace them by Subjects and/or BehaviourSubjects.
For the post from Ben Lesh, he is merely stating this: Always use an Observable if possible, only use Subject when it is really needed. In that particular post, he is stating that a click event can just be an Observable; using Subject will be inappropriate. However, in your context, which is react/redux, using Subject is fine - because the Subject is used to keep track of the state of the store, and NOT the click event handler.
TLDR:
Use Subject if you want to keep track of a state of a variable
.next() is Observer's method, not Observable.

Resources