Getting component propTypes as string - reactjs

I will like to make a wrapper component that will automatically display its children's proptypes as a string.
Example
For example, my component may be:
export class abc extends Component {
static propTypes = {
disabled: PropTypes.bool,
text: PropTypes.oneOf(['a','b'])
}
}
The wrapper will take in the component:
<Wrapper>
<abc />
</Wrapper>
And output HTML that looks like:
<pre>
disabled: PropTypes.bool
text: PropTypes.oneOf(['a', 'b'])
</pre>
What I have tried
I have tried extracting props directly from the actual children, and ended up with a wrapper component that looks like this:
const wrapper = props => (<pre>
{Object.entries(props.children.type.propTypes).map((entry) => (
<div key={entry[0]}>{entry[0]}: {getPropTypeFromFunction(entry[1])}<br/></div>
))}
</pre>)
where the getPropTypeFromFunction method is:
const getPropTypeFromFunction = func => {
for (const k in PropTypes) {
switch (func) {
case PropTypes[k]:
return k
case PropTypes[k].isRequired:
return `${k}.isRequired`
default:
break;
}
}
return "Unknown PropType"
}
This however does not work when we get non-primitive (?) proptypes (eg PropTypes.onOf(['a', 'b']). Also it feels kind of hacky to have to deal with something seemingly so simple this way.
Is there some kind of elegant solution to this (ideally without any external libraries?)

Ended up using react-docgen, which uses recast internally. This works by parsing the entire source file as a string into an Abstract Syntax Tree (AST) which is then used to generate the doc. There are some libraries using this to implement documentation, such as storybook.js and their info add-on, which I ended up using.

Related

How to succinctly export a wrapped and named function without using two names

I have a function which transforms other functions:
//library:
let transform = function(OriginalComponent) {
let WrappedComponent (props) => {
//some transformation
return <OriginalComponent {...props} />
};
//I specifically need the original component to have a NON EMPTY name here
Object.defineProperty(WrappedComponent, "name", { value: OriginalComponent.name });
}
I currently use this in a file like so
export const MyWrappedComponent = transform(function MyComponent(props){
return <h1>Hello {props.name}!</h1>;
});
With this setup I currently need to use different names for the export and the function.
My question is: Can I somehow export this in one line, using just one name instead of two?
I tried:
export function transform(function MyComponent(props){
return <h1>Hello {props.name}!</h1>;
});
But that is not valid, as the export has no name.
I also thought of
export const MyComponent = transform((props) => {
return <h1>Hello {props.name}!</h1>;
});
But then transform() receives an unnamed component (and it cannot know the export name I believe?)
This is regarding the standards of a library, so I want to keep the example as clean as possible. Naming a function and then naming the export could get confusing. If I have to name both, I'd prefer to use the same name, but I don't see how.
If you want to use a named export, and you want to pass the function directly into transform, you can't (reasonably¹) get around repeating the name, like this:
export const MyComponent = transform(function MyComponent(props){
return <h1>Hello {props.name}!</h1>;
});
With this setup I currently need to use different names for the export and the function.
Thankfully, you don't; it's perfectly valid to use the same name there, as above.
For what it's worth, there are a couple of issues with the transform function that I noticed:
You can't directly write to the name property of a function, it's read-only. But you can replace the property (because it's configurable) via Object.defineProperty.
It's not returning the wrapped component.
Here's a version with those fixed:
export let transform = function (OriginalComponent) {
let WrappedComponent = (props) => {
//some transformation
return <OriginalComponent {...props} />;
};
// I specifically need the original component to have a NON EMPTY name here
Object.defineProperty(WrappedComponent, "name", {
value: OriginalComponent.name,
});
return WrappedComponent;
};
In that Object.defineProperty call, I haven't specified any of the flags (writable, configurable, enumerable), so the flags from the previous property will be used. (The property is configurable but not writable or enumerable.)
As an alternative, you could put the unwrapped components in an object:
export const components = {
MyComponent(props) {
return <h1>Hello ${props.name}!</h1>;
},
// ...
};
...and then post-process them:
for (const [name, component] of Object.entries(components)) {
components[name] = transform(component);
}
But it means that your export is the components object, not the individual components, so you'd end up with usage like this:
import { components } from "./somewhere";
const { MyComponent } = components;
// ...
...which is less than ideal. (And sadly, you can't directly destructure imports.)

Replace a string with Component in ReactJS

I'm trying to replace a string with a React Component but it's not working. It's returning [object Object].
I've tried to use renderToString and renderToStaticMarkup from react-dom/server to render the component, but no success.
import React from 'react';
const MyComponent = ({ children }) => <strong>{children}</strong>;
function App() {
const content = 'Hi #user'.replace('user', <MyComponent>user</MyComponent>);
return <div className="App" dangerouslySetInnerHTML={{ __html: content }} />;
}
export default App;
Expected result:
Hi #<strong>user</strong>
Actual result:
Hi #[object Object]
To render components, they need to return those components as part of your rendering. This is typically done with jsx tags, which get transformed into calls to React.createElement, which in turn creates objects that instruct react what to do.
But with string.replace, you're only going to produce a string, not react elements, so react has nothing to work with other than that string. And as for dangerouslySetInnerHtml, that will only work if you have actual dom content you want to insert, not react components (plus, it's dangerous).
Most of the time when you're rendering components of an unknown quantity this is done by having an array of data, which you then map to the components. For example:
function App() {
const greetings = ['Hi #', 'Aloha #'];
const content = greetings.map(greeting => (
<React.Fragment>
{greeting}
<MyComponent>user</MyComponent>
</React.Fragment>
));
return <div>{content}</div>;
}
Taking in a string and trying to interrogate that string is rather unusual, but if that's what you need to do, then you'll probably want to do something like this:
function App() {
const str = 'Hi #userAloha #user';
const greetings = str.split('user');
greetings.pop() // remove an empty string from the end of the array
const content = greetings.map(greeting => (
<React.Fragment>
{greeting}
<MyComponent>user</MyComponent>
</React.Fragment>
));
return <div>{content}</div>
}
Note that this is basically identical to my first code, except there's an extra step to turn the string input into an array.

React High Order Components

Lets says I have an array of objects where each object have structure like below
obj = {
name:"Name1",
description:"Description1",
activeState:1,
rating:5
}
const User = (obj) => {
let userActiveState = (obj.activeState === 1) ? 'Active' : 'Non Active';
return (
<tr>
<td>{obj.name}</td>
<td>{obj.description}</td>
<td>{userActiveState}</td>
</tr>
);
}
User.propTypes = {
name: PropTypes.string
description: PropTypes.string
activeState: PropTypes.number
}
User.defaultProps = {
name: "Not Available"
description:""
activeState: 0
}
I use this array to create UI using User(a stateless functional react component), but before the stateless functional react component spits out the UI I want to make some modification to the object properties which are required by the
UI (example using text instead of number for activeState) and not all the object properties are required too.
Where would I remove unwanted properties so that I can have defaultProps and proptypes defined only for the required properties and would I use a high order component which transforms and filters the obj properties?
You don't need a HoC here - a simple composition is enough:
const Component = ...;
const ComponentButDifferentFormats = ({propToSkip, propToRename, ...props}) => (
<Component
{...props}
propRenamed={propToRename}
propX={parseFloat(props.propX)}
propY={'' + props.propY}
/>
);
With this approach, you'll decouple the transformation logic from the real UI. It's really useful for example with an API response. Creating a HoC is also an option: it might be parametrized with formats, filters or even the component itself. One more function in the above example: Component => ....

Localize React & React-Native components keeping them as reusable as possible

Recently I've been trying to keep my code as reusable as possible following the Container-Component design pattern in my React and React-Native applications. I usually try to make all the components dummy and let the container do the work and pass down the data using props.
Now I'm trying to localize these components but I want to keep them reusable for further projects. So far, I have come up with to solutions:
1.- Pass every single displayed string into the component as an individual prop. Like this.
<MyAwesomeComponent
...props
string1="String1"
string2="String2"
string3="String3"
/>
2.- Pass the translation as a single object
<MyAwesomeComponent
...props
translation={translation}
/>
I personally find a better solution the first one because it becomes easier to manage default props in the component.
Which one do you think is the best approach and why? Have you find a better approach?
My final approach if it is useful for someone:
I followed #LucasOliveira approach but went a little bit further and this is what I did using ex-react-native-i18n but you can use whatever plugin you feel most comfortable with:
First I declared a helper method inside my component to return a single object with the complete translation
Pass the translation object down to the "Dummy" component
ContaninerComponent.js
class ContainerComponent extends React.Component {
...
// Load translation outside context.
loadTranslation() {
return {
string1: I18n.t('key1'),
string2: I18n.t('key2'),
string3: I18n.t('key3')
}
}
...
render() {
return(
<MyAwesomeComponent
...props
translation={this.loadTranslation()}
/>
);
}
}
Then, in the dummy component I set up a default translation, to fit the case in which the translation is not set and then I created a helper method to handle the possible not handled strings and avoid undefined values:
MyAwesomeComponent.js
const MyAwesomeComponent = ({ ..., translation }) => {
const strings = handleTranslation(translation);
return (
.... some JSX here ....
);
};
const DEFAULT_TRANSLATION = {
string1: 'Your Default String',
string2: 'Your Default String',
string3: 'Your Default String'
}
const handleTranslation = (translation) => {
if (translation === undefined) return DEFAULT_TRANSLATION;
return {
string1: (translation.string1 !== undefined) ?
translation.string1 : DEFAULT_TRANSLATION.string1;
string2: (translation.string2 !== undefined) ?
translation.string2 : DEFAULT_TRANSLATION.string2;
string3: (translation.string3 !== undefined) ?
translation.string3 : DEFAULT_TRANSLATION.string3;
}
};
And now the whole translation is safe to use from the "strings" variable.
Hope it helps!
I would go for the second approach, cause the ability to define your object outside the component declaration, gonna make your component accepts an object, a string, a date, etc... allowing you to treat then later.
Doing that :
<MyAwesomeComponent
...props
translation={translation}
/>
means our code doesn't need to know that it is being rendered , as this will be your component responsibility

React.js: How to get all props component expects?

I'm starting to unit test a React.js application and one of the challenge I'm facing is to determine all the props a component needs to mount it properly. Is there any utility to check what all is needed to mount the component successfully? Additionally the data type of those props to initialize them appropriately for component rendering.
Like one of my component is getting props from parent using spread {...props} operator. And the parent is also getting these using spread operator and then adds some additional props and passes it to child. Which makes it very difficult for me to get all props a components expects. Is there any legit way to get the list of props?
An Interesting task.
I start with:
import React from "react";
import * as PropTypes from "prop-types";
function Hello(props) {
const { boo, foo } = props;
return (
<div>
<h1>{boo}</h1>
<h2>{foo}</h2>
</div>
);
}
Hello.propTypes = {
boo: PropTypes.string,
foo: PropTypes.number
};
export default Hello;
I found this article https://blog.jim-nielsen.com/2020/proptypes-outside-of-react-in-template-literal-components/ with function:
/**
* Anytime you want to check prop types, wrap in this
* #param {function} Component
* #param {Object} propTypes
* #return {string} result of calling the component
*/
function withPropTypeChecks(Component) {
return props => {
if (Component.propTypes) {
Object.keys(props).forEach(key => {
PropTypes.checkPropTypes(
Component.propTypes,
props,
key,
Component.name
);
});
}
return Component(props);
};
}
Then I wrote another one:
const getPropsInfo = (component) => {
const result = {};
const mock = Object.keys(component.propTypes).reduce(
(acc, p) => ({ ...acc, [p]: Symbol() }),
{}
);
const catching = (arg) => {
const [, , prop, type] = `${arg}`.match(
/Warning: Failed (.*) type: Invalid .* `(.*)` of type `symbol` supplied to.*, expected `(.*)`./
);
result[prop] = type;
};
const oldConsoleError = console.error.bind(console.error);
console.error = (...arg) => catching(arg);
withPropTypeChecks(component)(mock);
console.error = oldConsoleError;
return result;
};
I chose Symbol as the less expected type.
And called it:
const propsInfo = getPropsInfo(Hello);
console.log(propsInfo);
As result I got: {boo: "string", foo: "number"}
P.S.: I have not tested this on other types. Just for fun! :)
For unit testing this will be totally OK for your parent component to check only that properties which it adds to its child. Because in unit testing you just test functionality of a particular unit. In this case you want to check that your parent component adds all the needed properties to its child and passes all the other properties that it takes (whatever they are).
In parallel you test your child component and check its functionality.
To check that two or more components interact with each other correctly you should use E2E/functional testing. In this case you will test some functionality parts of your working app. If you have some issues with component interaction they will pop up.
One way to solve this problem will be to use decorators. I understand that they are not yet fully here and as for now you might need to use typescript or such. But it is a solution to the stated problem that will allow you to decorate and attach any required information to the properties of your component. Below is an example in typescript:
#PropContainer("Textbox")
export class TextboxProps
{
#Prop(PropertyType.Mandatory)
public propA: string;
#Prop(PropertyType.Optional)
public propB?: number;
}

Resources