Best Practice to Implement React Parent-Child Components Using Hooks - javascript

I'm picking up React and not sure if I'm doing this correctly. To preface the question I've read all about the React hooks; I understand them in isolation but am having trouble piecing them together in a real-life scenario.
Imagine I have a Parent component housing a list of Child components generated via a map function on the parent:
<Parent>
{items.map(i => <Child item={i} />)}
</Parent>
And say the Child component is just a simple:
function Child({item}) {
return <div>{item}</div>
}
However the Child component needs to update its view, and be able to delete itself. My question is - should I call useState(item) on the child so it internally has a copy of the item? In that case if I updated the item the items list in the parent wouldn't get updated right? To fix that I ended up having something that looks like:
<Parent>
{items.map(i =>
<Child
item={i}
updateItem={(index) => setItems( /* slice and concat items list at index */ )}
deleteItem={(index) => setItems( /* slice items list at index */ )}
/>)
}
</Parent>
And the Child component simply invokes updateItem and deleteItem as appropriate, not using any React hooks.
My question here are as follows:
should I have used useState in the child component?
should I have used useCallback on the updateItem/deleteItem functions somehow? I tried using it but it didn't behave correctly (the correct item in the Parent got removed but the state in the remaining rendered Child were showing values from the deleted Child for example.
My understanding is that this would be very inefficient because an update on 1 child would force all other children to re-render despite them not having been updated.
If done most properly and efficiently, what should the code look like?
Thanks for the pointers.

should I have used useState in the child component?
Usually duplicating state is not a good idea; so probably no.
should I have used useCallback on the updateItem/deleteItem functions
somehow
You might need it if you want to pass those callbacks to components wrapped in React.memo.
My understanding is that this would be very inefficient because an
update on 1 child would force all other children to re-render despite
them not having been updated
Yes your understanding is correct, but whether you would notice the slow down, depends on number of things such as how many child components there are, what each of them renders, etc.
If done most properly and efficiently, what should the code look like?
See below. Notice I added React.memo which together with useCallback should prevent those items from re rendering, props of which didn't change.
const Child = React.memo(function MyComponent({ item, update }) {
console.log('Rendered', item);
return (
<div
onClick={() => {
update(item);
}}
>
{item.name}
</div>
);
});
let itemsData = [
{ id: 0, name: 'item1' },
{ id: 1, name: 'item2' },
];
export default function App() {
let [items, setItems] = React.useState(itemsData);
let update = React.useCallback(
(item) =>
setItems((ps) =>
ps.map((x) => (x.id === item.id ? { ...x, name: 'updated' } : x))
),
[]
);
return (
<div>
{items.map((item) => (
<Child key={item.id} item={item} update={update} />
))}
</div>
);
}
Now if you click item1, console.log for item2 won't be called - which means item2 didn't rerender

No you don't have to create internal state. That's an anti pattern to create a local state just to keep a copy of props of the component.
You can keep your state on parent component in your case. Your child component can execute callbacks like you used,
for example,
const [items, _] = useState(initialItemArray);
const updateItem = useCallback((updatedItem) => {
// do update
}, [items])
const deleteItem = useCallback((item) => {
// do delete
}, [items])
<Child
data={item}
onUpdate={updateItem}
onDelete={deleteItem}
/>
Also note you shouldn't over use useCallback & useMemo. For example, if your list is too large and you use useMemo for Child items & React re renders multiple 100 - 1000 of list items that can cause performance issue as React now have to do some extra work in memo hoc to decide if your <Child /> should re render or not. But if the Child component contain some complex UI ( images, videos & other complex UI trees ) then using memo might be a better option.
To fix the issue in your 3rd point, you can add some unique key ids for each of your child components.
<Child
key={item.id} // assuming item.id is unique for each item
data={item}
onUpdate={(updatedItem) => {}}
onDelete={(item) => {}}
/>
Now react is clever enough not to re render whole list just because you update one or delete one. This is one reason why you should not use array index as the key prop

#Giorgi Moniava's answer is really good. I think you could do without useCallback as well and still adhere to best practices.
const {useEffect, useState} = React;
const Child = ({ item, update }) => {
const [rerender, setRerender] = useState(0);
useEffect(() => setRerender(rerender + 1), [item]);
useEffect(() => setRerender(rerender + 1), []);
return (
<div className="row">
<div className="col">{item.id}</div>
<div className="col">{item.name}</div>
<div className="col">{item.value}</div>
<div className="col">{rerender}</div>
<div className="col">
<button onClick={update}>Update</button>
</div>
</div>
);
}
const Parent = () => {
const [items, setItems] = useState([
{
id: 1,
name: "Item 1",
value: "F17XKWgT"
},
{
id: 2,
name: "Item 2",
value: "EF82t5Gh"
}
]);
const add = () => {
let lastItem = items[items.length - 1];
setItems([
...items,
{
id: lastItem.id + 1,
name: "Item " + (lastItem.id + 1),
value: makeid(8)
}
]);
};
const update = (sentItem) => {
setItems(
items.map((item) => {
if (item.id === sentItem.id) {
return {
...item,
value: makeid(8)
};
}
return item;
})
);
};
const makeid = (length) => {
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
return (
<div className="parent">
<div className="header">
<h1>Parent Component</h1>
<h2>Items in Parent State</h2>
</div>
<div className="table">
<section>
<header>
<div className="col">ID</div>
<div className="col">NAME</div>
<div className="col">VALUE</div>
</header>
{items.map((item, i) => (
<div className="row" key={item + "-" + i}>
<div className="col">{item.id}</div>
<div className="col">{item.name}</div>
<div className="col">{item.value}</div>
</div>
))}
</section>
<div className="header">
<h1>Children</h1>
<h2>Based on Items state</h2>
</div>
<button onClick={add}>Add</button>
<section>
<header>
<div className="col">ID</div>
<div className="col">Name</div>
<div className="col">Value</div>
<div className="col">Re-render</div>
<div className="col">Update</div>
</header>
{items.map((item, i) => (
<Child
item={item}
key={"child-" + item + "-" + i}
update={() => update(item)}
/>
))}
</section>
</div>
</div>
);
}
ReactDOM.render(
<Parent />,
document.getElementById("root")
);
.parent {
font-family: sans-serif;
}
.header {
text-align: center;
}
section {
display: table;
width: 100%;
}
section > * {
display: table-row;
background-color: #eaeaea;
}
section .col {
display: table-cell;
border: 1px solid #cccccc;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>

Related

how can i pass functional react component to a function?

so what I am trying to achieve here is storing a whole component in an array in a parent component which renders a specific component in the array using its index for example :
export const Test = () => {
const [components, setComponents] = useState([
<Order key={1} />,
<Order key={2} />,
<Order key={3} />,
]);
const [index, setIndex] = useState(0);
return (
<div>
<button onClick={() => setIndex((old) => (old + 1) % components.length)}>
change
</button>
{`page ` + index}
{components[index]}
</div>
);
};
const Order = () => {
const [someState, setSomeState] = useState(1);
return (
<div>
<button onClick={() => setSomeState((old) => old + 1)}>
{someState}
</button>
</div>
);
};
when I change the state of one item then cycle through the items then return to the item which I changed its state i found that it is not updated
what I figured out is that the component in the array (in the Test component) doesn't get updated and I couldn't figure out how to update it
what I don't want to do is storing the state of the order item in the parent and pass it as props (because it will be a pain to make it work)
const App = ({ flag }) => {
if (flag) <Order />
return null
}
I'm giving you an example so i can explain what might happen in your case. If the flag becomes false from a true, the App turns blank. But what happen to the Order? It's unmounted, why? Since when React compares between the previous scene and the current scene, it notice there's no such Order any more. So what you think about the memory of component of Order (which is called a fiber)?
I guess the answer is, the memory goes to be deleted and will be collected for future use.
Now back to your case, you are using an id to switch to different component. But in theory it should behave very similar to my example for each component.
NOTE: the take away is that if you want to use an array, that's fine, but all components has to be rendered at ALL time, you can hide them, but you can't unmount any of them.
what I don't want to do is storing the state of the order item in the
parent and pass it as props (because it will be a pain to make it
work)
Your problem is that when you render a Test component and then increase index, then you render another Test component with a different key, so reacts reconciliation algorithm unmounts the old one and you lose the state.
You have two options:
lift state of each Test component up, then when one gets unmounted, you will remount it with the old state, because state will be stored in parent, it will not be lost
another option is to render all components and only show those which you want using CSS display property, this way none of them gets unmounted and you retain state. Here is example:
const Order = () => {
const [someState, setSomeState] = React.useState(1);
return (
<div>
<button onClick={() => setSomeState((old) => old + 1)}>
{someState}
</button>
</div>
);
};
let components = [<Order />, <Order />, <Order />];
const Test = () => {
const [index, setIndex] = React.useState(0);
return (
<div>
<button onClick={() => setIndex((old) => (old + 1) % components.length)}>
change
</button>
{`page ` + index}
{[0, 1, 2].map((x) => (
<div key={x} style={{ display: index === x ? "block" : "none" }}>
{components[x]}
</div>
))}
</div>
);
};
ReactDOM.render(
<Test />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
PS I have removed components from state, I can't find official info now, but IMHO it is not good idea to store components in state.

How to use a callback function in OnClick in react

I have a component which I use when mapping each element of my array like so:
<DropDown
options={OPERATIONS}
placeholder={"Select operation"}
value={setOperation}
parentWidth={"47%"}
childWidth={"20%"}
makeSelection={this.selectOperation, ()=>this.checkIndex(index)} //I need the index as well
/>
I would like to use checkIndex as a callback to this.selectOperation
In the DropDown component, this is how makeSelection is used:
{options.map((option, index) => {
return (
<li key={index} onClick={() => { makeSelection(option); this.toggleDropDown(); }}>
<p>{option.name}</p>
</li>
);
})}
And this is how selectOperation is defined in my component:
selectOperation(op) {
this.setState({
setOperation: op
})
}
To summarize, I need to use the index to set a value in my array which is equal to the setOperation, so I need a callback to when the value has been set.
Also, I'm unable to do makeSelection={() => this.selectOperation} since that causes a maximum update depth reached problem
Edit: I should mention that I'm not trying to change the structure of the DropDown menu since I have not written that component so I would prefer to not change it
I suspect what you are looking for is how to pass a callback to the component that allows you to pass data back to the other component using your Dropdown.
Here's a really simple example with no state being updated. I used the fairly universal onChange prop name for the callback and when called it receives the index you mentioned and the option object. You can send whatever you want in that callback and document what it receives
const DropDown = (props) => {
const { options, onChange } = props;
const handleClick = (item, i) => {
return (e) => {
if (typeof onChange === "function") {
// you decide what to send back to other component
onChange(item, i);
}
};
};
return (
<ul> {options.map((option, index) => (
<li key={index} onClick={handleClick(option, index)}>
<strong>{option.name}</strong>
</li> )
)}</ul> );
};
const App = () => {
const options = [{ name: "foo" }, { name: "bar" }];
// arguments determined by whatever is passed to it when called in DropDown comp
const handleChange = (opt, i) => {
console.log(`Name:${opt.name}, Index: ${i}`);
};
return (<DropDown options={options} onChange={handleChange} />);
};
// Render it
ReactDOM.render(
<App />,
document.getElementById("react")
);
li{list-style:none; border:2px solid #ccc; width:100px; font-size:1.3em;padding:.5em; text-align:center}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<h3>Click item for result in console</h3>
<div id="react"></div>

React Redux - Mapping over array while rerendering only components holding changed object (in array)

Explanation
Hi, I'm pretty new in 'advanced' React/Redux field. The problem that I have is:
I didn't use actions so the problem and code can be simplified as much as possible.
MyParent component:
const MyParent = () => {
const ref = useRef(0)
const myArray = useSelector(state => state.someReducer.myArray)
const changeOneItem = () => {
dispatch({ type: CHANGE_ONE_ITEM })
}
return (
<div>
{ref.current ++ }
<button onClick={() => changeOneItem()}>Add</button>
{
myArray.map((item) =>
<MyChild
key={item.id}
name={item.text}
name2={item.text2}
></MyChild>
)
}
</div>
Now here is my child component:
const MyChild = ({name, name2}) => {
const ref = useRef(0)
return (
<div>
<hr/>
<p>{ref.current ++}</p>
<p>{name}</p>
<p>{name2}</p>
</div>
)}
And the reducer:
const initialState = {
myArray: [
{
id: "1",
text: "first",
text2: "001"
},
{
id: "2",
text: "second",
text2: "002"
}
]}
case CHANGE_ONE_ITEM:
return {
...state,
myArray: state.myArray.map(t => t.id == "1" ? {...t, text: "new text"} : t)
}
Question
Let's imagine there is ~1,000 objects inside the array. Everytime I change one of the object inside array, parent component rerenders (because of the selector), which also triggers all child components to rerender.
I'm kind of confused when it comes to immutable changes with Redux, when does immutable change helps if this one is not the case?
Every child component has their own key, but still, whole list will get rerender, is there something I'm missing? Is there a way to trigger render on only one child which corresponding object did change?
Example in main project
Subtitle translator. You will have table, each row will have own textarea where you can write your subtitle for specific timestamp (start of subtitle - end of subtitle). After leaving the textarea, changes should be saved, that save causes lag because each "child" component (in this case each row) rerenders.
Thanks!
Good luck :)
You can make MyChild a pure component with React.memo, your reducer already doesn't change all the other elements of the array t.id == "1" ? {...t, text: "new text"} : t and each MyChild item has a unuque key so none should re render when you only chanage one item but you have to use React.memo because functional components will always re render. That is they will re create jsx but React may not render Dom when current generated jsx is the same as last time.
const { memo, useRef, useCallback, useState } = React;
//using React.memo to make MyChild a pure component
const MyChild = memo(function MyChild({
id,
text,
change,
}) {
const ref = useRef(0);
return (
<div>
<p>Rendered: {++ref.current} times</p>
<input
type="text"
value={text}
onChange={(e) => change(id, e.target.value)}
/>
</div>
);
});
const App = () => {
const [data, setData] = useState(() =>
[...new Array(10)].map((_, i) => ({
id: i + 1,
text: `text ${i+1}`,
}))
);
//use callback so change is only created on mount
const change = useCallback(
(id, text) =>
//set data item where id is the id passed to change
setData((data) =>
data.map((d) => (d.id === id ? { ...d, text } : d))
),
[]//deps array is empty so only created on mount
);
return (
<div>
{data.map((item) => (
<MyChild
key={item.id}
id={item.id}
text={item.text}
change={change}
/>
))}
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<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="root"></div>

Update Component by reactivity way

Below demo has two Row components, one of them is dependent on styles['a'], another one is dependent on styles['b'].
But if update styles['a'] in place, it will not trigger re-render(same object reference)
if update styles['a'] with new object, it will trigger re-render for both <Row type={'a'}/> and <Row type={'b'}/>.
Is there any way we can implement updating the components based on the dependency? For example, when styles['a'] is updated, then <Row type={'a'}/> will be re-rendered, but <Row type={'b'}/> will not.
PS: I want to transplant one chart library from Vue to React, the chart may have thousands of components are dependent on the props.options/styles of the root component. So I don't want to re-render whole chart even if only one property of options/styles is changed.
const RootContext = React.createContext("RootOptions")
function Chart({options}) {
const [styles, setStyles] = React.useState({'a':'a', 'b':'b'})
const [innerOptions, setOptions] = React.useState({})
const value = React.useMemo(() => ({options: innerOptions, styles: styles}), [styles, innerOptions])
return (
<RootContext.Provider value={value}>
<Row type={'a'} items={[]}></Row>
<Row type={'b'} items={[]}></Row>
<input value={styles['a']} onChange={(e) => {
setStyles(Object.assign({}, styles, {'a': e.target.value}) // will trigger updateComponent for both Row-'a' and Row-'b'
// setStyles(Object.assign(styles, {'a': e.target.value}) will not trigger updateComponent
)}} />
</RootContext.Provider>
)
}
function Row(props) {
const {options, styles} = React.useContext(RootContext)
console.log('Row rendered ', props.type)
const value = React.useMemo(() => styles[props.type] + '...', [props.type, styles[props.type]])
return (
<div>
<pre>{props.type}: {value} -> <Cell num={props.type}/></pre>
</div>
)
}
function Cell({num}) {
console.log('Cell rendered', num)
return <span>{num}</span>
}
ReactDOM.render(<Chart options={{}} />, document.getElementById('root'))
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
Below is one similar demo for Vue, you will see only the component=<test1 :a="test.a"> will be updated when test.a is changed.
Vue.component('test1',{
render (h) {
return h('p', `test1: ${this.a}`)
},
props: ['a'],
updated: function () {
console.log('updated sidebar ' + this.a)
},
mounted:function () {
console.log('mounted sidebar ' + this.a)
}
})
Vue.component('test2',{
render (h) {
return h('p', `test2: ${this.b}`)
},
props: ['b'],
updated: function () {
console.log('updated tool ' + this.b)
},
mounted:function () {
console.log('mounted tool ' + this.b)
}
})
new Vue ({
el:'#app',
data () {
return {
test: {'a': 1, 'b': 2222}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<test1 :a="test.a"></test1>
<test1 :a="test.b"></test1>
<test2 :b="test.b"></test2>
<input v-model="test.a">
</div>
The rows are updating because of the context is being updated as you type. Look at the style value.
const [styles, setStyles] = React.useState({'a':'a', 'b':'b'})
const [innerOptions, setOptions] = React.useState({})
const value = React.useMemo(() => ({options: innerOptions, styles: styles}), [styles, innerOptions]);
Also the item={[]} prop on each row should be static(outside the component) or in state/memo/ref as this will also cause a rendering. Im assuming this is here for simplicity.
A simple solution would be just to not use a global/root context(thats not stable) and to use memo/selectors or even something like reslect to create stable state slices for each type then using React.memo to skip rendering when props are equal. The obvious downside is that you will need to prop drill those values down and that can get annoying. And probably why you decided to go with context in the first place. Here is an untested example using a memoed selector.
const Row = react.memo((props) => ...); // your row component now wrapped with memo.
// im injecting styles, options, ect so that you dont have to keep this code in
// the component because it makes the component code more focused.
// You could move this into a new file, put it directly inside your component, ect.
const selectRowType = (type, styles, options) => {
const style = styles[type];
return useMemo(() => {
return { options, style };
}, [style, options]);
};
<Row type={selectRowType('a', styles, innerOptions) items={...} />
<Row type={selectRowType('b', styles, innerOptions) items={...} />
A compromise could be to use a RowContext inside each row that way the cells and other row children can use context values derived by the type and would be stable unless the observed type changes in the parent. You would still need to preselect the values for each row type, but would avoid the prop drilling pain inside the <Row /> boundary.
const DEFAULT_ROW_CONTEXT_VALUE = {};
const RowContext = createContext(DEFAULT_ROW_CONTEXT_VALUE );
// create a stable row context value.
// We can further enrich the context with row specific data here.
const selectRowContextValue = (props) => {
const { type, items } = props;
return useMemo(() => {
return { type, items };
}, [type, items]);
};
const Row = React.memo((props) => {
return (
<RowContext.Provider value={selectRowContextValue(props)}>
{...}
</RowContext.Provider>
);
});
<Row type={selectRowType('a', styles, innerOptions) items={...} />
<Row type={selectRowType('b', styles, innerOptions) items={...} />
If you have only <Cell /> children and they arn't deep, then there really isn't a reason to use RowContext on every <Row />. It's up to you.

React hooks - Shopping cart - Proper use of props and prototype?

I'm trying to build an example shopping cart and came across this example that doesn't seem to work here, but does show each product, price, add to cart button, and correctly tallies up the total when you add to the cart.
QUESTIONS
1) Will the use of concat cause any prototypal issues that I should worry about?
2) What is the point of doing this part of the code? Why are they setting props and children? Can this be omitted or refactored without this?
const Product = props => {
const { product, children } = props;
return (
<div className="products">
{product.name} ${product.price}
{children}
</div>
);
};
CODE
const Product = props => {
const { product, children } = props;
return (
<div className="products">
{product.name} ${product.price}
{children}
</div>
);
};
function App() {
const [products] = useState([
{ name: "Superman Poster", price: 10 },
{ name: "Spider Poster", price: 20 },
{ name: "Bat Poster", price: 30 }
]);
const [cart, setCart] = useState([]);
const addToCart = index => {
setCart(cart.concat(products[index]));
};
const calculatePrice = () => {
return cart.reduce((price, product) => price + product.price, 0);
};
return (
<div className="App">
<h2>Shopping cart example using React Hooks</h2>
<hr />
{products.map((product, index) => (
<Product key={index} product={product}>
<button onClick={() => addToCart(index)}>Add to cart</button>
</Product>
))}
YOUR CART TOTAL: ${calculatePrice()}
{cart.map((product, index) => (
<Product key={index} product={product}>
{" "}
</Product>
))}
</div>
);
}
<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>
Will the use of concat cause any prototypal issues that I should worry about?
No, an Array.concat() would simply return you a new Array reference, which is also correct while setting a state. And why would there be any prototypal issue? You aren't changing anything over the prototype.
What is the point of doing this part of the code? Why are they setting props and children?
const { product, children } = props;
You need product and children to display in your product page, you simply extract it from props, This way of extracting variables is called Destructering, it the same as:
const product = props.product;
const children= props.children;

Categories