When I have some children(HTML dom) as props to a child component and control in parent, I found that will trigger rerender!
Why React parent pass children will trigger rerender without state/props change?
How to avoid it? Check following.
const InsideChild = React.memo(({children}) => {
const countRef = useRef(0)
countRef.current += 1
return (
<div>render count: {countRef.current} {children}</div>
)
})
const OutsideParent = () => {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<div>
Test1:
<InsideChild />
</div>
<div>
Test2:
<InsideChild>
<p>children as html dom will not trigger rerender.</p>
</InsideChild>
</div>
</div>
)
}
as sample code, Test1 will not trigger rerender, Test2 will. Is that possible to avoid it?
More detail and working sample here:
https://codepen.io/sky790312/pen/QWqygxQ
The React.memo is not working when you are sending html is becuase you the children is an object in that case, the only way you can compare it is with a deep compare or something.
You can add a comparer function to your memo, and compare the inner props of the objects in the case you send an html.
In the example i just check if the props are an object and then compare the object inside, hope it helps you
const comparisonFn = (prevProps, nextProps) => {
if(typeof prevProps?.children === 'object') {
return prevProps?.children.props.children ==
nextProps?.children.props.children
} else {
return prevProps?.children === nextProps?.children;
}
}
const InsideChild = React.memo(({children}) => {
const countRef = useRef(0)
countRef.current += 1
return (
<div>render count: {countRef.current} {children}</div>
)
}, comparisonFn)
Related
I have a container element that renders a bunch of button components within it. One of the requirements I'm trying to implement is that in case only one button is rendered, I want it to be disabled. Now, the logic that determines how many buttons will be rendered within the container is quite complicated so I can't just check the length of a list to determine that.
So I thought I would be creative and use a ref to check how many children the container has to determine whether the button inside should be disabled:
simplified code snippet:
import React, { useRef } from 'react';
const Component = () => {
const containerRef = useRef();
const isDisabled = !containerRef.current || ref.current.children.length < 2;
return (
<div ref={containerRef}>
<h3>Title</h3>
{roster.map((category) =>
category.positions.every((position) => position.isSelected) ? (
<Button disabled={isDisabled} {...otherProps} />
) : (
category.positions.map(
(position) =>
position.isSelected && (
<Button disabled={isDisabled} {...otherProps} />
)
)
)
)}
</div>
);
};
The above code works in my app but the problem is that when I'm trying to test this component, ref.current is always undefined which prevents me from testing the case where I have more than one button rendered in the container and that they are NOT disabled.
My test:
it('calls handleClick when a button is clicked', async () => {
const { user } = render(
<Component {...defaultProps} rosterPositionsConfig={config}/>
);
const firstButton = screen.getAllByRole('button')[0];
await user.click(firstButton );
expect(defaultProps.handleClick).toHaveBeenCalledTimes(1); <-- assertion failing
});
The first render of this component will always have undefined for containerRef.current, because the element on the page can't exist until after you've rendered.
So actually, the only reason your code is "working" is that your component is rendering twice (or more). The first render always has no ref and thus sets isDisabled to true, and the second one does have the ref and calculates isDisabled from that. I would guess that it's rendering twice due to <React.StrictMode>, in which case your code will stop working when you do a production build (strict mode causes a double render in dev builds only). In the test environment, you only render once, so this accidental double rendering goes away, and the bug becomes more apparent.
I recommend that you fix this by not using a ref. Instead, you can count the number of children from the data directly. There's a few ways this logic could look, but here's one:
const Component = () => {
const containerRef = useRef();
let count = 0;
for (const category of roster) {
if (category.positions.every((position) => position.isSelected)) {
count += 1;
} else {
count += category.positions.filter(
(position) => position.isSelected
).length;
}
}
const isDisabled = count <= 1;
return (
<div>
<h3>Title</h3>
{roster.map((category) =>
category.positions.every((position) => position.isSelected) ? (
<Button disabled={isDisabled} {...otherProps} />
) : (
category.positions.map(
(position) =>
position.isSelected && (
<Button disabled={isDisabled} {...otherProps} />
)
)
)
)}
</div>
);
};
If you don't like the fact that this is basically writing the same looping logic twice (i'm not a fan of that either), here's another way you might do it. You create an array of buttons assuming that they will not be disabled, but then if the length is 1, you use cloneElement to edit that JSX element:
import { cloneElement } from "react";
const Component = () => {
const containerRef = useRef();
const isDisabled = !containerRef.current || ref.current.children.length < 2;
// I'm building the array this way so that it's just a 1-d array, and it
// doesn't have any `false`s in it
const buttons = [];
roster.forEach((category) => {
if (category.positions.every((position) => position.isSelected)) {
buttons.push(<Button disabled={false} {...otherProps} />);
} else {
category.positions.forEach((position) => {
if (position.isSelected) {
buttons.push(<Button disabled={false} {...otherProps} />);
}
});
}
});
if (buttons.length === 1) {
buttons[0] = cloneElement(buttons[0], { disabled: true });
}
return (
<div ref={containerRef}>
<h3>Title</h3>
{buttons}
</div>
);
};
If you absolutely had to use a ref (which, again, i do not recommend for this case), you would need to wait until after the render is complete for the ref to be updated, then count the children, then set state to force a second render. You would probably need to use a layout effect so that this double render doesn't cause a flicker to the user:
const Component = () => {
const containerRef = useRef();
const [disabled, setDisabled] = useState(true);
useLayoutEffect(() => {
setDisabled(containerRef.current.children.length < 2);
});
return (
<div ref={containerRef}>
<h3>Title</h3>
{roster.map((category) =>
category.positions.every((position) => position.isSelected) ? (
<Button disabled={isDisabled} {...otherProps} />
) : (
category.positions.map(
(position) =>
position.isSelected && (
<Button disabled={isDisabled} {...otherProps} />
)
)
)
)}
</div>
);
};
in child component I want to update the state when user clicked on button available in parent component and I've keep track of state value as this state is also affect by other code as well so I was thinking to use useEffect() hook but I'm not sure how to achieve it.
child component:
const [sentimentButtonValue, setSentimentButtonValue] = useState(false);
return(
<>
{sentimentButtonValue}
</>
)
parent Component:
const handelTableCardOpen = (idx) => {
// when this function call, want to update child 'sentimentButtonValue' state value
console.log(idx);
setSelectedRow(idx);
};
<Button key={idx} onClick={() => handelTableCardOpen (idx)}> Click </Button>
as others have stated, you need to life the state up, BUT JUST IN CASE you have a special case where you really need it
const Child = React.forwardRef((_, ref) => {
const [sentimentButtonValue, setSentimentButtonValue] = React.useState(false);
React.useImperativeHandle(ref, () => ({
whatEver: sentimentButtonValue,
setWhatEver: setSentimentButtonValue,
}));
return <>{sentimentButtonValue.toString()}</>;
});
const Parent = () => {
const childRef = React.useRef();
const handelTableCardOpen = () => {
childRef.current.setWhatEver(!childRef.current.whatEver);
};
return (
<>
<button onClick={handelTableCardOpen}>Click</button>
<Child ref={childRef} />
</>
);
};
You need to lift the state from the child to the parent component, and then pass that state as a prop to the child:
const Parent = () => {
const [sentimentButtonValue, setSentimentButtonValue] = useState(false)
const yourFunction = () => {
setSentimentButtonValue(newValue);
}
<Button
sentimentButtonValue={sentimentButtonValue}
onClick={yourFunction}
>
Click
</Button>
}
In my container component I have a state that gets initialized with an object that I use as data.
I clone the state array to prevent the initial state from being mutated but it still gets mutated, which I don't want to happen since I will need to compare the current state with the initial state later on.
The who system is kept inside the CubeOfTruthSystem component
function CubeOfTruthSystem() {
const [cubeIndex, setCubeIndex] = useState(0);
const [faceIndex, setFaceIndex] = useState(0);
return (
<React.Fragment>
<CubeSelector handleClick={(index) => setCubeIndex(index)} />
<CubeContainer cubeIndex={cubeIndex} faceIndex={faceIndex} />
<FaceSelector handleClick={(index) => setFaceIndex(index)} />
<button id="reset-face" onClick={() => console.log(CubeOfTruth)}>
Reset
</button>
</React.Fragment>
);
}
The parent component for the state looks like this:
function CubeContainer({ cubeIndex, faceIndex }) {
const [cube, setCube] = useState(CubeOfTruthData);
const handleCellClick = (id, row) => {
const cubeClone = [...cube];
const item = cubeClone[cubeIndex].faces[faceIndex].data[0].find(
(item) => item.id === id
);
item.state = "active";
cubeClone[cubeIndex].faces[faceIndex].data = activateAdjacentCells(
id,
row,
cubeClone[cubeIndex].faces[faceIndex].data,
item
);
setCube(cubeClone);
};
return (
<div id="cube-container">
{cube[cubeIndex].faces[faceIndex].data.map((row) => {
return row.map((item) => {
return (
<CubeItem item={item} handleClick={handleCellClick} key={item.id} />
);
});
})}
</div>
);
}
And this is the child component
function CubeItem({ item, handleClick }) {
const handleBgClass = (cellData) => {
if (cellData.state === "inactive") {
return cellData.bg + "-inactive";
} else if (cellData.state === "semi-active") {
return cellData.bg + "-semi-active";
} else {
return cellData.bg;
}
};
return (
<button
className={`cell-item ${handleBgClass(item)}`}
disabled={item.state === "inactive" ? true : false}
onClick={() => handleClick(item.id, item.row)}
/>
);
}
In the CubeOfTruth component, I'm trying to get the initial state (which is the CubeOfTruth array), but after changing the state, cube, cubeClone and CubeOfTruth all have the same values.
How can I make sure CubeOfTruth never gets mutated?
You're trying to clone cube array but you're making a shallow copy of it.
If you want to prevent mutation of nested properties you should make a deep copy instead.
Replace this:
const cubeClone = [...cube];
With this:
const cubeClone = JSON.parse(JSON.stringify(cube));
Or use some library like lodash
const cubeClone = _.cloneDeep(cube)
Hello i am currently facing this situation.
here is the simplified version of the code.
const Parent = ({prop}) => {
const [listOfBool, setListOfBool] = useState([true, false])
const handleCallback = (e, ev) => {
var cloneListOfBool = [...listOfBool]
cloneListOfBool[e] = ev
setListOfBool(cloneListOfBool)
}
return (
<div>
<p>{prop}</p>
<div>{listOfBool.map((bool, idx) => <Child key={idx} prop1={idx} activeProp={bool} parentCallback={handleCallback} />)}</div>
</div>
)
}
this is the child component
const Child = ({prop1, activeProp, parentCallback}) => {
const [active, setActive] = useState(activeProp)
const setThis = (e) => {
if (active === true){
parentCallback(e, false)
setActive(false)
} else {
parentCallback(e, true)
setActive(true)
}
}
return (
<>
<p className={`${active === true ? 'selected' : ''}`} onClick={() => setThis(prop1)}>{prop1}</p>
</>
)
}
prop1 is a number, i use that number to acces the array and change its value.
in the parent i set the list of boolean values, i map through them and i create the childrens. Now when the props of the parent changes, i would like to re render every child . Everything works as i want except for this part. Later on i will need to make a request to get the list of bools. Can you tell me waht is wrong, i have tried a couple of different solutions with no succes. Thank You
You can use key to force a react component to rerender.
So in this case if prop is the parent prop that you want to listen to, you can do something similar to:
<div>
{listOfBool.map((bool, idx) => (
<Child
key={`idx_${prop}`}
prop1={idx}
active={bool}
parentCallback={handleCallback}
/>
))}
</div>
May I ask why you want the component to rerender on a prop it does not use? As much as possible, you should just pass that property to the child, as a setup like this implies some sort of side effect that might be hard to spot.
Update (see comments)
const Parent = ({prop}) => {
const [listOfBool, setListOfBool] = useState([true, false])
const setActiveAtIndex = (idx, active) => {
setListOfBool((list) => {
const newList = [...list]
newList[idx] = active
return newList
})
}
return (
<div>
<p>{prop}</p>
<div>
{listOfBool.map((bool, idx) => (
<Child
key={idx}
prop1={idx}
activeProp={bool}
setActive={active => setActiveAtIndex(idx, active)}
/>
))}
</div>
</div>
)
}
const Child = ({prop1, active, setActive, parentCallback}) => {
return (
<>
<p className={`${active ? 'selected' : ''}`} onClick={() => setActive(!active)}>{prop1}</p>
</>
)
}
There are few problems with your code. I think the children are re-rendered, but if you expect the className name to change that's not gonna happed.
prop1 is a number, but you are using a === equality when setting active. This means that it will always be false.
The second problem is that you're deconstructing props and get a var named active and then declare another one with the same name, shadowing the prop one.
You should have only one source of truth and that's the parent in this case. There is no need for a child state.
const Child = ({prop1, active, parentCallback}) => {
return (
<>
<p className={`${active === true ? 'selected' : ''}`} onClick={() => parentCallback(prop1, !active)}>{prop1}</p>
</>
)
}
And also, prop1 is a bad name.
Suppose I have the following code snippet (Please consider it as a pseudo code)
Parent.js
const [state,action]=useState(0);
return <View><Child1/><Button onPress={()=>action(1)}/></View>
Child1.js
const [state]=useState(Math.random());
return <Text>{state}</Text>
So my question is when I click the button in the parent will the Chil1 state change or not.
On my local machine it seems it changes.
The benefit of useState is that once a component is mounted, the state value does not change across re-renders until the update state function is called, passing a new state value.
Therefore, even though your parent component Button press state change triggers a rerender of the child, since the child component is only being rerendered and not unmounted/remounted, the initial state of Math.random() would remain the same.
See useState in React: A complete guide
I don't know what exact scenario is, but if you just set default state, the state will be memorized like Scenario 1
Scenario 1
In this way, the state of Child will not be changed even if Parent re-render
const Child = () => {
const [state] = useState(Math.random());
return <div>{state}</div>
}
const Parent = () => {
const [, action] = useState(true);
return (
<>
<button onClick={() => action(false)}>Not Change</button>
<Child />
</>
);
}
Scenario 2
Unless you remove it and then re-render Parent even if memorize all Child, that is
const Child = () => {
const [state] = useState(Math.random());
return <div>{state}</div>
}
const Parent = () => {
const [state, action] = useState(true);
useEffect(() => {
if (!state) action(true)
}, [state])
return (
<>
<button onClick={() => action(false)}>Change</button>
{state && <Child />}
</>
);
}
Scenario 3
By the may, if you don't use default state, in this way, it will be changed every rendering like that
const Child = () => {
return <div>{Math.random()}</div>
}
const Parent = () => {
const [, action] = useState(true);
return (
<>
<button onClick={() => action(prev => !prev)}>Change</button>
<Child />
</>
);
}
Scenario 4
If we don't want Child to re-render, we can try memo to memorize it
const Child = memo(() => {
return <div>{Math.random()}</div>
})
Scenario 5
However, when Child has props, perhaps we should invole useCallback or useMemo to make sure the values or memory addresses of props are "fixed" like constant, so that Child won't re-render
(We don't have to use useCallback or useMemo all the time, it doesn't much matter when there is no performance problem)
const Child = memo((props) => {
return <div {...props}>{Math.random()}</div>
})
const Parent = () => {
const [, action] = useState(true);
const style = useMemo(() => ({}), [])
const onOK = useCallback(() => alert(1), [])
return (
<>
<button onClick={() => action(prev => !prev)}>Change</button>
<Child className="test" style={style} onClick={onOK} />
</>
);
}