How to pass component constructor in React props?
I'm a beginner for React. So below example may make it clear what I am trying to achieve.
Example:
PorkComponent:
const PorkComponent = ({ children, variant }: PorkComponentProps) => {
return (
<motion.div>
...
</motion.div>
);
};
DuckComponent:
const DuckComponent = ({ children, variant }: DuckComponentProps) => {
return (
<motion.div>
...
</motion.div>
);
};
The Lunch component will contains PorkComponent or DuckComponent Based on LunchProps.meatType.
type LunchProps = {
meatType: React.ComponentType;
};
const Lunch = (props: LunchProps) => {
return (
<props.meatType> // Here it will return PorkComponent or DuckComponent based on what props.meatType.
);
}
Sure I can use something like:
const Lunch = (props: LunchProps) => {
return (
if (props.isPork) {
< PorkComponent >
} else(props.isDuck) {
< DuckComponent >
}
....
);
}
But I don't want to have multiple IFELSE check in Lunch component. Instead, I want the caller to specify the "meat type", like <Lunch meatType= PorkComponent>.
I recently saw this someone shared on Twitter,
const MeatComponents = {
'pork': <PorkComponent />
'duck': <DuckComponent />
}
const Lunch = (props) => {
return MeatComponent[props.meatType]
}
// usage
<Lunch meatType='pork' />
<Lunch meatType='duck' />
Or you could just use,
const Lunch = ({ children }) => {
return <>{children}</>
}
// usage
<Lunch>
<PorkComponent />
</Lunch>
You can simply write :
const Lunch = (props: LunchProps) => <props.meatType />;
<Lunch meatType={DuckComponent} />
Here is an example on stackblitz
You can pass the rendered component directly instead:
// in Lunch component
return (<div>{props.meatType}</div>);
<Lunch meatType={<PorkComponent />} />
Playground
If you just want to return the given component, simply use:
return props.meatType;
you can pass component functions as props. You almost got the answer.
const Lunch = (props) => {
return (
// React.createElement(props.meatType, {}, null)
<props.meatType /> // this is valid, I have tested before replying.
);
}
// then
<Lunch meatType={PorkComponent} />
// or
<Lunch meatType={DuckComponent} />
// even this works and renders a div
<Lunch meatType="div" />
Related
I was wondering if it was possible to type a JSX element (or component) that returns a single function as its child?
An example:
interface ComponentChildrenProps {
someProp: string;
}
const Component: React.FC<ComponentProps> = ({ children }): JSX.Element => {
const propsForChildren: ComponentChildrenProps = { ... }
const childrenFunction = children as (props: ComponentChildrenProps) => ReactNode;
return <div>{children && childrenFunction(propsForChildren)}</div>
}
So in this case, I return only a JSX.Element, but I was wondering if it was possible to actually type the Component to return a JSX.Element that outputs a single function as its child.
Kind of like
const Component = (props): JSX.Element<{(props: propsForChildren) => ReactNode}> =>
{ ...component code }
In essence, I would like TS to autocomplete the Component props when I open the function inside of it, and I was wondering if that was possible
Edit:
This is a kind of generic form component. I'll attach a semi practical use of it below (I stripped it from most of its inner workings not to add too much confusion):
const Form: React.FC<FormProps> = (props): JSX.Element | null => {
const { ... } = props;
const { handleSubmit, control, watch, formState } = useForm(props)
const childrenFunction = children as (props: FormChildrenProps) => ReactNode;
if (!children) return null;
return (
<form id={id} onSubmit={handleSubmit(onValidSubmit)}>
{childrenFunction({ control, formState, watch })}
</form>
);
};
Usage:
<Form>
{({ control, watch }: FormChildrenProps) => (
<section>
<TextField control={control} label="foo" name="foo" />
<TextField control={control} label="bar" name="bar" />
</section>
)}
</Form>
I guess it depends on the side effect of your function. You can certainly return multiple items from the component. JSX is merely a template language overlaid on Javascript. If you are wanting to just run the function, it may look like this:
Running the function
function Component({children}:{children:React.FC<ComponentProps>}): JSX.Element {
...
return (
<div>
{children}
{childFunction()} // or, to print to div {childFunction().toString()}
</div>
);
}
I'm not entirely sure what the purpose of the function could be, if using it as a side-effect, you should look at useEffect() or this blog here
If you just want to pass it along to the child to use, you could set up a state object or just pass it as a prop
//Parent
function ParentComponent(...): JSX.Element {
const funcForChild = () => void; //example function, either received as prop or defined here.
//using state
const [funcState, setFuncState] = useState(() => void);
...
return (
<>
<ChildComponent function={funcForChild} />
<ChildComponent function={funcState} />
</>
);
}
//Child
function Child({funcToRun}:{funcToRun: (() => void)}) //whatever your type is. Maybe unknown
useEffect(() => {
funcToRun() //runs on mount
},[]);
return(
...
);
Just returning the elements:
const Component = ({children}) => {
const jsx = <h1>Hello, World!</h1>
...
return({children, jsx});
}
Though, I don't know why you would use the latter.
Could use more clarification. Thanks.
Alrighty, I have found and answer, and tbh it was just my own lack of intelligence. I guess I just got tunnel vision.
You can simply cast the children as a function in the props
interface FormProps {
children: (values: FormChildrenProps) => ReactNode;
...
}
const Form: React.FC<FormProps> = (props): JSX.Element | null => {
const { children, ... } = props;
const { handleSubmit, control, watch, formState } = useForm(props)
if (!children) return null;
return (
<form id={id} onSubmit={handleSubmit(onValidSubmit)}>
{children({ control, formState, watch })}
</form>
);
};
So this is my parent component - it's getting a state set from it's child component when what component is updated which works great
const [filteredCases, setFilteredCases] = useState([]);
<ChildComponent
setFilteredCases={setFilteredCases}
/>
What I would like to do is also call a function whenever the setFilteredCases state is updated .. so something like this
const [filteredCases, setFilteredCases] = useState([]);
function foo() {
// do things
}
<ChildComponent
setFilteredCases={setFilteredCases, foo}
/>
But doing so seems to break the state update and never calls the function .. have I got the syntax wrong here or is there a better way to implement what I'm doing here?
Any advice / guidance / links to documentation welcome!
Try using useEffect instead
const [filteredCases, setFilteredCases] = useState([]);
const foo = () => {
// some changes
}
useEffect(() => {
// filteredCases has been updated
foo();
}, [filteredCases]);
<
ChildComponent setFilteredCases = {
setFilteredCases
}
/>
const ChildComponent = ({
setFilteredCases
}) => {
// Call the function this way:
setFilteredCases([]);
return (
// Send out something.
);
};
export default ChildComponent;
You can pass a state or method as a props from parent to child component.
Example:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
Another Example :
const [filteredCases, setFilteredCases] = useState([]);
function foo() {
// do things
}
<ChildComponent
setFilteredCases={setFilteredCases}
foo={foo}
/>
const ChildComponent = ({ foo, setFilteredCases }) => {
const onHandleClick = () => {
// Call the functions
setFilteredCases('value');
foo();
}
return (
<button onClick={onHandleClick} >Click me</button>
);
};
export default ChildComponent;
I show a cool feature of react where Component state can be used from where it's body where it's being used.
Here is an example from firebase Link
<FirebaseDatabaseNode
path="user_bookmarks/"
limitToFirst={this.state.limit}
orderByValue={"created_on"}
>
{d =>
return (
<React.Fragment>
<pre>Path {d.path}</pre>
</React.Fragment>
);
}}
</FirebaseDatabaseNode>
In this example FirebaseDatabaseNode is Component and we're accessing d inside it.
I want to implement similar so I could access data of component in similar way. Here is my attempt to implement similar state access for Dashboard Component.
export default function Dashboard({
children,
user
}: {
children: ReactNode;
user: any
}) {
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedMenu, setSelectedMenu] = React.useState(LinkItems[DEFAULT_SELECTED_ITEM])
//...
}
And I want to access selectedMenu inside of Dashboard in index.js
export default function () {
return (
<Dashboard user={user}>
{selectedMenu => {
return (
<div>{selectedMenu}</div>
)
}
}
</Dashboard>
)
}
But this is not working in my case and I don't know the exact terminology.
Finally while I explore the firebase source I found out that they are using render-and-add-props Library.
Dashboard.js
import { renderAndAddProps } from 'render-and-add-props';
//...
export default function Dashboard({
children,
user
}: {
children: ReactNode;
user: any
}) {
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedMenu, setSelectedMenu] = React.useState(LinkItems[DEFAULT_SELECTED_ITEM])
//...
return (
<div>
//...
// for rendering element with props
{renderAndAddProps(children, { 'selectedMenu': selectedMenu })}
</div>
)
}
//in index
export default function () {
return (
<Dashboard user={user}>
{({selectedMenu}) => { // {({ selectedMenu }: { selectedMenu: LinkItemProps }) if you're using typescript.
return (
<div>{selectedMenu}</div>
)
}
}
</Dashboard>
)
}
i want to fix the error "cannot invoke an object which can possibly be undefined" using react and typescript.
what i am trying to do?
I am using usecontext react hook and creating a variable from it (dialogContext) in the components (home, Dialog and books component). In doing so i get the above mentioned error.
I am defining DialogContext in helper file like below
interface ContextProps {
setDialogOpen?: (open: boolean) => void;
}
export const DialogContext = React.createContext<ContextProps>({});
And importing DialogContext in components where needed that is (Main, home, Dialog components)
function MainComponent() {
let [showDialog, setShowDialog] = React.useState(false);
return (
<DialogContext.Provider
value={{
setDialogOpen: (open: boolean) => {
if (open) {
const sessionDialogClosed = sessionStorage.getItem('dialog');
if (sessionDialogClosed !== 'closed') {
setShowDialog(open);
sessionStorage.setItem('dialog', 'closed');
}
} else {
setShowDialog(open);
}
},
}}
>
{showDialog && <Dialog DialogContext={DialogContext}/>
<Route
path="/items">
<Home />
</Route>
<Route
path="/id/item_id">
<Books/>
</Route>
</DialogContext.Provider>
)
}
function Home() {
const dialogContext= React.useContext(DialogContext);
const handleClick = () {
dialogContext.setDialogOpen(true); //get error here
}
return (
<button onClick={handleClick}>add</button>
)
}
function Books({DialogContext} : Props) {
const dialogContext= React.useContext(DialogContext);
const handleClick = () {
dialogContext.setDialogOpen(true); //get error here
}
return (
<button onClick={handleClick()}>Click me</button>
)
}
function Dialog() {
return(
<div>
//sometext
<button onClick={dialogContext.setDialogOpen(false)}> hide</button> //get errror here
</div>
)
}
What i have tried?
I have added a check for undefined where dialogContext is used in components (Books, Home, Dialog) like for example in Books components i used like below,
function Books({DialogContext} : Props) {
const dialogContext= React.useContext(DialogContext);
const handleClick = () {
if (dialogContext !== 'undefined') {
dialogContext.setDialogOpen(true); //get error here
}
}
return (
<button onClick={handleClick()}>Click me</button>
)
}
But still the error "cannot invoke an object which can possibly be undefined" throws.
Could someone help me fix this error. thanks.
Edit:
I have tried to do below and it removes the error
function Books({DialogContext} : Props) {
const dialogContext= React.useContext(DialogContext);
const handleClick = () {
if (dialogContext && dialogContext.setDialogOpen) {
dialogContext.setDialogOpen(true);
}
}
return (
<button onClick={handleClick()}>Click me</button>
)
}
But instead of adding a check like this in every component what change should i make in the DialogContext helper file or what needs to be changed to fix keep checking for undefined or not. thanks.
You have a lot of typing mistakes in your code , I've edited the answer based on your comments and here is the final result. you can also see how it's work in codesandbox just click here to see.
and here is All of your code + a some fixes + a little change in structure
import * as React from "react";
import { Route, Switch, BrowserRouter as Router } from "react-router-dom";
interface ContextProps {
setShowDialog: (open: boolean) => void;
}
const DialogContext = React.createContext<ContextProps>({
setShowDialog: (open: boolean) => {}
});
function Home() {
const dialogContext = React.useContext(DialogContext);
const handleClick = () => {
dialogContext.setShowDialog(true); //It's Ok
};
return <button onClick={handleClick}>add</button>;
}
function Books() {
const dialogContext = React.useContext(DialogContext);
const handleClick = () => {
dialogContext.setShowDialog(true); //get error here
};
return <button onClick={handleClick}>Click me</button>;
}
function Dialog() {
const dialogContext = React.useContext(DialogContext);
const handleClick = () => {
dialogContext.setShowDialog(false);
};
return (
<div>
<button onClick={handleClick}> hide</button>
</div>
);
}
export default function MainComponent() {
const [showDialog, setShowDialog] = React.useState(false);
return (
<DialogContext.Provider
value={{
setShowDialog
}}
>
{showDialog && <Dialog />}
<Router>
<Switch>
<Route path="/">
<Home />
</Route>
<Route path="/books">
<Books />
</Route>
</Switch>
</Router>
</DialogContext.Provider>
);
}
There's no sense in providing a default value for the context. It's redundant boilerplate, especially when it's complex.
Just fake the type:
const DialogContext = createContext(null as any as ContextProps);
The issue is you are saying setDialogOpen is optional in ContextProps when you provide the ?. This error will go away if you always require setDialogOpen (remove the ? from your interface definition)
interface ContextProps {
setDialogOpen: (open: boolean) => void;
}
Alternatively you can call setDialogOpen conditionally
setDialogOpen?.(true)
So, I have an icon component - For example, a search magnifying glass. Sometimes I'd like this icon to have a click function - <icon onClick={} /> which works fine. Other times I just want to use it for purely visual purposes.
I know I could pass some sort of function boolean prop. If true, return X, else Y.
The reason I don't want to do this with my icon component is because it's built up of a lot of SVG path information and it seems silly to duplicate all of that for the sake of adding an onClick event.
Is there a tidier, simpler way of doing that?
Here's an example of my code:
Function with param
export function myFunction(param) {
// Do something
}
Component 1:
import { myFunction } from "xxx";
const Icon = ({ ...props }) => {
const {
myParam,
} = props;
return ( <icon></icon> ); // Actually lots of svg stuff going on here...
}
What I'd like to happen sometimes:
return ( <icon onClick={() => myFunction(myParam)}></icon> );
This works, but I'm not sure it's the best, or cleanest way?!
import { myFunction } from "xxx";
const Icon = ({ ...props }) => {
const {
myParam,
clickEvent // true or false
} = props;
if (clickEvent ) {
return ( <icon onClick={() => myFunction(myParam)></icon> );
} else {
return ( <icon></icon> );
}
}
Component 2:
const SomeDiv = ({ ...props }) => {
const {
// Props,
} = props;
return ( <div><icon myParam="paramStuff" /></div> );
// Or
return ( <div><icon myParam="paramStuff clickEvent={true}" /></div> );
}
You could write it like this as well:
const Icon = ({ ...props }) => {
const {
myParam,
clickEvent // true or false
} = props;
const onClick = clickEvent ? () => myFunction(myParam) : undefined;
return ( <icon onClick={onClick}/> );
}
You could check for the prop if it is getting passed and type of prop is function and then based on that you can click or remove the click function. Something like below:
<icon
onClick={() => clickEvent ? myFunction(myAgrs): null } />
You can make it more generic and accept function as props you can do something like:
const { myFunction } = this.props;
<icon
onClick={() => myFunction && typeof myFunction === 'function'? myFunction(myAgrs): null } />