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.
Related
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.
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 */}
);
}
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?
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.
I need to resolve all React components into their respective React elements (e.g., type=div,ul,li), including all nested components that there may be. I'm doing it successfully already; however, I'm getting a warning about calling a React component directly. Also, it's quite possible I'm going about it wrong in the first place.
function resolveNestedDomElements(reactElement) {
// is there a better way to check if it's a component?
if (React.isValidElement(reactElement) && _.isFunction(reactElement.type)) {
// is there a better way to access a component's children?
reactElement = reactElement.type(reactElement.props);
// Warning: Something is calling a React component directly. Use a
// factory or JSX instead. See https://fb.me/react-legacyfactory
// Note: Unfortunately, the factory solution doesn't work.
}
if (!React.isValidElement(reactElement)) {
return reactElement;
}
let { children } = reactElement.props;
if (React.Children.count(children)) {
if (React.isValidElement(children)) {
children = resolveNestedDomElements(children);
} else {
children = React.Children.map(
children,
resolveNestedDomElements
);
}
}
return React.cloneElement(reactElement, reactElement.props, children);
}
Your original code now returns an error in 0.14.7, with no output, from React, containing the same message that used to warn (again, behavior as described in the link provided above).
Updating your code to this, as per the update here:
function resolveNestedDomElements(reactElement) {
if (React.isValidElement(reactElement) && typeof reactElement.type === 'function') {
reactClass = React.createFactory(reactElement.type)
reactElement = reactClass(Object.assign({},reactElement.props));
}
if (!React.isValidElement(reactElement)) {
return reactElement;
}
let { children } = reactElement.props;
if (React.Children.count(children)) {
if (React.isValidElement(children)) {
children = resolveNestedDomElements(children);
} else {
children = React.Children.map(
children,
resolveNestedDomElements
);
}
}
return React.cloneElement(reactElement, reactElement.props, children);
}
This no longer warns (or, in current versions, it now just returns an error in the original form you posted), but the output is not resolved, either. It's unclear to me why this has changed.
The answer seems to be that it seems impossible to resolve anymore. I can't quite follow what's happening in the React source (I believe the render functions of components eventually are called by ReactDOM.render these days, but how that works is unclear to me still). Calling ReactDOM.render doesn't work without a DOM. There is a react-test-utils.shallowRenderer that looks like it may be helpful, that I need to experiment with.
It's a bit absurd how difficult this is. I'm not sure what architecture decision I'm missing at this point.