I have an issue with the CSS property visibility: hidden; and useEffect.
I have a simple component with two divs inside of it. Each div has a text. I want to split the letters and add a span for each. It actually works. But if I add visibility: hidden;, the letters don't exist.
Here is my component:
const MyComponent = () => {
const splitRef = useRef(null);
useEffect(() => {
const divs = splitRef.current.querySelectorAll("div");
divs.forEach((item) => {
let newInnerText = "";
const letters = item.innerText.split("");
letters.forEach((letter) => {
newInnerText += `<span>${letter}</span>`;
});
item.innerHTML = newInnerText;
});
}, []);
return (
<>
<div ref={splitRef} style={{ visibility: "hidden" }}>
<div>CONTACT</div>
<div>ABOUT</div>
</div>
</>
);
};
And I display it in my index.js:
export default function IndexPage() {
return (
<div>
<MyComponent />
</div>
);
}
I don't have errors, and without visibility: hidden; I have:
<div>
<div>
<span>C</span><span>O</span><span>N</span><span>T</span><span>A</span><span>C</span><span>T</span>
</div>
<div>
<span>A</span><span>B</span><span>O</span><span>U</span><span>T</span>
</div>
</div>
With visibility: hidden; I have empty divs:
<div style="visibility:hidden">
<div></div>
<div></div>
</div>
I tried to use useLayoutEffect, but nothing changed. And it doesn't matter if I use visibility: hidden; in CSS file. And I need visibility: hidden; for future animations.
What am I doing wrong?
It is recommended to not use querySelector... or any of the getElement... functions in React.
I solved this using a custom component which only returns its children. We map over the children, if the child element is a div we get the text create a span for all letters and return the modified child.
const SpanTextComponent = ({ children }) => {
// map over the children
const childrenWithSpanText = children.map((child) => {
// if the child element is not of type 'div' return the child
if (child.type !== "div") return child;
// get the text from the child
const text = child.props.children;
// map over all the characters in the text
const spans = text.split("").map((letter, idx) => {
// create a new span element with the letter and a key
const span = React.createElement("span", {
children: letter,
key: letter + idx,
});
// return the new span
return span;
});
// copy the child and add the new spans as children
const newChild = {
...child,
props: {
...child.props,
children: spans,
},
};
// return the new child
return newChild;
});
// return the modified children
return childrenWithSpanText;
};
You can use this like
<div style={{ visibility: "hidden" }}>
<SpanTextComponent>
<div>CONTACT</div>
<div>ABOUT</div>
</SpanTextComponent>
</div>
Related
I have two components in my project.
One is App.jsx
One is Child.jsx
Right now inside, there are 3 child components was rendered manually. The presenting data in child was passed from parent.
However, in future, I would like to add a button that can create new child on the fly.
let say, I can see a new child 4 appear after child 3, after clicking a new button.
So,
Q1: First question, since presenting data must be from parent (as I dont want to losing data after condition changing from false to true), how could I write things like creating extra state on the fly?
Q2: Second question: How to create a new component after child 3, after clicking a add child button?
For better illustrate, here is my code https://playcode.io/940784
In App.jsx:
import React,{useState,useEffect} from 'react';
import {Child} from './Child.jsx'
export function App(props) {
[message,setMessage]=useState('');
[showChild1,setShowChild1]=useState(true);
[showChild2,setShowChild2]=useState(true);
[showChild3,setShowChild3]=useState(true);
const [child1data,setChild1data] = useState('child1');
const [child2data,setChild2data] = useState('child2');
const [child3data,setChild3data] = useState('child3');
useEffect(() => {
console.log('parent was rendered')
})
return (
<div className='App'>
<button >add child</button>
<br/>
<br/>
<button onClick={()=>setShowChild1(!showChild1)}>Show child1</button>
{showChild1 && <Child key='1' data={child1data} setData={setChild1data}/>}
<br/>
<br/>
<button onClick={()=>setShowChild2(!showChild2)}>Show child2</button>
{showChild2 && <Child key='2'data={child2data} setData={setChild2data}/>}
<br/>
<br/>
<button onClick={()=>setShowChild3(!showChild3) } setData={setChild3data}>Show child3</button>
<br/>
{showChild3 && <Child key='3' data={child3data}/>}
</div>
);
}
// Log to console
console.log('Hello console')
In Child.jsx
import React, {useState, useEffect} from 'react';
export const Child = (props) => {
const {data,setData} = props;
useEffect(()=>{
console.log(data)
})
return <>
<h1>This is {data}</h1>
<input value={data} onChange={((e)=>setData(e.target.value))}></input>
</>
}
In the code snippet below, I've created an example demonstrating how to create, manage, and update an array of child state objects from a parent component. I've included lots of inline comments to help explain as you read the code:
After you Run the code snippet, you can select "Full page" to expand the viewport area of the iframe.
body, button, input { font-family: sans-serif; font-size: 1rem; } button, input { padding: 0.5rem; } ul { list-style: none; } .vertical { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; }
<div id="root"></div><script src="https://unpkg.com/react#18.2.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#18.2.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.18.12/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
// import ReactDOM from 'react-dom/client';
// import {StrictMode, useState} from 'react';
// This Stack Overflow snippet demo uses UMD modules
// instead of the commented import statments above
const {StrictMode, useState} = React;
// Returns a new child state object with a unique ID:
function getInitialChildState () {
return {
hidden: false,
id: window.crypto.randomUUID(),
text: '',
};
}
// A child component that displays a text state and allows for
// modifying the text state using a controlled input:
function Child ({text, setText}) {
return (
<div className="vertical">
<div>{text ? text : 'Empty 👀'}</div>
<input
type="text"
onChange={ev => setText(ev.target.value)}
value={text}
/>
</div>
);
}
// A wrapper component for each child that allows toggling its "hidden" property
// and conditionally renders the child according to that value:
function ChildListItem ({state, updateState}) {
const toggleHidden = () => updateState({hidden: !state.hidden});
const setText = (text) => updateState({text});
return (
<li className="vertical">
<button onClick={toggleHidden}>{
state.hidden
? 'Show'
: 'Hide'
} child</button>
{
state.hidden
? null
: <Child text={state.text} setText={setText} />
}
</li>
);
}
function App () {
// Array of child states:
const [childStates, setChildStates] = useState([]);
// Append a new child state to the end of the states array:
const addChild = () => setChildStates(arr => [...arr, getInitialChildState()]);
// Returns a function that allows updating a specific child's state
// based on its ID:
const createChildStateUpdateFn = (id) => (updatedChildState) => {
setChildStates(states => {
const childIndex = states.findIndex(state => state.id === id);
// If the ID was not found, just return the original state (don't update):
if (childIndex === -1) return states;
// Create a shallow copy of the states array:
const statesCopy = [...states];
// Get an object reference to the targeted child state:
const childState = statesCopy[childIndex];
// Replace the child state object in the array copy with a NEW object
// that includes all of the original properties and merges in all of the
// updated properties:
statesCopy[childIndex] = {...childState, ...updatedChildState};
// Return the array copy of the child states:
return statesCopy;
});
};
return (
<div>
<h1>Parent</h1>
<button onClick={addChild}>Add child</button>
<ul className="vertical">
{
childStates.map(state => (
<ChildListItem
// Every list item needs a unique key:
key={state.id}
state={state}
// Create a function for updating a child's state
// without needing its ID:
updateState={createChildStateUpdateFn(state.id)}
/>
))
}
</ul>
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root'));
reactRoot.render(
<StrictMode>
<App />
</StrictMode>
);
</script>
Use data and setData inside Child.jsx, otherwise you can not have infinite childs.
import React, {useState} from 'react';
export const Child = (props) => {
const [data, setData] = useState(props.initialData);
return <>
<h1>This is {data}</h1>
<input value={data} onChange={((e)=>setData(e.target.value))}></input>
</>
}
Now, inside your App.jsx:
const [childs, setChilds] = useState([]);
return (
<button onClick={() => setChilds([...childs, {data: {`child${childs.length}`, showChild: true} }])}>add child</button>
{
childs.length &&
childs.map(child => {
if(child.showChild){
return (
<Child initialData={child.data} />
<button onClick={() => {let newChildsArray = childs.forEach(item =>{if(item.data === child.data){child.showChild = false}} ) setChilds(newChildsArray)}}>show {child.data}</button>
)
}
}
)
Some of the concepts I used here was Rest Operator, Literal function, and Controlled Component, if you want to search further.
The better approach for this type of problem is not to use separate useState for every child.
But, to use one useState which itself is an array of objects.
For this, you can add and manipulate as per your required wish.
I like to add styles based on the props.
import {ArrowBackIcon} from './styles';
const App = () => {
...
const isFirstImageAttachment = (index) => {
return (
filteredImages.length &&
filteredImages[0]?.uuid === attachments[index]?.uuid
);
};
...
return (
<div className="App">
...
<ArrowBackIcon
isfirstimageattachment={isFirstImageAttachment(idx)}
onClick={clickBackAttachment}
/>
...
</div>
);
};
)
styles.js
export const ArrowForwardIcon = styled(ForwardArrowIcon)`
...
display: ${props => (props.isfirstimageattachment ? 'none' : 'block')}; ;
`;
isFirstImageAttachment is supposed to return true or false so depending on that value I like to hide or show ArrowForwardIcon component.
I'm getting an error saying Warning: Received false for a non-boolean attribute isfirstimageattachment.If you want to write it to the DOM, pass a string instead: isfirstimageattachment="false" or isfirstimageattachment={value.toString()}
I don't want to do isfirstimageattachment="false" because isfirstimageattachment doesn't always return false. Also this function doesn't return string so isfirstimageattachment={value.toString()} don't work the way I want to.
Is there any I can do?
Attempts
What I figured out is even though I passed string false style didn't change.
return (
<div className="App">
...
<ArrowBackIcon
isfirstimageattachment="false"
onClick={clickBackAttachment}
/>
...
</div>
I saw the element got applied display: none;
Is this not the way giving the props to styled component?
import {ArrowBackIcon} from './styles';
const App = () => {
...
const isFirstImageAttachment = (index) => {
const showEl = filteredImages.length &&
filteredImages[0]?.uuid === attachments[index]?.uuid;
return showEl ? 'hidden' : 'visible';
};
...
return (
<div className="App">
...
<ArrowBackIcon
isfirstimageattachment={isFirstImageAttachment(idx)}
/>
...
</div>
);
};
)
styles.js
export const ArrowBackIcon = styled(BackArrowIcon)`
visibility: ${props => props.isfirstimageattachment};
`;
I was able to add style dynamically this way!
I've been doing some tests about changing a parent state property affecting directly with a new child re-render.
Using onMouseEnter/onMouseLeave methods of a parent to change "isMouseOver" state --> Affects to child.
Using useHover hook
implemented using parent ref and event listener --> Affects to child
Using css Hover --> Doesn't affect to child
The problem is that parent "isMouseOver" state or "isHovered" shouldn't affect to child because it's not a child prop.
In child, I'm passing as a props "top" and "left", both setted in child styles, calculating a new top.
For each parent hover, child handleTop is triggered, giving me to undersantd that child is re-rendered for each state change of parent but in my head it has no logic because it should not re-render thanks to the virtual dom, so maybe is due the way of setting child style.
CODE:
PARENT
export default function App() {
const [isMouseOver, setIsMouseOver] = useState<boolean>(false);
const [elementRef, isHovered] = useHover();
const handleMouseOver = (isMouseOver: boolean): void => {
setIsMouseOver(isMouseOver);
};
return (
<div className={styles.App}>
<div
className={
isMouseOver
? `${styles.element} ${styles.element___isMouseOver}`
: styles.element
}
onMouseEnter={() => handleMouseOver(true)}
onMouseLeave={() => handleMouseOver(false)}
>
{/* <div ref={elementRef} className={styles.element}> */}
{/* <div className={styles.element}> */}
<PopOverComponent top={0} left={200} />
</div>
</div>
);
}
CHILD:
interface PopOverComponentProps {
top: number;
left: number;
}
export default function PopOverComponent({ top, left }: PopOverComponentProps) {
useEffect(() => {
console.log("top changes", top);
}, [top]);
const handleTop = (): number => {
const newTop = top + 25;
console.log("handleTop is triggered", newTop);
return newTop;
};
return (
<div style={{ top: handleTop(), left: left }} className={styles.popOver} />
);
}
Playground
Thank you very much in advance
You can use React.memo() for avoiding re-rendering stuff in react component. Avoiding React component re-renders with React.memo
Example :
interface PopOverComponentProps {
top: number;
left: number;
}
const PopOverComponent = React.memo(({ top, left }: PopOverComponentProps) => {
useEffect(() => {
console.log("top changes", top);
}, [top]);
const handleTop = (): number => {
const newTop = top + 25;
console.log("handleTop is triggered", newTop);
return newTop;
};
return (
<div style={{ top: handleTop(), left: left }} className={styles.popOver} />
);
})
export default PopOverComponent
I want to keep my controls as self-contained as possible, and doing so causes the need for my parent controls to access their children's functions.
Examples of this would be a list child component inside of its parent,when an additional record is added, then the parent needs to tell the child to re-fetch its data, or there is modal control on the page and action on the page triggers and the parent needs to call the modal show function.
Adding a ref to the child component and calling the method that ways works but feels incorrect and will force you to use none stateless components. The second way is passing in a prop into the child component and applying the function to that also works but if parent updates that cause the child to update the reference can get lost since the reference is just a local variable.
const Parent = ({}) => {
let childComponent = null;
return (
<React.Fragment>
<button
onClick={() => {
childComponent.show({});
}}
/>
<Child
recordId={recordId}
permissions={permissions}
actions={ref => (childComponent = ref)}
/>
</React.Fragment>
);
};
export default Parent;
const Child = ({ actions }) => {
const [show, setShow] = useState(false);
const [initValues, setInitValues] = useState(null);
if (actions) {
actions({
show: data => {
if (data) {
setInitValues(data);
}
setShow(true);
}
});
}
return (
<Modal size="md" isOpen={show} onClosed={() => handleHide()}>
</Modal>
)}
export default Child ;
What is the correct way of handling this?
Something like this?
[CodePen Mirror]
ORIGINAL:
// PARENT
function Parent(props) {
const doChildMethod = event => {
console.log("[Parent]::Invoked Child method from parent");
alert(event.target.innerHTML);
};
return (
<div style={{ border: "1px solid blue" }}>
<p>PARENT</p>
<Child iAmAChildMethod={e=>doChildMethod(e)} />
</div>
);
}
// CHILD
function Child(props) {
const handleClick = event => {
props.iAmAChildMethod(event);
console.log("[Child]::Do whatever you want in the child");
};
return (
<div style={{ border: "1px solid red", margin: "5px" }}>
<p onClick={handleClick}>CHILD - CLICK ME</p>
</div>
);
}
ReactDOM.render(<Parent />, document.body);
<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>
UPDATED:
Keep in mind this is an anti-pattern.
// Parent
const Parent = () => {
let child;
function getChildData(callbacks){
child = callbacks;
}
return (
<div>
<button onClick={() => child.getData()}>Retreive Child Data</button>
<Child onRetreiveChildData={callbacks => getChildData(callbacks)} />
</div>
);
}
// Child
const Child = ({ onRetreiveChildData }) => {
const [state, setState] = React.useState({
data: "Default Child Data"
});
if (onRetreiveChildData) {
onRetreiveChildData({
getData: () => getData("Data from Child: " + Math.random())
});
}
function getData(data){
setState({ data: data });
};
return (
<pre>{state.data}</pre>
)
};
ReactDOM.render(<Parent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>
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>;
}
}