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.
Related
I am currently working on a project that requires dynamically injecting one component into another.
My project is using Redux, so I came up with two possible solutions which both have their advantages and disadvantages, but I don't know which one to choose. I know that by nature, React encourages composition, but I'm still curious to know if the second approach (simpler and faster to use) is still good :
export const SlideOverComponents = {
'UserCreate': UserCreate,
'UserUpdate': UserUpdate,
};
The idea is to register all components that can be injected as a key value pair, and dispatch a Redux action with the key and the props required by this component.
{(!!componentKey && !!SlideOverComponents[componentKey]) && React.createElement(SlideOverComponents[componentKey], props)}
Then in my parent container, I just read this key and use the React.createElement to display the injected one.
This solution is working fine and is easy and fast to use because I just have to register any new component to the object to make it work.
Is this approach "ok" ? Or should I use composition ?
(I'm asking from a "good practice" or "anti-pattern" point of view.)
Yes that's fine, as long as the interface between all of the SlideOverComponents are completely identical. Your code is more verbose than it needs to be. You don't need createElement either if you assign it to a variable first
const Component = SlideOverComponents[componentKey]
return (
<div>
{Component && <Component {...props} />}
</div>
)
Edit:
I noticed that you are using TypeScript from other answers. Considering that, I still think you can use Composition but with types using String Literal Types like this:
type SlideOverComponentsType = "update" | "create";
type SlideOverComponentsProps = UserUpdateProps | UserCreateProps;
type SlideOverProps = {
key: SlideOverComponentsType;
} & SlideOverComponentsProps;
function SlideOver({ key, ...props }: SlideOverProps) {
switch (key) {
case "update":
return <UserUpdate {...props} />;
case "create":
return <UserCreate {...props} />;
default:
return null; // this will never happen but need to be addressed
}
}
And with an approach like that, you don't need an "Object" to store all the possible types of SlideOverComponents. You also guarantee that the props will always be using the proper interface and if eventually, you pass it wrongly TS will warn you about that.
Again: consider using types instead of declaring "options" as objects for cases like this.
Hope that this could help you or give you some good ideas!
Original Answer:
You can still use Composition for this and create some kind of check or `switch` statement inside the "Generic" Component. That way you could avoid adding so many checks(`if`s) outside of the parent component and guarantee that eventually non-existing `keys` could fallback to a default behavior or even to an error.
There are several ways of implementing it but one using switch that I like is this one:
function UserInteraction({ key, ...props }) {
switch (key) {
case "create": {
return <UserCreate {...props} />;
}
case "update": {
return <UserUpdate {...props} />;
}
default: {
return null;
// or you could thrown an error with something like
throw new Error(`Error: key ${key} not present inside component User`);
}
}
}
You could also use the Object.keys() method to accomplish almost the same behavior:
const UserInteractionOptions = {
"create": UserCreate,
"update": UserUpdate,
}
function UserInteraction({ key, ...props }) {
if (!Object.keys(UserInteractionOptions).includes(key)) {
return null;
// or you could thrown an error with something like
throw new Error(`Error: key ${key} not present inside component User`);
}
const InteractionComponent = UserInteractionOptions[key];
return <InteractionComponent {...props} />;
}
The main idea is to isolate the logic from deciding which component to render (and if it can be rendered) inside that component.
For future reading, you could check on TypeScript and how this can be easily handled by types, coercion, and the checks for non-present keys could be made before even the code runs locally.
A little of nitpicking: you are not "injecting" a Component inside another Component. You are just passing a key to deciding if the Parent Component renders or not the Child component through a flag. The injection of one Component into another involves passing the full component as a prop and just rendering it (or customizing it, eventually).
You could look at how React decides to render the children prop and how it decides if it is null, a string, or a ReactComponent to render an actual component. Also, a good topic to research is Dependency Injection.
As a simple example, injecting a component could looks like this:
function Label({ text }) {
return <p>{text}</p>;
}
function Input({ Label, ...props }) {
return (
<div>
<Label />
<input {...props} />
</div>
);
}
Short version: I have a component type (a class or a function) and props for it. I need to "render" the component to obtain its representation in JSX elements.
(I use the quotes because I mean «render into JSX elements» not «render into UI» and I am not sure about the terminology.)
Example:
const Foo = (props) => <div><Bar>{props.x + props.y}</Bar></div>;
// is an equivalent of `const elements = <div><Bar>3</Bar></div>;`
const elements = render2elements(Foo, { x: 1, y: 2 });
function render2elements(type, props) {
/* what should be here? */
}
Long version (for background story enthusiasts, may be skipped imo)
I have a React code whose very simplified version looks like this:
function Baby(props) {
/* In fact, it does not even matter what the component renders. */
/* It is used primarily as a configuration carrier. */
}
function Mother({ children }) {
const babies = getAllBabies(React.Children.toArray(children));
const data = parseData(babies);
return buildView(data);
}
function SomeOtherComponent(props) {
const { someProps1, someProps2,
someProps3, someCondition } = someLogic(props);
return (
<Mother>
<Baby {...someProps1} />
<Baby {...someProps2} />
{someCondition ? <Baby {...someProps3} /> : null}
</Mother>
);
}
It may be strange but it works. :) Until someone wants to do a little refactoring:
function Stepmother(props) {
const { someProps1, someProps2,
someProps3, someCondition } = someLogic(props);
return (
<>
<Baby {...someProps1} />
<Baby {...someProps2} />
{someCondition ? <Baby {...someProps3} /> : null}
</>
);
}
function SomeOtherComponent(props) {
return <Mother><Stepmother {...props} /></Mother>;
}
Now the Mother receives in its children only a JSX element for the Stepmother and can not parse the JSX elements for the Baby'ies. :(
So we return to my original question: I need to "render" Stepmother and then parse its internal JSX representation. But how can I do this?
P.S. I used functional components for brevity, but of course, all examples could use class components as well.
Thank you.
Don't do that.
I strongly encourage you to just rethink this solution altogether, ESPECIALLY if
It is used primarily as a configuration carrier.
...but.
So this kinda works however there's a couple of caveats:
if a component passed to that function is a class component and has some state, you won't be able to use any of it, in general it will probably cause a ton of issues that I'm not aware of
if a component passed is a function component, you can't use any hooks. It will just throw an error at you.
function render2elements(component, props) {
if (component.prototype.isReactComponent) {
return new component(props).render();
}
return component(props);
}
So if your "babies" are really simple this technically would work. But you just shouldn't refactor it the way you want and, again, ideally rethink this whole concept.
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.
I'm trying to use an internal Javascript library which is normally used for the web and which contains millions of lines of code (hence why I would really like to use it rather than change anything in it :D) with React Native.
This library has some DOM manipulations in it which are abstracted, and I'm trying to make the abstraction work using React Native.
Basically, what we do on the web is that we pass an HTML element to the library, and the library fills it with other elements. Later on, this library may remove elements from the tree or add some, depending on what we're telling it to do.
So I'd like to be able to pass this library a ReactElement, so that React Native handles the drawing part automatically.
Here is what the library does, very very simplified:
function libraryCode(element) {
element.append(otherElement);
}
var element = document.createElementNS('div');
libraryCode(element);
return element;
And here is what I'd like to be able to do:
render() {
var element = React.createElement(Element);
libraryCode(element);
return element;
}
But ReactElement doesn't have an append function. So I was wondering if there was a way to add functions to ReactElement someway. It would allow me to create a function called append that would add the child and re-render the element. I've tried and it's readonly obviously. I wanted to extend the ReactElement class but how to tell React to use my class instead of ReactElement?
var elem = React.createElement(Element);
elem.append = function() {};
I'm also open to new ideas, I'm quite new to React/React Native so maybe I'm going all wrong with this! :)
Thanks
What you need to do is use React.cloneElement and then do your appending by passing in children. This should allow you to do:
function libraryCode(element, otherElement) {
return React.cloneElement(element, null, [otherElement]);
}
EDIT
The closest I got to getting it to work but not conform to your needs is this:
var oldCreateElement = React.createElement;
React.createElement = function(...args) {
let element = Object.assign({}, oldCreateElement(...args));
element.append = (otherElement) => {
// Unfortunately you simply can't replace the
// reference of `element` by the result of this operation
return React.cloneElement(element, null, [
React.cloneElement(otherElement, {
// You must give a `key` or React will give you a warning
key: Math.random(),
}),
]);
};
return element;
};
This will only let you do:
var element = React.createElement(Element);
element = element.append(<Text>Hi</Text>);
return element;
But not:
var element = React.createElement(Element);
element.append(<Text>Hi</Text>);
return element;
Anyway, this is very hacky and NOT recommended and frowned upon because it requires monkey-patching.
I'm using propTypes with React because I like how it warn me when I pass something dumb. But sometimes I misspell my prop or I forget to put it in my propTypes and it never get validated.
Is there a (standard) way to make React also validate that no extra props have been passed ?
I'm not sure whether there's a standard way, but you can certainly do a quick and dirty check using Object.keys.
var propsCount = Object.keys(this.props).length,
propTypesCount = Object.keys(this.propTypes).length;
if(propsCount === propTypesCount) {
// correct number of props have been passed
}
The only edge case you will have to watch for is props.children, as this arrives as an implicit property if you nest components/HTML inside your component.
If you want a more fine grained approach, then you'll have to pick out the keys and iterate them yourself, checking.
var passedPropNames = new Set(Object.keys(this.props)),
expectedPropNames = new Set(Object.keys(this.propTypes));
passedPropNames.forEach(function(propName) {
if(!expectedPropNames.has(propName)) {
console.warn('Not expecting a property called', propName);
}
});
expectedPropNames.forEach(function(propName) {
if(!passPropNames.has(propName)) {
console.warn('Expected a property called', propName);
}
});
This will do as you ask.
componentDidMount() {
let matchPropTypes = Object.keys(this.constructor.propTypes).every((a, index) => a === Object.keys(this.props)[index])
if (!matchPropTypes) {console.log('propTypes do not match props', Object.keys(this.constructor.propTypes), Object.keys(this.props))}
}