Set and access refs on {children} from parent with cloneElement - reactjs

I'm trying to create a full page scrolling effect similar to fullpage.js.
I would like to accomplish this by using components like this:
<FullPageContainer>
<FullPageSection>1</FullPageSection>
<FullPageSection>2</FullPageSection>
<FullPageSection>3</FullPageSection>
</FullPageContainer>
The full page container should be able to control its view and scroll to any of its children via a function scroll(). Scroll, keyboard, and UI elements would trigger this function.
I was thinking I could create refs within <FullPageContainer> and set them on the children. This way no matter how many children were nested, they would all be linked up by refs. However I am having trouble with the implementation.
FullPageContainer
import React from "react";
export const FullPageContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
// Create one ref for each child
let refs = children.map(() => React.useRef<HTMLDivElement>());
const childrenWithProps = React.Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { ref: refs[index] });
}
return child;
});
return (
<div>
<button
onClick={() => {
console.log(refs);
}}
>
click
</button>
{childrenWithProps}
</div>
);
};
FullPageSection
import React from "react";
export const FullPageSection = React.forwardRef(
({
children,
ref,
}: {
children: React.ReactNode;
ref: React.RefObject<HTMLDivElement>;
}) => {
return (
<div
ref={ref}
style={{
height: "100vh",
width: "100%",
}}
>
{children}
</div>
);
}
);
I can't seem to access the refs from FullPageContainer. From my understanding, the refs should be set once the mounts, so I added a button to check the state of refs after everything renders. All of them are still coming back as undefined!

You don't need to create those ref, but use ref from child instead,
Might be this what you want?
export const FullPageContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
// Create one ref for each child
let refs = useRef([]);
const childrenWithProps = React.Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { ref: (ref) => (refs.current[index] = ref) });
}
return child;
});
return (
<div>
<button
onClick={() => {
console.log(refs.currrent);
}}
>
click
</button>
{childrenWithProps}
</div>
);
};
export const FullPageSection = ({
children
}: {
children: React.ReactNode
}) => {
return (
<div
style={{
height: "100vh",
width: "100%",
}}
>
{children}
</div>
);
};

Related

How to fix the error cannot assing "xx" to type intrinstic attributes and props using react typescript?

i am getting an error cannot find count and cannot assign {count: number; title:string} type to IntrinsicAttributes using react and typescript.
i have two components ParentComponent and ChildComponent
within parent component i am passing count prop to ChildComponent and the code is like below,
function ParentComponent = () => {
render = () => {
return (
<div>
<ChildComponent count={5} title="sometitle"/>
</div>
)
}
}
interface Props {
count: number;
title: string;
}
function ChildComponent = ({ count, title }: Props) => {
render =() =>{
return (
<>
<span>
{title}
</span>
<span>
{count}
</span>
</>
);
}
}
Could someone help me understand or fix this. thanks.
Try this:
const ParentComponent = () => {
return (
<div>
<ChildComponent count={5} title="sometitle" />
</div>
);
};
interface Props {
count: number;
title: string;
}
const ChildComponent = ({ count, title }: Props) => {
return (
<>
<span>{title}</span>
<span>{count}</span>
</>
);
};
Notes:
You can't use the React.ClassComponent method render in React.FunctionComponents (you simply return instead)
You're mixing function syntax with arrow function syntax. I used the latter above.

Assigning useRef through render props

Using Render props pattern I wanted to see if there was a way to make this work using the current setup. I have a Parent component that uses an Example component as a wrapper to render some children inside it. I wanted to pass off a ref from inside of Example to one of the children in the render prop. Is this possible ?
const Example = ({ children }) => {
const ref = useRef(null);
const [open, SetOpen] = useState(false);
const [controls] = useCustomAnimation(open, ref);
return (
<div>
{children({ ref })}
</div>
);
};
const Parent = () => {
return (
<div>
<Example>
{ref => {
<motion.div
ref={ref}
>
{console.log('ref= ', ref)}
....... more children
</motion.div>;
}}
</Example>
</div>
);
};
Yes, your current file is almost exactly correct. I setup an example, but here is the gist:
const Example = ({ children }) => {
const ref = useRef(null);
return <div>{children({ ref })}</div>;
};
const Parent = () => {
return (
<div>
<Example>
{({ ref }) => {
console.log(ref);
return <input type="text" ref={ref} />;
}}
</Example>
</div>
);
};
Note: You need to destructure the object you are passing into the children function.

PropTypes for specific children components

I’m struggling to figure out how to specify PropTypes for a set of specific children components. My dialog component is allowed to get components of the type Title, Body and/or Footer. All these components may only be used once but can appear together at the same time.
Is there a recommended way to specify an appropriate PropType?
const Title = ({ text }) => (
<h1>{ text }</h1>
);
const Body = ({ text }) => (
<p>{ text }</p>
);
const Footer = ({ text }) => (
<small>{ text }</small>
);
const Dialog = ({ children }) => (
React.Children.map(children, (child) => {
return (
<div>{child}</div>
)
})
);
Dialog.propTypes = {
children: PropTypes.oneOfType([
PropTypes.instanceOf(Title),
PropTypes.instanceOf(Body),
PropTypes.instanceOf(Footer)
]).isRequired
}
Edited
You could use a custom PropType validation to check if the prop value fits the rules your component expects: component name (Title, Body and/or Footer), if there is only one of each or not, the order of these components...
But it's overkill.
The best in my opinion it's to have these components inside the Dialog component and use props to customize it. eg:
CodeSandbox
const Title = ({ children }) => <h2>{children}</h2>;
const Dialog = ({
title,
showConfirmationButton,
showCancelButton,
onConfirmation,
onCancel,
children
}) => (
<div>
{!!title && <Title>{title}</Title>}
{children}
{(showConfirmationButton || showCancelButton) && <hr />}
{showCancelButton && <button onClick={onCancel}>Cancel</button>}
{showConfirmationButton && <button onClick={onConfirmation}>Ok</button>}
</div>
);
Dialog.propTypes = {
title: PropTypes.string,
showOkButton: PropTypes.bool,
onClickOk: PropTypes.func,
children: PropTypes.any.isRequired
};
export default function App() {
return (
<Dialog
title="Title dialog"
showConfirmationButton
onConfirmation={() => console.log("ok")}
showCancelButton
onCancel={() => console.log("cancel")}
>
<p>The Dialog body as children.</p>
</Dialog>
);
}
Or if you still need to pass the component as a prop, then the best should be:
const Dialog = ({ title, body, footer }) => (
<div>
{ title }
{ body }
{ footer }
</div>
)
Dialog.propTypes = {
title: PropTypes.instanceOf(Title),
footer: PropTypes.instanceOf(Footer),
body: PropTypes.instanceOf(Body)
}
You can check the available prop types here https://reactjs.org/docs/typechecking-with-proptypes.html

onClick event won't work in my React code

I have an onClick event inside render() that won't work, I've tried (clearly not) everything.
What am I doing wrong?
render() {
const Card = ({backgroundColor, id, state}) =>{
const style = {
width: '100px',
height:'100px',
backgroundColor: backgroundColor,
key: id,
state: state,
}
return <div style={style} />
}
const finalCards = this.state.cards.map((n) =>(
<Card backgroundColor={n.hiddenColor} key={n.id} state={n.state} onClick={() => alert('clicked')} />
));
return (
<div className="App">
{finalCards}
</div>
);
}
}
Since Card is a separate component, you should keep it declare it outside of your render method.
But the problem is that you're passing an onClick function to a component but you're not hooking up that onClick function to the HTML element (in this case the <div />).
This should work
const Card = ({backgroundColor, id, state, onClick}) =>{
const style = {
width: '100px',
height:'100px',
backgroundColor: backgroundColor,
key: id,
state: state,
}
return <div style={style} onClick={onClick} />
}

Callback function, responsible for updating state, passed as props to child component not triggering a state update

The callback function (lies in Images component) is responsible for making a state update. I'm passing that function as props to the Modal component, and within it it's being passed into the ModalPanel component.
That function is used to set the state property, display, to false which will close the modal. Currently, that function is not working as intended.
Image Component:
class Images extends Component {
state = {
display: false,
activeIndex: 0
};
handleModalDisplay = activeIndex => {
this.setState(() => {
return {
activeIndex,
display: true
};
});
};
closeModal = () => {
this.setState(() => {
return { display: false };
});
}
render() {
const { imageData, width } = this.props;
return (
<div>
{imageData.resources.map((image, index) => (
<a
key={index}
onClick={() => this.handleModalDisplay(index)}
>
<Modal
closeModal={this.closeModal}
display={this.state.display}
activeIndex={this.state.activeIndex}
selectedIndex={index}
>
<Image
cloudName={CLOUDINARY.CLOUDNAME}
publicId={image.public_id}
width={width}
crop={CLOUDINARY.CROP_TYPE}
/>
</Modal>
</a>
))}
</div>
);
}
}
export default Images;
Modal Component:
const overlayStyle = {
position: 'fixed',
zIndex: '1',
paddingTop: '100px',
left: '0',
top: '0',
width: '100%',
height: '100%',
overflow: 'auto',
backgroundColor: 'rgba(0,0,0,0.9)'
};
const button = {
borderRadius: '5px',
backgroundColor: '#FFF',
zIndex: '10'
};
class ModalPanel extends Component {
render() {
const { display } = this.props;
console.log(display)
const overlay = (
<div style={overlayStyle}>
<button style={button} onClick={this.props.closeModal}>
X
</button>
</div>
);
return <div>{display ? overlay : null}</div>;
}
}
class Modal extends Component {
render() {
const {
activeIndex,
children,
selectedIndex,
display,
closeModal
} = this.props;
let modalPanel = null;
if (activeIndex === selectedIndex) {
modalPanel = (
<ModalPanel display={this.props.display} closeModal={this.props.closeModal} />
);
}
return (
<div>
{modalPanel}
{children}
</div>
);
}
}
export default Modal;
links to code
https://github.com/philmein23/chez_portfolio/blob/chez_portfolio/components/Images.js
https://github.com/philmein23/chez_portfolio/blob/chez_portfolio/components/Modal.js
You're dealing with this modal through a very non-react and hacky way.
Essentially, in your approach, all the modals are always there, and when you click on image, ALL modals display state becomes true, and you match the index number to decide which content to show.
I suspect it's not working due to the multiple children of same key in Modal or Modal Panel.
I strongly suggest you to ditch current approach. Here's my suggestions:
Only a single <Modal/> in Images component.
Add selectedImage state to your Images component. Every time you click on an image, you set selectedImage to that clicked image object.
Pass selectedImage down to Modal to display the content you want.
This way, there is only ONE modal rendered at all time. The content changes dynamically depending on what image you click.
This is the working code I tweaked from your repo:
(I'm not sure what to display as Modal content so I display public_id of image)
Images Component
class Images extends Component {
state = {
display: false,
selectedImage: null
};
handleModalDisplay = selectedImage => {
this.setState({
selectedImage,
display: true
})
};
closeModal = () => {
//shorter way of writing setState
this.setState({display: false})
}
render() {
const { imageData, width } = this.props;
return (
<div>
<Modal
closeModal={this.closeModal}
display={this.state.display}
selectedImage={this.state.selectedImage}
/>
{imageData.resources.map((image, index) => (
<a
//Only use index as key as last resort
key={ image.public_id }
onClick={() => this.handleModalDisplay(image)}
>
<Image
cloudName={CLOUDINARY.CLOUDNAME}
publicId={image.public_id}
width={width}
crop={CLOUDINARY.CROP_TYPE}
/>
</a>
))}
</div>
);
}
}
Modal Component
class Modal extends Component {
render() {
const { display, closeModal, selectedImage } = this.props;
const overlayContent = () => {
if (!selectedImage) return null; //for when no image is selected
return (
//Here you dynamically display the content of modal using selectedImage
<h1 style={{color: 'white'}}>{selectedImage.public_id}</h1>
)
}
const overlay = (
<div style={overlayStyle}>
<button style={button} onClick={this.props.closeModal}>
X
</button>
{
//Show Modal Content
overlayContent()
}
</div>
);
return <div>{display ? overlay : null}</div>;
}
}

Resources