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>
Related
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>
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 have been searching for a while but can't seem to find a concrete answer to this problem. I have encountered this many times and overcame them by using "hacky" solutions. What is the recommended/standard way to get around child components re-rendering when setting parent state.
I have made a very simple example to show this problem where we have a left side child component with a count state and a right side child component which displays text when a button on the left side is clicked.
Example.js
import React, { useState } from "react";
import "../css/pages/Example.css";
function Example() {
const [rightText, setRightText] = useState();
const LeftSide = () => {
const [count, setCount] = useState(0);
return (
<div className="egleft">
<p>{count}</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
setRightText("hello world");
}}
>
Update right
</button>
</div>
);
};
const RightSide = () => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
return (
<div className="wrapper">
<LeftSide />
<RightSide />
</div>
);
}
export default Example;
Example.css
.wrapper {
width: 300px;
height: 300px;
border: 1px solid red;
display: flex;
}
.egleft {
border: 1px solid green;
width: 50%;
}
.egleft p,
.egright p {
color: black;
}
.egright {
width: 50%;
border: 1px solid blue;
}
To replicate: click add button to increment value on left side.. then click update right. You will see due to the parent's setState, left side is re-rendered, causing its own count state to be reset to the initial value.
Here is a gif showing the problem
Thanks
The issue is you are creating the LeftSide (and it's state) and RightSide components on the fly each time the Example component re-renders.
It's not common practice to create child components inside the render function of the parent as it creates an unecessary overhead (creating a new function every render instead of using the same existing function). You can split up the components into multiple functions in the root of the file, or multiple files, and compose them like #dejan-sandic answer.
But if creating the child component inside the render function is a must. You can use the useMemo hook to stop react from recreating your child component:
import React, { useState, useMemo } from "react";
import "../css/pages/Example.css";
function Example() {
const [rightText, setRightText] = useState();
const LeftSide = useMemo(() => () => {
const [count, setCount] = useState(0);
return (
<div className="egleft">
<p>{count}</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
setRightText("hello world");
}}
>
Update right
</button>
</div>
);
}, []);
const RightSide = () => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
return (
<div className="wrapper">
<LeftSide />
<RightSide />
</div>
);
}
export default Example;
But I would advise against it, as it can become unnecessarily complex, unless for something extremely dynamic.
You are defining both RightSide and the LeftSide components inside of Example. You can't do that because those two components will be created every time` Example component renders, which is happening after every statechange.
const RightSide = () => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
Do this in stead:
const LeftSide = ({ setRightText }) => {
const [count, setCount] = useState(0);
return (
<div className="egleft">
<p>{count}</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
setRightText("hello world");
}}
>
Update right
</button>
</div>
);
};
const RightSide = ({ rightText }) => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
function Example() {
const [rightText, setRightText] = useState();
return (
<div className="wrapper">
<LeftSide rightText={rightText} />
<RightSide setRightText={setRightText} />
</div>
);
}
Those are the three components I’m using, excluding the component that displays them in the DOM, but that’s not needed.
Here I have a Parent and two Child components.
For some reason when the popup is active, and I click the Refresh Child 1 Component button, it changes the state back to Child1, but I lose the functionality within that component. So the popUpToggle function stops working.
It was working fine before. When I click the Refresh Child 1 Component again however, it starts working. Why is that?
import React, { useState, useEffect } from 'react';
import Child1 from './child1'
import Child2 from './child2'
const Parent = () => {
const [display, setDisplay] = useState('');
const [popUp, setPopUp] = useState(false);
const [renderCount, setRenderCount] = useState(0);
const popUpToggle = () => {
setPopUp(!popUp)
console.log('PopUp Toggle ran')
};
const reRenderComponent= () => {
setRenderCount(renderCount + 1);
setDisplay(
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
);
popUpToggle();
console.log('reRenderComponent ran, and the key is ' + renderCount)
};
useEffect(() => {
setDisplay(
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
);
}, [])
return (
<div>
<button
style={{position: 'fixed', zIndex: '999', right: '0'}}
onClick={reRenderComponent}
>
Refresh Child 1 Component
</button>
{popUp ? <Child2 popUpToggle={popUpToggle}/> : null}
{display}
</div>
);
};
export default Parent;
Child 1:
import React from 'react';
const Child1 = ({ popUpToggle, renderCount }) => {
return (
<>
<button onClick={popUpToggle}>
Pop Up Toggle function
</button>
<h1>Child 1 is up, count is {renderCount}</h1>
</>
);
};
export default Child1;
Child 2:
import React, { useState } from 'react';
const Child2 = ({ popUpToggle }) => {
return (
<div
style={{
backgroundColor: 'rgba(0,0,0, .7)',
width: '100vw',
height: '100vh',
margin: '0',
}}
>
<h1>Child 2 is up</h1>
<h2>PopUp active</h2>
<button onClick={popUpToggle}>Toggle Pop Up</button>
</div>
);
};
export default Child2;
setDisplay(<Child1 /*etc*/ />);
Putting elements into state is usually not a good idea. It makes it very easy to cause bugs exactly like the one you're seeing. An element in state never gets updated, unless you explicitly do so, so it can easily refer to stale data. In your case, i think the issue is that the child component has a stale reference to popUpToggle, which in turn has an old instance of popUp in its closure.
The better approach, and the standard one, is for your state to contain just the minimal data. The elements get created when rendering, based on the data. That way, the elements are always in sync with the latest data.
In your case it looks like all the data already exists, so we don't need to add any new state variables:
const Parent = () => {
const [popUp, setPopUp] = useState(false);
const [renderCount, setRenderCount] = useState(0);
const popUpToggle = () => {
setPopUp(prev => !prev);
};
const reRenderComponent = () => {
setRenderCount(prev => prev + 1);
popUpToggle();
};
return (
<div>
<button
style={{ position: "fixed", zIndex: "999", right: "0" }}
onClick={reRenderComponent}
>
Refresh Child 1 Component
</button>
{popUp && <Child2 popUpToggle={popUpToggle} />}
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
</div>
);
};
I have multiple components with the same module using map().
list.map((data, index) => <MyComponent key={index} value={d}/>)
Then <p> in each of MyComponent changes colors from green to red when it is clicked.
const MyComponent = ({value}) => {
const [clicked, setClicked] = useState(false);
const buttonOnClick = () => {
setClicked(true);
}
return (
<div>
<p style={clicked ? {color: 'green'} : {color: 'red'}}>{value}</p>
<button onClick={buttonOnClick}>click</button>
</div>
);
};
In this case, I would like to turn color of <p> in other MyComponent red when one of them are clicked.
How can I check the <p> state of other MyComponent?
You need to pass a callback into your child component, and have your parent component to store and control the state. Here's an example:
const list = [1, 2, 3];
const MyComponent = ({ value, clickedValue, onClick }) => {
const style = { color: clickedValue === value ? 'green' : 'red' };
return (
<div>
<p style={style}>{value}</p>
{/* Callback with the value */}
<button onClick={() => onClick(value)}>click</button>
</div>
);
};
const App = () => {
const [clickedValue, setClickedValue] = React.useState();
const handleClick = value => {
setClickedValue(value);
};
return React.Children.toArray(
list.map(value => (
<MyComponent
value={value}
clickedValue={clickedValue}
onClick={handleClick}
/>
))
);
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Probably the easiest option would be to lift state up into the parent component: https://reactjs.org/docs/lifting-state-up.html