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 ?
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);
I'm using swr in a react project and I'm trying to generify the loading/error messages in a parent component wrapping the components loading data.
The wrapping component is a very simple component returning different messages depending on the loadingstate.
const LoadingView = ({ loading, error, children }) => {
if (error) {
return <span>Error</span>
}
if (loading) {
return <span>Loading...</span>
}
return <Container>{children}</Container>
}
And the child component:
const WipePeriodTeams = ({ wipePeriodId }) => {
const params = useParams()
const { data, error } = useSWR(
`/some-endpoint`
)
return <LoadingView loading={!data}>{console.log(data.length)}</LoadingView> <--- ReferenceError
}
The issue being that the child component's render method is always evaluated, doesn't matter if loading is true/false which could end up in a ReferenceError due to data not loaded.
Is the return value always evaluated no matter what the parent returns? Is there a way around this?
Thanks! :)
That is the correct behaviour - the evaluation of children occurs in the parent component. You are seeing an error because data is undefined, so data.length is trying to point to a property of something that doesn't exist.
One way to avoid the error is to use && separator to check if data exists before referring to its length:
<LoadingView loading={!data}>{data && console.log(data.length)}</LoadingView>
Another approach is to replace your JSX expression with a component. I understand your example has a contrived child console.log(), but in the real world you're likely to pass in another component(s). Components are functions, so not evaluated at parent level:
const ChildComponent = ({data}) => {
return (<>{console.log(data.length)}</>)
}
const Parent = () => {
const { data, error } = useSWR(
`/some-endpoint`
);
return (
<LoadingView loading={!data}>
<ChildComponent data={data} />
</LoadingView>
);
}
Live example
There'a a few other approaches to delaying evaluation of children if you dig around online, but be cautious as some of them feel like messy workarounds.
I have this following ESLint warning :
React Hook "useBuilderFeatureFlagContext" is called in function "Slide.RenderBuilder" that is neither a React function component nor a custom React Hook function.
and this is the following component :
Slide.RenderBuilder = ({ children }) => {
const flags = useBuilderFeatureFlagContext();
return (
<>
<SlideWrapper $flags={flags}>
{children}
</SlideWrapper>
<ImageSetter attributeName="containerBackgroundImage" />
</>
);
};
How do I write a rule that can whitelist this specific case ?
If you can, define the component first, then add it to your object.
const RenderBuilder = ({ children }) => {
const flags = useBuilderFeatureFlagContext();
return (/**/);
};
Slide.RenderBuilder = RenderBuilder;
That way, the rule properly check for hooks, and you have the structure you're looking for.
Make sure to use it as a component <Slide.RenderBuilder /> otherwise you might end up breaking rules of hooks.
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 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]);