Children in Parent Functional Component are not Re-rendering on Props Change - reactjs

I'm dynamically generating children components of HOC parent (see below). I pass the props directly to one of children and set the prop in it. I expect to see child re-rendering on props change but it doesn't.
Is the code incorrect somewhere?
ParentComponent
...
const ParentComponent = ({children}) => {
const [state1, setState1] = useState(true);
...
const changeOpacity = event => setState1(!state1);
const renderChildren = React.useCallback(() => React.Children.toArray(children).map((child, index) => (
<div key={index} style={{opacity: `${state1 ? 0 : 1}`}}>
{child}
</div>
)), [state1]);
return (
<div>
<Button onClick={changeOpacity}>Toggle Opacity</Button>
{renderChildren()}
</div>
);
};
App.js
...
const App = () => {
const [prop1, setProp1] = useState(123);
return (
<ParentComponent>
<Child1 prop1={prop1} setProp1={setProp1} />
<Child2 />
</ParentComponent>
);
};

In your ParentComponent, the children are cloned and then used to render as a part of the return value from the renderChildren function. Since the logic to compute children is not run on change of props to children, your child component is not affected by a change in its prop.
You can add children dependency to useCallback and it will work fine.
const { useState, useCallback } = React;
const ParentComponent = ({children}) => {
const [state1, setState1] = useState(true);
const changeOpacity = event => setState1(!state1);
const renderChildren = useCallback(() => React.Children.map(children, (child, index) => (
<div key={index} style={{opacity: `${state1 ? 0 : 1}`}}>
{child}
</div>
)), [children, state1]);
return (
<div>
<button onClick={changeOpacity}>Toggle Opacity</button>
{renderChildren()}
</div>
);
};
const Child1 = ({prop1, setProp1}) => <div>{prop1} <button onClick={() => setProp1(234)}>Click</button></div>;
const Child2 = () => <div>Hello</div>
const App = () => {
const [prop1, setProp1] = useState(123);
return (
<ParentComponent>
<Child1 prop1={prop1} setProp1={setProp1} />
<Child2 />
</ParentComponent>
);
};
ReactDOM.render(<App />, document.getElementById('app'));
<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="app" />

Is there anything prevent you from the approach below;
const ParentComponent = ({children}) => {
const [state1, setState1] = useState(true);
...
const changeOpacity = event => setState1(!state1);
const renderChildren = useCallback(() => React.Children.toArray(children).map((child, index) => (
<div key={index}>
{child}
</div>
)), [children]);
return (
<div>
<Button onClick={changeOpacity}>Toggle Opacity</Button>
{state1 && renderChildren()}
</div>
);
};

Related

React - How to prevent parent re-render on prop change

I am making a calculator using react.
Every time I press a number button, the whole application re-renders, instead of the <Display />.
To prevent it, I tried 2 different approaches for App, But neither of them worked.
Here is the sandbox link.
Any help would be appreciated.
Put clickHandler inside of useCallback()
const App = () => {
const [screen, setScreen] = useState("0");
console.log("render");
const clickHandler = useCallback(
(val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
},
[screen]
);
return (
<div className="App">
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</div>
);
};
Put Display component inside of React.memo
const App = () => {
const [screen, setScreen] = useState("0");
console.log("render");
const clickHandler = (val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
};
const displayComponent = () => {
return (
<>
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</>
);
};
const MemoizedComponent = React.memo(displayComponent);
return (
<div className="App">
<MemoizedComponent />
</div>
);
};
And here's the ButtonList & Button component.
export const ButtonList = ({ clickHandler }) => {
const arr = [...Array.from(Array(10).keys()).reverse(), "AC"];
return (
<div className="buttons">
<div className="numbersWrapper">
{arr.map((item) => (
<Button
key={item}
clickHandler={clickHandler}
value={item.toString()}
/>
))}
</div>
</div>
);
};
export const Button = ({ value, clickHandler }) => {
return (
<button
name={value}
onClick={() => {
clickHandler(value); //where the clickEvent happens
}}
>
{value}
</button>
);
};
If you don't want a component re-render,You would have to define the click handler in another component that you would like to re-render.
So do it like this:
const App = () => {
console.log("render");
return (
<div className="App">
<childComponent />
</div>
);
};
export const childComponent = () => {
const [screen, setScreen] = useState("0");
const clickHandler = (val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
};
return (
<>
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</>
);
}
This way you prevent a particular component from re-rendering. But note that if you update a state or do anything from which causes re-renders from the parent component, It would equally re-render the child component.

useRef for element in loop in react

Using React, i have a list of ref statically declared this way:
let line1 = useRef(null);
let line2 = useRef(null);
let line3 = useRef(null);
...
//IN MY RENDER PART
<li ref={(el) => (line1 = el)}>line1</li>
<li ref={(el) => (line2 = el)}>line1</li>
<li ref={(el) => (line3 = el)}>line1</li>
the refs are then passed to an animation function and everything works correctly;
now things changed a bit and i create the list item using map and im no longer able to ref the element correctly;
i tried something like:
{menu.menu.map((D) => {
let newRef = createRef();
LiRefs.push(newRef);
return (
<li
key={D.id}
ref={(el) => (newRef = el)} >
{D.label}
</li>
);
})}
but the array i pass to the animation function is empty (i guess because the function is called inside useEffect hook and LiRefs is not yet a useRef)
i also know the number of i will create, so i can declare them at the beginning and the reference with something like
ref={(el) => (`line${i}` = el)}
which is not working
any other solution i could try?
Issue
This won't work as each render when menu is mapped it creates new react refs.
Solution
Use a ref to hold an array of generated refs, and assign them when mapping.
const lineRefs = React.useRef([]);
lineRefs.current = menu.menu.map((_, i) => lineRefs.current[i] ?? createRef());
later when mapping UI, attach the react ref stored in lineRefs at index i
{menu.menu.map((D, i) => {
return (
<li
key={D.id}
ref={lineRefs.current[i]} // <-- attach stored ref
{D.label}
</li>
);
})}
Mine is React Hooks version.
useMemo to create an array of refs for performance sake.
const vars = ['a', 'b'];
const childRefs = React.useMemo(
() => vars.map(()=> React.createRef()),
[vars.join(',')]
);
React will mount each ref to childRefs
{vars.map((v, i) => {
return (
<div>
<Child v={v} ref={childRefs[i]} />
<button onClick={() => showAlert(i)}> click {i}</button>
</div>
)
})
}
Here is a workable demo, hope that helps. ^_^
const Child = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
showAlert() {
window.alert("Alert from Child: " + props.v);
}
}));
return <h1>Hi, {props.v}</h1>;
});
const App = () => {
const vars = ['a', 'b'];
const childRefs = React.useMemo(
() => vars.map(()=> React.createRef()),
// maybe vars.length
[vars.join(',')]
);
function showAlert(index) {
childRefs[index].current.showAlert();
}
return (
<div>
{
vars.map((v, i) => {
return (
<div>
<Child v={v} ref={childRefs[i]} />
<button onClick={() => showAlert(i)}> click {i}</button>
</div>
)
})
}
</div>
)
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
There may be some Typescript inconsistencies and complexity in other answers. So I think the best way to use the useRef hook in a loop is:
// Declaration
const myRef = useRef([]);
myRef.current = [];
const addToRefs: (el) => void = (el) => {
if (el && !myRef.current.includes(el)) {
myRef.current.push(el);
}
};
And
// Assignment (input element example)
...
...
{anyArrayForLoop.map((item, index) => {
return (
<input
key={index}
ref={addToRefs}
/>
);
})}
...
...
Final:
// The Data
myRef.current
Instead of storing refs in an array, you could create a ref for each component within the loop.
You can also access that ref in the parent component by a function.
You could do something similar to this.
const { useRef, useState } = React;
const someArrayToMapStuff = ["a", "b", "c"];
const ListWithRef = ({ text, setDisplayWhatsInsideRef }) => {
const ref = React.useRef(null);
const logAndDisplayInnerHTML = () => {
setDisplayWhatsInsideRef(ref.current.innerHTML);
console.log(ref.current);
};
return (
<li
ref={ref}
onClick={logAndDisplayInnerHTML}
>
<button>{text}</button>
</li>
);
};
const App = () => {
const [displayWhatsInsideRef, setDisplayWhatsInsideRef] = useState("");
return (
<ul>
{someArrayToMapStuff.map(thing => <ListWithRef
key={thing}
text={thing}
setDisplayWhatsInsideRef={setDisplayWhatsInsideRef}
/>)}
{displayWhatsInsideRef && (
<h1>Look, I'm a ref displaying innerHTML: {displayWhatsInsideRef}</h1>
)}
</ul>
);
};
ReactDOM.createRoot(
document.getElementById("root")
).render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
Hopefully this helps someone.

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.

How do I make one component talk to another when they are generated with .map() in React?

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

React Hook : Send data from child to parent component

I'm looking for the easiest solution to pass data from a child component to his parent.
I've heard about using Context, pass trough properties or update props, but I don't know which one is the best solution.
I'm building an admin interface, with a PageComponent that contains a ChildComponent with a table where I can select multiple line. I want to send to my parent PageComponent the number of line I've selected in my ChildComponent.
Something like that :
PageComponent :
<div className="App">
<EnhancedTable />
<h2>count 0</h2>
(count should be updated from child)
</div>
ChildComponent :
const EnhancedTable = () => {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Click me {count}
</button>
)
};
I'm sure it's a pretty simple thing to do, I don't want to use redux for that.
A common technique for these situations is to lift the state up to the first common ancestor of all the components that needs to use the state (i.e. the PageComponent in this case) and pass down the state and state-altering functions to the child components as props.
Example
const { useState } = React;
function PageComponent() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1)
}
return (
<div className="App">
<ChildComponent onClick={increment} count={count} />
<h2>count {count}</h2>
(count should be updated from child)
</div>
);
}
const ChildComponent = ({ onClick, count }) => {
return (
<button onClick={onClick}>
Click me {count}
</button>
)
};
ReactDOM.render(<PageComponent />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
You can create a method in your parent component, pass it to child component and call it from props every time child's state changes, keeping the state in child component.
const EnhancedTable = ({ parentCallback }) => {
const [count, setCount] = useState(0);
return (
<button onClick={() => {
const newValue = count + 1;
setCount(newValue);
parentCallback(newValue);
}}>
Click me {count}
</button>
)
};
class PageComponent extends React.Component {
callback = (count) => {
// do something with value in parent component, like save to state
}
render() {
return (
<div className="App">
<EnhancedTable parentCallback={this.callback} />
<h2>count 0</h2>
(count should be updated from child)
</div>
)
}
}
To make things super simple you can actually share state setters to children and now they have the access to set the state of its parent.
example:
Assume there are 4 components as below,
function App() {
return (
<div className="App">
<GrandParent />
</div>
);
}
const GrandParent = () => {
const [name, setName] = useState("i'm Grand Parent");
return (
<>
<div>{name}</div>
<Parent setName={setName} />
</>
);
};
const Parent = params => {
return (
<>
<button onClick={() => params.setName("i'm from Parent")}>
from Parent
</button>
<Child setName={params.setName} />
</>
);
};
const Child = params => {
return (
<>
<button onClick={() => params.setName("i'm from Child")}>
from Child
</button>
</>
);
};
so grandparent component has the actual state and by sharing the setter method (setName) to parent and child, they get the access to change the state of the grandparent.
you can find the working code in below sandbox,
https://codesandbox.io/embed/async-fire-kl197
IF we Have Parent Class Component and Child function component this is how we going to access child component useStates hooks value :--
class parent extends Component() {
constructor(props){
super(props)
this.ChildComponentRef = React.createRef()
}
render(){
console.log(' check child stateValue: ',
this.ChildComponentRef.current.info);
return (<> <ChildComponent ref={this.ChildComponentRef} /> </>)
}
}
Child Component we would create using
React.forwardRef((props, ref) => (<></>))
. and
useImperativeHandle(ref, createHandle, [deps])
to customizes the instance value that is exposed to parent components
const childComponent = React.forwardRef((props, ref) => {
const [info, setInfo] = useState("")
useEffect(() => {
axios.get("someUrl").then((data)=>setInfo(data))
})
useImperativeHandle(ref, () => {
return {
info: info
}
})
return (<> <h2> Child Component <h2> </>)
})
I had to do this in type script. The object-oriented aspect would need the dev to add this callback method as a field in the interface after inheriting from parent and the type of this prop would be Function. I found this cool!
Here's an another example of how we can pass state directly to the parent.
I modified a component example from react-select library which is a CreatableSelect component. The component was originally developed as class based component, I turned it into a functional component and changed state manipulation algorithm.
import React, {KeyboardEventHandler} from 'react';
import CreatableSelect from 'react-select/creatable';
import { ActionMeta, OnChangeValue } from 'react-select';
const MultiSelectTextInput = (props) => {
const components = {
DropdownIndicator: null,
};
interface Option {
readonly label: string;
readonly value: string;
}
const createOption = (label: string) => ({
label,
value: label,
});
const handleChange = (value: OnChangeValue<Option, true>, actionMeta: ActionMeta<Option>) => {
console.group('Value Changed');
console.log(value);
console.log(`action: ${actionMeta.action}`);
console.groupEnd();
props.setValue(value);
};
const handleInputChange = (inputValue: string) => {
props.setInputValue(inputValue);
};
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
if (!props.inputValue) return;
switch (event.key) {
case 'Enter':
case 'Tab':
console.group('Value Added');
console.log(props.value);
console.groupEnd();
props.setInputValue('');
props.setValue([...props.value, createOption(props.inputValue)])
event.preventDefault();
}
};
return (
<CreatableSelect
id={props.id}
instanceId={props.id}
className="w-100"
components={components}
inputValue={props.inputValue}
isClearable
isMulti
menuIsOpen={false}
onChange={handleChange}
onInputChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Type something and press enter..."
value={props.value}
/>
);
};
export default MultiSelectTextInput;
I call it from the pages of my next js project like this
import MultiSelectTextInput from "../components/Form/MultiSelect/MultiSelectTextInput";
const NcciLite = () => {
const [value, setValue] = useState<any>([]);
const [inputValue, setInputValue] = useState<any>('');
return (
<React.Fragment>
....
<div className="d-inline-flex col-md-9">
<MultiSelectTextInput
id="codes"
value={value}
setValue={setValue}
inputValue={inputValue}
setInputValue={setInputValue}
/>
</div>
...
</React.Fragment>
);
};
As seen, the component modifies the page's (parent page's) state in which it is called.
I've had to deal with a similar issue, and found another approach, using an object to reference the states between different functions, and in the same file.
import React, { useState } from "react";
let myState = {};
const GrandParent = () => {
const [name, setName] = useState("i'm Grand Parent");
myState.name=name;
myState.setName=setName;
return (
<>
<div>{name}</div>
<Parent />
</>
);
};
export default GrandParent;
const Parent = () => {
return (
<>
<button onClick={() => myState.setName("i'm from Parent")}>
from Parent
</button>
<Child />
</>
);
};
const Child = () => {
return (
<>
<button onClick={() => myState.setName("i'm from Child")}>
from Child
</button>
</>
);
};

Resources