I have three components:
const Comp0 = () => <div>1</div>;
const Comp1 = () => <div>2</div>;
const Comp2 = () => <div>3</div>;
I have also a class, with state:
state = { activeComponent: 0 }
This activeComponent can be changed by user to 1, 2 or 0.
In render, I have:
return (
{React.createElement(`Comp${this.state.activeComponent}`)};
}
It should work... theoretically. However - Im getting a really weird error. Two errors.
Warning: <Comp0 /> is using uppercase HTML. Always use lowercase HTML tags in React.
Warning: The tag <Comp0> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
How is that possible that they appear simultaneously?
You could simply render the dynamic tag like
const Tag = `Comp${this.state.activeComponent}`;
return (
<Tag />
}
According to the docs:
You cannot use a general expression as the React element type. If you
do want to use a general expression to indicate the type of the
element, just assign it to a capitalized variable first.
In your case it doesn't work because, you are passing the string name to React.createElement whereas for a React Component you need to pass the component like
React.createElement(Comp0);
and for a normal DOM element you would pass a string like
React.createElement('div');
and since you write
`Comp${this.state.activeComponent}`
what you get is
React.createElement('Comp0')
which isn't quite understandable to react and it throws a warning
Warning: <Comp0 /> is using uppercase HTML. Always use lowercase HTML
tags in React.
If you were to create a custom component element with React.createElement, you have to pass the direct class/function, instead of its name (that's only for DOM elements), to it, e.g. React.createElement(Shoot0) instead of React.createElement('Shoot0');
You can circumvent the issue by putting the components you intend for in array and index them
const Shoot0 = () => <div>1</div>;
const Shoot1 = () => <div>2</div>;
const Shoot2 = () => <div>3</div>;
const Shoots = [Shoot0, Shoot1, Shoot2];
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
activeComponent: 0
};
}
componentDidMount() {
setInterval(() => {
this.setState((prevState) => {
return {
activeComponent: (prevState.activeComponent + 1) % 3
}
})
}, 1000)
}
render() {
return React.createElement(Shoots[this.state.activeComponent])
}
}
ReactDOM.render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
You can just do a function with a mapping like this:
const stateArray = [Comp0, Comp1, Comp2];
const getComp = (Comp) => <Comp>
const getCompFromArray = (i) => getComp(stateArray[i]);
Related
I need to create a generic HOC with will accpect an interface which will be added to component props.
I have implemented following function, but it requires two arguments instead of one. I want second argument to be taken from the Component that it's passed into the function.
export const withMoreProps = <NewProps, Props>(WrappedComponent: React.FC<Props>): React.FC<Props & NewProps> => {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
const ComponentWithMoreProps = (props: Props & NewProps) => <WrappedComponent {...props} />;
ComponentWithMoreProps.displayName = `withMoreProps(${displayName})`;
return ComponentWithMoreProps;
};
Currently when I try to use this:
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
export const Button2 = withMoreProps<{ newProperty: string }>(Button);
I get this error message
Expected 2 type arguments, but got 1.
It should work like styled-components, where you can define only additional props.
export const StyledButton = styled(Button)<{ withPadding?: boolean }>`
padding: ${({ withPadding }) => (withPadding ? '8px' : 0)};
`;
EDIT:
This is simplified version of HOC I have created in application. The real HOC is much more complex and does other stuff, but for sake of simplification I made this example to focus only on the problem I run into.
In general, you want to use the infer keyword. You can read more about it here, but in short you can think of it as a helper to "extract" a type out of a generic type.
Let's define a type that extract the prop type out of a react component.
type InferComponentProps<T> = T extends React.FC<infer R> ? R : never;
example on what it does:
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
type ButtonProps = InferComponentProps<typeof Button>; // hover over ButtonProps, see {color: string}
Now that we have this "helper type", we can move on to implement what you want - but we do run into an issue. When calling a generic function in typescript, you can't specify some of the types, and some no.
You either specify all the concrete types matching for this function call, or specify none, and let Typescript figure out the types.
function genericFunction<T1, T2>(t1: T2) {
//...
}
const a = genericFunction('foo') //ok
const b = genericFunction<number, string>('foo') //ok
const c = genericFunction<string>('foo') //error
You can track the typescript issue here.
So to solve this we need to do a small change to your code and do a function that returns a function that returns the new component. If you notice, it's exactly how styled works also, as using tagged template literals is really a function call. So there are 2 function calls in the styled components code you posted above.
so the final code looks something like this:
export const withMoreProps =
<C extends React.FC<any>>(WrappedComponent: C) =>
<NewProps extends Object>(): React.FC<InferComponentProps<C> & NewProps> => {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
//need to re-type the component
let WrappedComponentNew = WrappedComponent as React.FC<InferComponentProps<C> & NewProps>;
const ComponentWithMoreProps = (props: InferComponentProps<C> & NewProps) => <WrappedComponentNew {...props} />;
ComponentWithMoreProps.displayName = `withMoreProps(${displayName})`;
return ComponentWithMoreProps;
};
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
export const Button2 = withMoreProps(Button)<{ newProperty: string }>(); //Notice the function call at the end
If you just want a generic way to add more props to a component, you don't need the overhead of a HOC for this. You can easily achieve this using the rest and spread operator to pass on the props. (I use color on the HOC here unlike OP's example where it's on the main button, it's just a nice example)
const ColoredButton = ({color, ...other}) => <Button {...other, style: {color}/>
It's maybe slightly more code than the HOC version that basically handles passing on ...other for you. However in return:
You don't get a weird display name (withMoreProps(Button) which could be any props). Instead it will just use the function name like any other React component (e.g. ColoredButton). You'd rather have the latter while debugging.
The resulting component is as flexible as any other React component. Just add logic to the function body if you find you need it. But with HOCs there is no function body. You could add more than 1 HOC but that gets unwieldy very quickly.
Similarly, your issue with declaring the types simply goes away. It works exactly the same like the main button type.
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
export const Button2 = ({ newProperty: string, ...other }) => <Button {...other, newProperty}/>
// Original for comparison.
export const Button3 = withMoreProps<{ newProperty: string }>(Button);
Just I have been fiddling with ReactJs and ASP.net core and I am wondering why it throws an error that each component should have a key and when I have modified my code to assign a key it keeps displaying the same messagem how could I solve it?
react.development.js:401 Warning: Each child in a list should have a unique "key" prop.
Check the render method of CommentBox. See https:// fb . me/react-warning-keys for more information.
in label (created by CommentBox)
in CommentBox
class CommentBox extends React.Component {
render() {
const span = React.createElement('span', { className: "span" }, 'First Component React');
const label = React.createElement('label', { className: "label" }, span);
const brTag = React.createElement('br')
const textBox = React.createElement('input', { className: "input", placeholder: "Hola" });
const upperDiv = React.createElement('div', { key:"upperDivinput", className: "div" }, [label, brTag, textBox]);
let comments = [];
let baseUrl = 'https://localhost:44379';
let endPointComments = '/Posts/GetComments';
baseUrl = baseUrl + endPointComments;
fetch(baseUrl)
.then(response => response.json())
.then(result => {
comments = result;
console.log(result);
});
let commentsParagraph = [];
for (let i = 0; i < comments.length; i++) {
console.log(comments[i].author);
const paragItemTitle = React.createElement('p', { key:'author_${i}' ,className: "p" }, comments[i].author);
const paragraphItemText = React.createElement('p', { key: 'comment_${i}', className: "p" }, comments[i].text);
commentsParagraph.push(paragItemTitle);
commentsParagraph.push(paragraphItemText);
}
const lowerDiv = React.createElement('div', { key:"lowerDivComments", className: 'div' }, commentsParagraph);
const wrapperOuter = React.createElement('div', { key:"wapperDivComments", className:"div" }, [upperDiv, lowerDiv])
return wrapperOuter;
}
}
ReactDOM.render(<CommentBox />, document.getElementById('root'));
Yes, you should use functional components instead of class components. componentWillMount() can then be replaced by the useEffect() hook.
And you should consider using JSX as this is way easier to read then your lengthy createElement() calls. Probably best for you to start with the official React guide.
As I've mentioned in my comment use ` (=backticks) for string literals. And try to avoid using an index as a key as an index is not stable.
Here a small example of a similar component as the one you want to build that uses string literals, a good key, a functional component and an API request to request some data. Click this link to see the dummy data that is returned by the API.
function CommentBox() {
const [comments, setComments] = React.useState([]);
React.useEffect(() => {
const url = "https://gorest.co.in/public/v2/comments";
const response = fetch(url).then(resp => resp.json().then(json => setComments(json)));
}, []);
return (
<ul>
{comments.map((comment) => (
<li>
<span>{`Comment No. ${comment.id} from ${comment.name}`}</span>
<blockquote>{comment.body}</blockquote>
</li>
))}
</ul>
);
}
ReactDOM.render(<CommentBox />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Unfortunately I cannot use async/ await when using React on StackOverflow as I have to tick Babel/ ES2015 within my snippet. Usually I would use an async function in useEffect() and use await instead of chaining then().
The useEffect() hook would then look like this. Both versions result in the same output though.
React.useEffect(async () => {
const url = "https://gorest.co.in/public/v2/comments";
const response = await fetch(url);
const data = await resp.json();
setComments(data);
}, []);
Please note: you would still need to implement some error handling here.
The problem is kind of hidden with this kind of code: [label, brTag, textBox] and [upperDiv, lowerDiv].
Any time you have an array of elements, every element in that array must have a key. In this case, your label, brTag, and textBox need a key.
Also, you never want to do things like API requests inside your render function. Please use the lifecycle hooks like componentDidMount for loading data, and calling setState to trigger a new render. DO NOT call setState inside your render method.
Also, do yourself and everybody else a favor and use JSX. It's compiled a build time (not run time), so it doesn't cost you any performance and is much easier to read (note: this advice is coming from someone who initially resisted JSX). In such case, you could use react fragments and avoid having to define unnecessary keys:
<>
<label>...</label>
<br />
<input ... />
</>
I have a simple React component that injects an instance of the Rich Text Editor, TinyMCE into any page.
It is working, but sometimes a bad prop value gets through and causes errors.
I was wondering, if there is a way to check if the values of planetId or planetDescriptor are either empty or null before anything else on the page loads.
I tried wrapping all the code in this:
if(props)
{
const App = (props) => { ... }
}
But that always throws this error:
ReferenceError: props is not defined
Is there a way to check for certain values in props before I finish loading the component?
thanks!
Here is the app:
const App = (props) => {
const [planetDescriptor, setPlanetDescriptorState] = useState(props.planetDescriptor || "Planet Descriptor...");
const [planetId, setPlanetIdState] = useState(props.planetId);
const [planet, setPlanetState] = useState(props.planet);
const [dataEditor, setDataEditor] = useState();
const handleEditorChange = (data, editor) => {
setDataEditor(data);
}
const updatePlanetDescriptor = (data) => {
const request = axios.put(`/Planet/${planetId}/planetDescriptor`);
}
return (
<Editor
id={planetId.toString()}
initialValue={planetDescriptor}
init={{
selector: ".planetDescriptor",
menubar: 'edit table help'
}}
value={dataEditor}
onEditorChange={handleEditorChange}
/>
)
}
export default App;
You had the right idea in the conditional. Just need to put it inside the component rather than wrapping the whole thing. What you can try is something similar to what the react docs for conditional rendering has for a sample. What this does is it check if the props = null / undefined and then returns or renders the error state. Else it returns the Editor.
if (!props) {
return <h1>error state</h1>
}
return <Editor></Editor>
You can't wrap the code in the way you tried as you are working with JSX, not plain javascript, so you can't use the if statement there.
I suggest using a ternary, like so:
const SomeParentComponent = () => {
const propsToPass = dataFetchOrWhatever;
return (
<>
{propsToPass.planetDescriptor && propsToPass.planetId ?
<App
planetDescriptor={propsToPass.planetDescriptor}
planetId={propsToPass.planetId}
anyOtherProps={???}
/> :
null
}
</>
)
};
This will conditionally render the App component, only if both of those props exist.
You can also use && to have the same effect:
... code omitted ...
{propsToPass.planetDescriptor && propsToPass.planetId &&
<App
planetDescriptor={propsToPass.planetDescriptor}
planetId={propsToPass.planetId}
anyOtherProps={???}
/>
}
... code omitted ...
Which approach you use is largely up to preference and codebase consistency.
I am trying to refactor some code written by someone else and was wondering if the following is possible. can I create a react component that returns non html or jsx? and returns a function or something?
basically the component so far contains 4 functions and then a return block that returns some jsx that gets rendered
I want to extract 2 of these functions out into their own component as they are similar and can be reused
however, these functions just make ajax calls and call other functions rather than returning any jsx. they also require props inside them and need the props either passed in or connected to the redux store
some of the functions it calls are dispatch functions and therefore MUST be connected to the redux store. so I cannot build a standalone function
does this make sense or am I missing something. pseudo code below to demonstrate:
const component = ({...props}) => {
const func1(){
prop1()
try{
} catch(){
callingAnotherProp()
}
}
const func2(){
}
const func3(){
}
const func4(){
}
}
imagine func1 and func2 were identical. and as you can see they don't return any jsx just make further calls.
I don't think I can isolate into a sep function. I'd rather not pass the props in as args and just connect the function to the redux store and use the props that way. is this possible?
What about HOC? You can create a component with the func1()method and the render function of this component will just return {this.props.children}.
The two other options are a standalone function (you don't like it) and inheritance (I don't like it).
Not sure i fully understand your goal here but from what i understand, you want a component that will fetch data and will expose it to other components (or child components?).
There is a nice pattern called children as a function (or render props).
Basically you treat the children passed to the component as a function and you can pass the function anything (fetched data in you case).
return this.children(this.state.data)
Here is a small running example:
class Fetcher extends React.Component {
state = { result: { data: [] } };
componentDidMount() {
const { url } = this.props;
fetch(url)
.then(response => response.json())
.then(data => this.setState({ result: { data } }))
.catch(err => this.setState({ result: { error: true, message: err } }));
}
render() {
const { children } = this.props;
return children(this.state.result);
}
}
class App extends React.Component {
renderUsers = users => users.map(user => <div>{user.name}</div>);
render() {
return (
<div>
<Fetcher url={"https://jsonplaceholder.typicode.com/users"}>
{response => (
<div>
{response.data.length
? this.renderUsers(response.data)
: "Loading..."}
</div>
)}
</Fetcher>
</div>
);
}
}
const root = document.getElementById("root");
ReactDOM.render(<App />, root);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"/>
I am refactoring a stateless functional component to use branch and renderComponent from recompose.
The original component looks like this:
const Icon = props => {
const { type, name } = props
let res
if (type === 'font') {
return (<FontIcon name={name} />)
} else if (type === 'svg') {
res = (<SvgIcon glyph={name} />)
}
return res
}
The component with branch looks like this:
const isFont = props => {
const { type } = props
return type === 'font'
}
const FontIconHoC = renderComponent(FontIcon)
const SvgIconHoC = renderComponent(SvgIcon)
const Icon = branch(isFont, FontIconHoC, SvgIconHoC)
Icon.propTypes = {
type: string,
name: string
}
export default Icon
I try and render the component using:
<Icon name='crosshairs' type='font' />
The resulting error looks like this:
invariant.js:44Uncaught Error: Icon(...): A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object.
branch returns a HOC, which accepts a component and return a component, so branch(...) is a HOC and branch(...)(...) is a component.
In your case, because Icon is not a component but a HOC, so React can't render it. To fix it, you can move SvgIcon out from branch's arguments and apply it to the HOC returned by branch(...), ex:
const Icon = branch(
isFont,
FontIconHoC,
a => a
)(SvgIcon)
We apply an identity function (a => a) to the third argument of branch. You can think of the identity function is also a HOC, which basically just return the component it gets and does nothing more.
Because this pattern is used very often, so the third argument of branch is default to the identity function. As a result, we can omit it and make our code simpler:
const Icon = branch(
isFont,
FontIconHoC
)(SvgIcon)
I've created a jsfiddle for these code. You can try it here.
You can also just use an if statement instead of branch. Consider that you just had some difficulties doing what an if statement does.
Maybe time to reconsider that library ?