React functional component child won't re-render when props change - javascript

I'm passing an array of objects to a child as props, and I wanted the child component to re-render when the aray changes, but it doesn't:
parent:
export default function App() {
const [items, setItems] = useState([]);
const buttonCallback = () => {
setItems([...items, { text: "new item" }]);
};
return (
<div className="App">
<h1>Items list should update when I add item</h1>
<button onClick={buttonCallback}>Add Item</button>
<Items />
</div>
);
}
child:
const Items = ({ itemlist = [] }) => {
useEffect(() => {
console.log("Items changed!"); // This gets called, so the props is changing
}, [itemlist]);
return (
<div className="items-column">
{itemlist?.length
? itemlist.map((item, i) => <Item key={i} text={item.text + i} />)
: "No items"}
<br />
{`Itemlist size: ${itemlist.length}`}
</div>
);
};
I found this question with the same issue, but it's for class components, so the solution doesn't apply to my case.
Sandbox demo

<Items propsname={data} />

const buttonCallback = () => {
setItems([...items, { text: "new item" }]);
};
but you should put it as:
const buttonCallback = () => {
setItems([...items, { text: "new item", id: Date.now() }]);
};
Because is not recommended to use index as a key for react children. Instead, you can use the actual date with that function. That is the key for React to know the children has changed.
itemlist.map((item) => <Item key={item.id} text={item.text} />)

Try below:
you are adding array as a dependency, to recognise change in variable, you should do deep copy or any other thing which will tell that useEffect obj is really different.
`
const Items = ({ itemlist = [] }) => {
const [someState,someState]=useState(itemlist);
useEffect(() => {
someState(itemlist)
}, [JSON.stringify(itemlist)]);
return (
<div className="items-column">
{someState?.length
? someState.map((item, i) => <Item key={i} text={item.text
+ i}
/>)
: "No items"}
<br />
{`Itemlist size: ${someState.length}`}
</div>
);
};

Related

React Pass the ID of clicked Element to another Component

My App.js have this structure.
return (
<Container fluid="true" className="h-100">
<Header />
<Row className="contentRow">
<CustomerList />
<DetailPage />
</Row>
</Container>
);
There are many elements in CustomerList. With a click I want to send the ID of the element to DetailPage and display the details of the associated element. But I am still quite new in react and don't really know how to pass the data. Or if I even need to change something in the structure of the components.
You need to define a new state variable in your component.
Then please pass it with the setter function into CustomerList.
Define state variable.
const [id, setId] = useState(null);
Then pass setter function into <CustomerList />
<CustomerList setId={setId} />
// on CustomerList click event
const onClick = (event) => {
// your logic and use setId from props.
// This is just an example.
props.setId(event.target.value);
}
Finally, pass id state variable into <DetailPage /> so that your DetailPage component uses in its props.
<DetailPage id={id} />
Usage in Detailpage:
const DetailPage = (props) => {
const id = props.id;
// use id for your purpose.
}
You can use the event.target.id property. add an onclick function:
onClick={(e) => handleClick(e)}
handleClick = (e) => {
//access e.target.id here
}
See if this help you:
import React, { useState } from "react";
const Header = () => <div />;
const CustomerList = ({ onChange }) => (
<ul>
{["item1", "item2", "item3", "item4"].map((item) => (
<li onClick={() => onChange(item)} key={item}>
{item}
</li>
))}
</ul>
);
const DetailPage = ({ selectedItem }) => <div>{selectedItem}</div>;
const Component = () => {
const [selectedItem, setSelectedItem] = useState(null);
const handleChange = (item) => {
setSelectedItem(item);
};
return (
<div> {/* Container */}
<Header />
<div className="contentRow"> {/* Row */}
<CustomerList onChange={handleChange} />
<DetailPage selectedItem={selectedItem} />
</div>
</div>
);
};
When you click some item, we set the state in parent component, and then send to DetailPage, in DetailPage, you can use this selectedItem to show the info.You also can replace ["item1", "item2", "item3", "item4"] with an array of objects.
App.js
import "./styles.css";
import React, { useState } from "react";
import CustomersList from "./CustomersList";
import { DetailPage } from "./DetailPage";
export default function App() {
const [listOfElements, setListOfElements] = useState([
{ name: "abc", id: "0" },
{ name: "def", id: "1" },
{ name: "ghi", id: "2" },
{ name: "jkl", id: "3" },
{ name: "mno", id: "4" }
]);
const [selectedId, setSelectedId] = useState(1);
const [customerDEatiledinfo, setCuatomerDetailedInfo] = useState({
name: "sample"
});
const idSelectedHandler = (id) => {
const idd = +id;
const newState = listOfElements[idd];
setCuatomerDetailedInfo(newState);
};
return (
<div className="App">
<CustomersList customers={listOfElements} selectId={idSelectedHandler} />
<DetailPage customer={customerDEatiledinfo} />
</div>
);
}
CustomersList.js
export const CustomersList = (props) => {
const onClickHandler = (id) => {
props.selectId(id);
};
return (
<div>
{props.customers.map((customer) => {
return (
<div key={customer.id} onClick={()=>onClickHandler(customer.id)}>
{customer.name}
</div>
);
})}
</div>
);
};
export default CustomersList;
DeatilPage.js
export const DetailPage = (props) => {
return <div style={{ color: "blue" }}>
<br/>
DetailPage
<p>{props.customer.name}</p></div>;
};

How to pass HTML attributes to child component in React?

I have a parent and a child component, child component has a button, which I'd like to disable it after the first click. This answer works for me in child component. However the function executed on click now exists in parent component, how could I pass the attribute down to the child component? I tried the following and it didn't work.
Parent:
const Home = () => {
let btnRef = useRef();
const handleBtnClick = () => {
if (btnRef.current) {
btnRef.current.setAttribute("disabled", "disabled");
}
}
return (
<>
<Card btnRef={btnRef} handleBtnClick={handleBtnClick} />
</>
)
}
Child:
const Card = ({btnRef, handleBtnClick}) => {
return (
<div>
<button ref={btnRef} onClick={handleBtnClick}>Click me</button>
</div>
)
}
In general, refs should be used only as a last resort in React. React is declarative by nature, so instead of the parent "making" the child disabled (which is what you are doing with the ref) it should just "say" that the child should be disabled (example below):
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = ({isDisabled, onButtonClick}) => {
return (
<div>
<button disabled={isDisabled} onClick={onButtonClick}>Click me</button>
</div>
)
}
Actually it works if you fix the typo in prop of Card component. Just rename hadnlBtnClick to handleBtnClick
You don't need to mention each prop/attribute by name as you can use javascript Object Destructuring here.
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = (props) => {
return (
<div>
<button {...props}>Click me</button>
</div>
)
}
You can also select a few props and use them differently in the child components. for example, see the text prop below.
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card text="I'm a Card" isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = ({text, ...restProps}) => {
return (
<div>
<button {...restProps}>{text}</button>
</div>
)
}

Is it possible to add ref to the props.children elements?

I have a Form and Input components, which are rendered as below.
<Form>
<Field />
<Field />
<Field />
</Form>
Form component will act as wrapper component here and Field component ref are not being set here. I want iterate through props.children in Form Component and want to assign a ref attribute to each children. Is there any possibility to achieve this?
You need Form to inject your refs with React.Children and React.cloneElement APIs:
const FunctionComponentForward = React.forwardRef((props, ref) => (
<div ref={ref}>Function Component Forward</div>
));
const Form = ({ children }) => {
const childrenRef = useRef([]);
useEffect(() => {
console.log("Form Children", childrenRef.current);
}, []);
return (
<>
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
ref: (ref) => (childrenRef.current[index] = ref)
})
)}
</>
);
};
const App = () => {
return (
<Form>
<div>Hello</div>
<FunctionComponentForward />
</Form>
);
};
You can map children create new instance of component based on it using one of two ways showed in React Docs.
With React.Children.map and React.cloneElement (this way, key and ref from original element are preserved)
Or only with React.Children.map (Only ref from original component is preserved)
function useRefs() {
const refs = useRef({});
const register = useCallback((refName) => ref => {
refs.current[refName] = ref;
}, []);
return [refs, register];
}
function WithoutCloneComponent({children, ...props}) {
const [refs, register] = useRefs();
return (
<Parent>
{React.Children.map((Child, index) => (
<Child.type
{...Child.props}
ref={register(`${field-${index}}`)}
/>
)}
</Parent>
)
}
function WithCloneComponent({children, ...props}) {
const [refs, register] = useRefs();
return (
<Parent>
{
React.Children.map((child, index) => React.cloneElement(
child,
{ ...child.props, ref: register(`field-${index}`) }
)
}
</Parent>
)
}

update is not capturing and unable to update the input field

please find below code which contains name id and am rendering initially using map
am replacing id value to input type in UI
with the updated input type am trying to update the value onchange
update is not capturing and unable to update the input field
any suggestion?
please refer below snippet
import React, { useState } from "react";
const CstmInput = (props) => {
return (
<input
name={props.name}
type="text"
value={props.value}
onChange={(event) => props.onInputChange(event)}
/>
);
};
export default CstmInput;
import React, { useState } from "react";
import CstmInput from "./CstmInput";
const HierarcyTest = () => {
let rowData = [
{ name: "first", id: 10 },
{ name: "second", id: 20 },
];
const [data, setData] = useState(rowData);
const [name, setName] = useState({ fn: "test" });
const onInputChange = (e) => {
console.log("---event---", e.target.value);
setName({ ...name, fn: e.target.value });
};
let updateValue = () => {
let newData = data.map(
(item, index) =>
(item.id = (
<CstmInput name={item.name} value={item.id} onInputChange={(e) => onInputChange(e)} />
))
);
setData([...data, newData]);
};
return (
<div>
<div>Testing</div>
{data.map((val) => (
<h6>
{" "}
{val.name} {val.id}
</h6>
))}
<button onClick={updateValue}> Click </button>
</div>
);
};
export default HierarcyTest;
A few things why your code isn't working as intended:
1.
let updateValue = () => {
let newData = data.map((item, index) => {
if (item.id === 10) {
return [
(item.id = (
<CstmInput
value={item.id}
onInputChange={(e) => onInputChange(e)}
/>
)),
];
}
});
setData([...data, newData]);
};
In the above function inside the callback of map, you're only returning when a condition satisfies. Are you trying to filter the array instead? If not then return something when the if condition fails.
And why are you returning an array?
return [
(item.id = (
<CstmInput
value={item.id}
onInputChange={(e) => onInputChange(e)}
/>
)),
];
the above code seems logically wrong.
2.
const onInputChange = (e) => {
console.log("---event---", e.target.value);
setName({ ...name, fn: e.target.value });
};
If you want to update state which depends on the previous state then this is how you do it:
setName((prevState) => ({ ...prevState, fn: e.target.value }));
but since you're not actually relying on the properties of the previous state you can just use:
setName({fn: e.target.value });
Note that since your state only has one property and you want to update that single property you can completely overwrite the state, you don't need to spread the previous state.
update
change the updateValue function as the following:
let updateValue = () => {
setData(prevData => {
return prevData.map(el => {
return { ...el, id: <CstmInput value={el.id} onInputChange={(e) => onInputChange(e)} /> };
})
});
};
A stackblitz example I've created that implements what you're trying to do.

How to implement multiple checkbox using react hook

I want to implement multiple checkboxes on my HTML page using react-hook.
I tried implementing using this URL: https://medium.com/#Zh0uzi/my-concerns-with-react-hooks-6afda0acc672. In the provided link it is done using class component and working perfectly but whenever I am using React hook setCheckedItems to update checkbox checked status it's not re-rendering the view.
The very first time the view is rendering and console.log() is printing from Checkbox component. After clicking on checkbox function handleChange gets called and checkedItems updates the value but the view is not rendering again (no console.log() printing). And {checkedItems.get("check-box-1")} is also not printing any value.
Below is my sample code.
CheckboxExample :
import React, { useState } from 'react';
import Checkbox from '../helper/Checkbox';
const CheckboxExample = () => {
const [checkedItems, setCheckedItems] = useState(new Map());
const handleChange = (event) => {
setCheckedItems(checkedItems => checkedItems.set(event.target.name, event.target.checked));
console.log("checkedItems: ", checkedItems);
}
const checkboxes = [
{
name: 'check-box-1',
key: 'checkBox1',
label: 'Check Box 1',
},
{
name: 'check-box-2',
key: 'checkBox2',
label: 'Check Box 2',
}
];
return (
<div>
<lable>Checked item name : {checkedItems.get("check-box-1")} </lable> <br/>
{
checkboxes.map(item => (
<label key={item.key}>
{item.name}
<Checkbox name={item.name} checked={checkedItems.get(item.name)} onChange={handleChange} />
</label>
))
}
</div>
);
}
export default Example;
Checkbox:
import React from 'react';
const Checkbox = ({ type = 'checkbox', name, checked = false, onChange }) => {
console.log("Checkbox: ", name, checked);
return (<input type={type} name={name} checked={checked} onChange={onChange} /> )
}
export default Checkbox;
I don't think using a Map to represent the state is the best idea.
I have implemented your example using a plain Object and it works:
https://codesandbox.io/s/react-hooks-usestate-xzvq5
const CheckboxExample = () => {
const [checkedItems, setCheckedItems] = useState({}); //plain object as state
const handleChange = (event) => {
// updating an object instead of a Map
setCheckedItems({...checkedItems, [event.target.name] : event.target.checked });
}
useEffect(() => {
console.log("checkedItems: ", checkedItems);
}, [checkedItems]);
const checkboxes = [
{
name: 'check-box-1',
key: 'checkBox1',
label: 'Check Box 1',
},
{
name: 'check-box-2',
key: 'checkBox2',
label: 'Check Box 2',
}
];
return (
<div>
<lable>Checked item name : {checkedItems["check-box-1"]} </lable> <br/>
{
checkboxes.map(item => (
<label key={item.key}>
{item.name}
<Checkbox name={item.name} checked={checkedItems[item.name]} onChange={handleChange} />
</label>
))
}
</div>
);
}
EDIT:
Turns out a Map can work as the state value, but to trigger a re-render you need to replace the Map with a new one instead of simply mutating it, which is not picked by React, i.e.:
const handleChange = (event) => {
// mutate the current Map
checkedItems.set(event.target.name, event.target.checked)
// update the state by creating a new Map
setCheckedItems(new Map(checkedItems) );
console.log("checkedItems: ", checkedItems);
}
but in this case, I think there is no benefit to using a Map other than maybe cleaner syntax with .get() and .set() instead of x[y].
As an alternative to Map, you might consider using a Set. Then you don't have to worry about initially setting every item to false to mean unchecked. A quick POC:
const [selectedItems, setSelectedItems] = useState(new Set())
function handleCheckboxChange(itemKey: string) {
// first, make a copy of the original set rather than mutating the original
const newSelectedItems = new Set(selectedItems)
if (!newSelectedItems.has(itemKey)) {
newSelectedItems.add(itemKey)
} else {
newSelectedItems.delete(itemKey)
}
setSelectedItems(newSelectedItems)
}
...
<input
type="checkbox"
checked={selectedItems.has(item.key)}
onChange={() => handleCheckboxChange(item.key)}
/>
Seems a bit of a long way round but if you spread the map out and apply it to a new Map your component will re-render. I think using a Object reference instead of a Map would work best here.
const {useState} = React
const Mapper = () => {
const [map, setMap] = useState(new Map());
const addToMap = () => {
const RNDM = Math.random().toFixed(5)
map.set(`foo${RNDM}`, `bar${RNDM}`);
setMap(new Map([...map]));
}
return (
<div>
<ul>
{[...map].map(([v, k]) => (
<li key={k}>
{k} : {v}
</li>
))}
</ul>
<button onClick={addToMap}>add to map</button>
</div>
);
};
const rootElement = document.getElementById("react");
ReactDOM.render(<Mapper />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
As a supplement to using a single object to hold the state of numerous items, the updates will not occur as expected if updating multiple items within a single render. Newer commits within the render cycle will simply overwrite previous commits.
The solution to this is to batch up all the changes in a single object and commit them all at once, like so:
// An object that will hold multiple states
const [myStates, setMyStates] = useState({});
// An object that will batch all the desired updates
const statesUpdates = {};
// Set all the updates...
statesUpdates[state1] = true;
statesUpdates[state2] = false;
// etc...
// Create a new state object and commit it
setMyStates(Object.assign({}, myStates, statesUpdates));
export default function Input(props) {
const {
name,
isChecked,
onChange,
index,
} = props;
return (
<>
<input
className="popup-cookie__input"
id={name}
type="checkbox"
name={name}
checked={isChecked}
onChange={onChange}
data-action={index}
/>
<label htmlFor={name} className="popup-cookie__label">{name}</label>
</>
);
}
const checkboxesList = [
{name: 'essential', isChecked: true},
{name: 'statistics', isChecked: false},
{name: 'advertising', isChecked: false},
];
export default function CheckboxesList() {
const [checkedItems, setCheckedItems] = useState(checkboxesList);
const handleChange = (event) => {
const newCheckboxes = [...checkedItems];
newCheckboxes[event.target.dataset.action].isChecked = event.target.checked;
setCheckedItems(newCheckboxes);
console.log('checkedItems: ', checkedItems);
};
return (
<ul className="popup-cookie-checkbox-list">
{checkboxesList.map((checkbox, index) => (
<li className="popup-cookie-checkbox-list__item" key={checkbox.name}>
<Input
id={checkbox.name}
name={checkbox.name}
isChecked={checkbox.isChecked}
onChange={handleChange}
index={index}
/>
</li>
))}
</ul>
);
}```

Categories

Resources