I have a parent component with mutiple slot components. I'm searching for a way to assign the slot component props outside the slot defintion to make it more readable.
This is my example component:
const Component = (props) => {
return (
<ParentComponent
child1={<Child1 child1Props={child1Props} />}
child2={<Child2 child2Props={child2Props} />}
/>
);
};
This would be one possible solution but is this a legit way?
const Component = (props) => {
const child1 = <Child1 child1Props={child1Props} />;
const child2 = <Child2 child2Props={child2Props} />;
return <ParentComponent child1={child1} child2={child2} />;
};
Is there any other way to achieve this?
you can make a object for it and then spread it in ParentComponent props
like this :
const Component = (props) => {
const children = {
child1:<Child1 child1Props={child1Props} />,
child2:<Child2 child1Props={child2Props} />,
}
return <ParentComponent {...children} />;
};
or use createElement:
import { createElement } from 'react';
const Component = props => {
const children = {
child1: createElement(Child1, child1Props),
child2: createElement(Child2, child2Props),
};
return <ParentComponent {...children} />;
};
Related
Problem
I have multiple Child components, which are able to pass up its state to a Parent component. I now want to be able to render multiple Parent components within a Grandparent component, and then be able to take the states of each Parent component and combine it into 1 singular state/object within the Grandparent component. Please refer to this codesandbox or look at the code below.
Child.tsx
import TextField from "#mui/material/TextField"
type Props = {
valueChange: (e: React.ChangeEvent<HTMLInputElement>) => void
id: string
}
const Child: React.FC<Props> = ({ valueChange, id }) => {
return (
<>
<TextField
id={id}
label={id}
name={id}
variant="outlined"
onChange={valueChange}
/>
</>
)
}
export default Child
Child2.tsx
import Checkbox from "#mui/material/Checkbox"
type Props = {
valueChange: (e: React.ChangeEvent<HTMLInputElement>) => void
id: string
}
const Child2: React.FC<Props> = ({ valueChange, id }) => {
return (
<>
<Checkbox name={id} onChange={valueChange} />
</>
)
}
export default Child2
Parent.tsx
import { useState } from "react"
import Child from "./Child"
import Child2 from "./Child2"
type Props = {}
const Parent: React.FC<Props> = ({}) => {
const [values, setValues] = useState({})
const valuesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const name = e.target.name
let value: any
if (name === "bool") {
value = e.target.checked
} else {
value = e.target.value
}
setValues((prev) => {
return { ...prev, [name]: value }
})
}
return (
<>
<div>{JSON.stringify({ values })}</div>
<div>
<Child valueChange={valuesChange} id={"Text1"} />
<Child valueChange={valuesChange} id={"Text2"} />
<Child2 valueChange={valuesChange} id={"bool"} />
</div>
</>
)
}
export default Parent
Grandparent.tsx
import { Button } from "#mui/material"
import Parent from "./Parent"
const Grandparent: React.FC = () => {
const buttonClick = () => {
alert(
"Want this to look like following \n" +
"data: [{Parent1 state},{Parent2 state}]"
)
}
return (
<>
<Parent />
<Parent />
<Button onClick={buttonClick}>Get Grandparent State</Button>
</>
)
}
export default Grandparent
Just move state to the highest parent (grandparent) in your case which needs them.
You can then pass what state each child component needs as props accordingly.
Thus the state in your case should be in grandpa component. It is passed to each parent as <Parent state={state.child1} /> and so on for others.
The onchange will be complex though.
I tried the following code but it fails
So, this is my Parent Component:
import React from 'react'
import ChildComponent from './ChildComponent';
const ParentComponent = (props) => {
//step 1
// const inputRef = React.createRef();
const buttonRef = React.useRef();
const focusHandler = () => {
alert("hi");
}
return (
<div>
{/* In parent, we generally pass reference to child which we dint do here, lets see if props children help here */}
{props.children}
<ChildComponent ref="buttonRef" />
</div>
)
}
export default ParentComponent;
This is my child component:
import React from 'react'
const ChildComponent = React.forwardRef((props, ref) => {
return (
<div>
<button onClick={ref.focusHandler}>Focus Input</button>
</div>
)
})
export default ChildComponent;
On click of the button above in child component, I wish to call Parent method.
How can that be achieved?
EDITED
The reason you're getting the error is because refs in function components need to be passed using ref={buttonRef}, not ref="buttonRef". Class components have a thing they can do with string refs, but it's not recommended even there.
As for calling a function from a parent component, you don't need refs to do this. So if that was the only reason you were using a ref, you can remove the ref. Instead, pass the function as a prop:
const ParentComponent = (props) => {
const focusHandler = () => {
alert("hi");
}
return (
<div>
<ChildComponent focusHandler={focusHandler} />
</div>
)
}
const ChildComponent = (props) => {
return (
<div>
<button onClick={props.focusHandler}>Focus Input</button>
</div>
)
}
Just replace ref by focusHandler like below in parent component
<ChildComponent focusHandler={focusHandler} />
Then in ChildComponent, remove ref as well.
If you wonder how to use refs in this case (even though this is not the recommended way to pass callbacks), you need to assign focusHandler key and use the ref with ref.current, refer to Components and Props docs.
const ParentComponent = () => {
const buttonRef = React.useRef({ focusHandler: () => alert("hi") });
return (
<div>
<ChildComponent ref={buttonRef} />
</div>
);
};
const ChildComponent = React.forwardRef((props, ref) => {
return (
<div>
<button onClick={ref.current.focusHandler}>Focus Input</button>
</div>
);
});
i want to set the state in Parent component on clicking a button in child component. Also i want to access this state in other child component.
what i am trying to do?
On clicking upload button (UploadButton component) i want the state isDialogOpen to be set to true. and i want to access isDialogOpen state in UserButton component.
below is the snippet,
function Main() {
return (
<Wrapper>
<React.Suspense fallback={null}>
<Switch>
<Route
exact
path="/page1"
render={routeProps => (
<Layout>
<React.Suspense fallback={<PlaceHolder></>}>
<child1 {...routeProps} />
</React.Suspense>
</Layout>
)}
/>
<Route
exact
path="/page2"
render={routeProps => (
<Layout>
<Child2 {...routeProps} />
</Layout>
)}
/>
</Switch>
</React>
</Wrapper>
)
}
function Child1() {
return (
<UploadButton/>
);
}
type Props = RouteComponentProps<{ itemId: string; productId: string }>;
function UploadButton({ match }: Props) { //here i set the state isDialogOpen
const [isDialogOpen, setDialogOpen] = React.useState(false);
const handle_click = () => {
setDialogOpen(!isDialogOpen);
};
return (
<>
<Button onClick={handle_click}/>
{isDialogOpen && (
<UploadForm/>
)}
</>
);
}
function Child2() {
return (
<UserButton/>
);
}
function UserButton() {
return (
<Icon/>
);
}
In the above snippet, isDialogOpen state is set in UploadButton component.
Now i want to modify above snippet such that the Icon component in UserButton component is hidden if isDialogOpen is true.
i want to access this isDialogOpen state in UserButton component.
what i have tried?
I can define a function in main component that sets isDialogOpen to true when Upload button is clicked in UploadButton component. but this needs passing the function as prop from main component to Upload Button and similarly passing the state to UserButton from main component.
Is there some neat way to do this? i am new to typescript and react. could someone help me solve this. thanks.
You should define state value and function which update state as props respectively to child components as props. You can take example of the code which I provide bellow
const Child1 = (props) => {
return <div>This is the counter value {props.counter}</div>
}
const Child2 = (props) => {
return <div>
<h2>Here the button to update the counter</h2>
<button onClick={props.update}>
Update counter state in the parent
</button>
</div>
}
class MainComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0
}
}
updateCounter = () => {
this.setState({counter: this.state.counter + 1});
}
render() {
return <div>
<Child1 counter={this.state.counter} />
<Child2 update={this.updateCounter} />
</div>
}
}
ReactDOM.render(<MainComponent />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can use the same component with context API and React HOOKS like this
import React, { useContext, useState} from 'react';
const CounterContext = React.createContext({
counter: 0
});
const MainComponent = (props) => {
const [counter, setCounter] = useState(0);
const updateCounter = () => {
setCounter(counter + 1);
}
return <CounterContext.Provider value={
counter,
update: updateCounter
}>
<div>
<Child1 />
<Child2 />
</div>
</CounterContext.Provider>;
}
const Child1 = (props) => {
const counter = useContext(CounterContext);
return <div>This is the counter value {counter.counter}</div>
}
const Child2 = (props) => {
const counter = useContext(CounterContext);
return <div>
<h2>Here the button to update the counter</h2>
<button onClick={counter.update}>
Update counter state in the parent
</button>
</div>
}
Is there a way to check if a component renders null ?
const ComponentA = ({ shouldRender }) => {
if(!shouldRender){
return null;
}
return <div>Foo</div>
}
const ComponentB = () => <div>Bar</div>
// I want this component to render ComponentA
const ComponentC = () => {
return <ComponentA shouldRender /> || <ComponentB />;
}
// I want this component to render ComponentB, as ComponentA returns null
const ComponentD = () => {
return <ComponentA shouldRender={false} /> || <ComponentB />;
}
Renders <div>Foo</div>, as expected
<ComponentC />
I want this to render <div>Bar</div>, but ComponentA is mounted and renders null
<ComponentD />
AFAIK, this is not possible. You will have to either render conditionally on the outer component:
const ComponentD = () => {
return shouldRender ? <ComponentA shouldRender={true} /> : <ComponentB />;
}
or render both component, but giving them the shouldRender property, as a component rendering null is actually not displayed:
const ComponentD = () => {
return (
<>
<ComponentA shouldRender={shouldRender} />
<ComponentB shouldRender={!shouldRender} />
</>
);
}
EDIT: If the condition for rendering or not ComponentA is inside the component, and complex enough for you don't want to duplicate it, you should probably externalize it in a hook, used by both components A and B.
const ComponentC = () => {
return shouldRender ? <ComponentA /> : <ComponentB />;
}
I have code similar to the following:
const HOC = (props, WrappedComponent) => {
return (
<>
<WrappedComponent />
<Icon className="props.iconStyles" />
</>
);
};
This is not actually valid code, but you can hopefully see what I'm trying to accomplish. I want to be able to use it in the following way:
<HOC iconStyles="icon-stuff">
<TheComponentIWantToWrap>
</HOC>
How can I write this correctly, so as to be able to pass props through? I think I might need to be using children here too, not sure.
It would be something like this.
const HOC = (WrappedComponent) => {
const MyComponent = props => {
return (
<>
<WrappedComponent {...props} />
<Icon className={props.iconStyles} />
</>
);
}
return MyComponent;
};
An HOC is a funtion which returns a Component and usually inject some props on it. You should separate concerns. An actual HOC should look like this
const withFoo = Component =>{
return props =>{
return <Component injectedProps='foo' />
}
}
And be called like this
const Component = withFoo(({ injectedProps }) =>{
return <jsx />
})
If you want to merge arbitrary props as well try to spread the props passed to Component
const withFoo = Wrapped =>{
return props =>{
return <Wrapped injectedProps='foo' {...props}/>
}
}
<Component propsToBeSpreaded='bar' />
If you prefer you can create an aditional layer.
HOC injects some props in a Container
Container accepts arbitrary props
Container renders children
The code
const withHoc = Container =>{
return () => <Container injected='foo' />
}
const Container = withHoc(({ children, injected, ...aditionalProps}) =>{
return(
<div {...aditionalProps}>
{ children }
</div>
)
})
And call it like
const FinalComp = () =>{
return <Container foo='foo'> <span /> </Container>
}
A higher-order component would return another component (another function in this case). A component is a function that returns JSX.
const HOC = (Component) => {
return function WrappedComponent(props) {
return (
<>
<Component {...props} />
<Icon className="props.iconStyles" />
</>
);
};
};
You could still pass down props along with a Component to be enhanced (as per your original approach which you think is wrong) It is right since -
HOCs are not part of React API. They emerge from React's nature of composition.
So your HOC usage is -
const EnhancedComponent = higherOrderComponent(WrappedComponent, anyProps);
Points to note:
Your HOC takes in a Component and returns a Component (enhanced or not)
const higherOrderComponent = (WrappedComponent, anyProps) => {
return class extends React.Component {
// Component body
}
}
Don’t Mutate the Original Component. Use Composition.
Pass Unrelated Props Through to the Wrapped Component.
const higherOrderComponent = (WrappedComponent, anyProps) => {
return class extends React.Component {
render() {
const { HOCProp, ...compProps } = this.props;
return (
<WrappedComponent
...compProps,
someProp={anyProps.someProp}
/>
);
}
}
}
Considering all this, your HOC would look like this -
const withWrappedIcon = (CompToWrap, iconStyle) => {
return (
<Icon className={iconStyle}>
<CompToWrap />
</Icon>
);
}