How to avoid re-render of React Hooks - reactjs

Hi I am new developer at ReactJs. I have a question about Hooks. I know there are so many answers about this quest but I did not integrate that solutions to my project. I have parent and child component. I am passing value from child to parent. When I try to send a data via react callback function, parent is re-rendering while taking item and so I lost my selected value on Child. How can I protect that problems ? Could you any advice to me ?
Parent Component :
import React,{useCallback,useState} from 'react'
const Dropable = ({item}) => {
const [list, setList] = useState(item)
const [selectedItems, setSelectedItems] = useState<Array<any>>([])
const handleSelectedItems = useCallback( (data: any) => {
if (data.type === "player") {
if (selectedItems.length === 0) {
const arr = selectedItems
arr.push(data)
setSelectedItems(arr)
}
}, [selectedItems],
)
return (
{list.map((data: any, index: any) => {
return (
<>
{
<div onClick={() => handleSelectedItems(index, data)}>
<Detailed
key={uuid()}
data={data}
dataIndex={index}
/>
</div>
}
</>
)
})}
)
}
export default Dropable;
Child Component:
import React,{useState} from 'react'
const Detailed : React.FC<IProps> = (props) {
const [selected, setSelected] = useState(false)
const handleSelect = () => {
if (selected) {
setSelected(false)
}
else {
setSelected(true)
}
}
return (
<div onClick={handleSelect} className="detail" style={selected?{border: " 1px solid #5c6ef5"}: {}}>
</div>
)
}
export default Detailed;

Hi Instead of maintaining the boolean value in the child to see if the item is selected or not you can pass that value from parent to child.
So the parent component:
const Dropable = ({ item }) => {
const [list, setList] = useState(item)
const [selectedItems, setSelectedItems] = useState<Array<any>>([])
const handleSelectedItems = useCallback((data: any) => {
if (data.type === "player") {
if (selectedItems.length === 0) {
const arr = selectedItems
arr.push(data)
setSelectedItems(arr)
}
}, [selectedItems],
)
return (
{
list.map((data: any, index: any) => {
return (
<>
{
<div onClick={() => handleSelectedItems(index, data)}>
<Detailed
key={uuid()}
data={data}
dataIndex={index}
selected={selectedItems[0][ANY_KEY] === data[ANY_key]}
/>
</div>
}
</>
)
})
}
)
}
export default Dropable;
And your child component:
There is no need to use useState to set the value coz we are doing the same work in the parent component.
import React,{useState} from 'react'
const Detailed : React.FC<IProps> = (props) {
return (
<div className="detail" style={props.selected?{border: " 1px solid
#5c6ef5"}: {}}>
</div>
)
}
export default Detailed;
One more tip: It is not a good practice to send data from child component to parent.

You can simply pass the selected value from parent to child component and initiate the selected state using that props.
const [selected, setSelected] = useState(props.selected)
Then every time the parent updates, the child's selected state will not be null.
You can pass null as initial value for selected via props. So, you will call the child component like this,
import React,{useCallback,useState} from 'react'
const Dropable = ({item}) => {
const [list, setList] = useState(item)
const [selectedItems, setSelectedItems] = useState<Array<any>>([])
const handleSelectedItems = useCallback( (data: any) => {
if (data.type === "player") {
if (selectedItems.length === 0) {
const arr = selectedItems
arr.push(data)
setSelectedItems(arr)
}
}, [selectedItems],
)
return (
{list.map((data: any, index: any) => {
const isSelected = selectedItems.findIndex(item => item.[some_unique_attribute] === data.[some_unique_attribute]) > -1;
return (
<>
{
<div onClick={() => handleSelectedItems(index, data)}>
<Detailed
key={uuid()}
data={data}
dataIndex={index}
selected={isSelected}
/>
</div>
}
</>
)
})}
)
}

The only reason that your child loses its state when the parent re-renders is because you are assigning a new key to the Child component while mapping
Assigning a key that is unique but also remains same across re-renders will prevent this problem from happening
If the key to a component changes, its not re-rendered but re-mounted and hence state values are reset
Either you can use a unique value as key from data or alternative use index if there is no unique key in data. However you must try to have atleast 1 field in data which can be used to uniquely identify it
return (
{
list.map((data: any, index: any) => {
return (
<>
{
<div onClick={() => handleSelectedItems(index, data)}>
<Detailed
key={data.id} // if no id field. You can assign index
data={data}
dataIndex={index}
selected={selectedItems[0][ANY_KEY] === data[ANY_key]}
/>
</div>
}
</>
)
})
}
)

Related

useState set to string not working in Reactjs

I have this code that controls the behavior of what to map from an array onClick. The useState is set to a string const [activeFilter, setActiveFilter] = useState('All'); that is supposed to automatically filter all products containing the string as tag but it doesn't do this automatically and I can't figure out why. Please help with code below.
index.js
import React, { useEffect, useState } from 'react'
import {client} from '../lib/client'
import { Product, FooterBanner, HeroBanner } from '../components'
const Home = ({products, bannerData}) => {
const [productItems, setProductItems] = useState([])
const [filterWork, setFilterWork] = useState([]);
const [activeFilter, setActiveFilter] = useState('All');
useEffect(() => {
setProductItems(products)
}, [])
const handleProductFilter = (item) => {
setActiveFilter(item)
setTimeout(() => {
if (item == 'All'){
setFilterWork(productItems)
}else{
setFilterWork(productItems.filter((productItem)=> productItem.tags.includes(item)))
}
}, 500)
}
return (
<>
<HeroBanner heroBanner={bannerData.length && bannerData[0]} />
<div className='products-heading'>
<h2>Best Selling Products</h2>
<p>Smoke accessories of many variations</p>
</div>
<div className='product_filter'>
{['Lighter', 'Pipe', 'Roller', 'Hookah', 'All'].map((item, index) => (
<div
key={index}
className={`product_filter-item app__flex p-text ${activeFilter === item ? 'item-active' : ''}`}
onClick={() => handleProductFilter(item)}
>
{item}
</div>
))}
</div>
<div className='products-container'>
{
filterWork.map((product) => <Product key={product._id} product={product} />)
}
</div>
<FooterBanner footerBanner={bannerData && bannerData[0]} />
</>
)
};
export const getServerSideProps = async () => {
const query = '*[_type == "product"]'
const products = await client.fetch(query)
const bannerQuery = '*[_type == "banner"]'
const bannerData = await client.fetch(bannerQuery)
return {
props: {products, bannerData}
}
}
export default Home
The image below is what it looks like on load and the only time All products containing 'All' tags are visible is when the All button is clicked on again, regardless of it being active initially
No products are being displayed initially when the component renders because the displayed products are loaded from the filterWork state that is only set once an onClick event is triggered. To fix this you can simply set the initial products in the useEffect because you are starting with all the products being displayed.
useEffect(() => {
setProductItems(products);
setFilterWork(products);
}, [])

React TypeScript containment parent passing props to children

I've been trying this for hours but I haven't found a satisfactory solution. I want to have this wrapper that contains some state that I can then either pass to its child or render something else.
I would like to do something like this abstract example. Is there anything along these lines that I can do?
const MyChild = (props:{state:boolean}) => {
return <Text>`the state is ${props.state}`</Text>
}
const StateWrapper = ({children}:{children:React.ReactNode}) => {
const hookState:boolean|null = useHookState()
if (null) return <Loading />
return {children} <-- with {state:boolean}
}
const App = () => {
return <StateWrapper><MyChild /><StateWrapper>
}
A common pattern for this kind of problem is the "Render Props" approach. The "state wrapper" object takes a prop that passes its data to something else to render. This way you don't have to do any weird changing or copying of state data, and names don't necessarily have to align perfectly, making it easy to swap in other components in the future.
const MyChild = (props: {state: boolean}) => {
return <Text>`the state is ${props.state}`</Text>
}
const StateWrapper = ({children}:{children: (state: boolean) => React.ReactNode}) => {
const hookState:boolean|null = useHookState()
if (null) return <Loading />
return children(state);
}
const App = () => {
return (
<StateWrapper>
{(state) => (<MyChild state={state}/>)}
</StateWrapper>
);
}
See more: https://reactjs.org/docs/render-props.html
3 types of wrapper with ref and added props in typescript:
Sandbox Demo:https://codesandbox.io/s/wrapper-2kn8oy?file=/src/Wrapper.tsx
import React, { cloneElement, forwardRef, useRef, useState } from "react";
interface MyChild extends React.ReactElement {
ref?:React.Ref<HTMLDivElement>|undefined
}
interface MyProps {
children:MyChild|MyChild[],
added:string,
}
const Wrapper1 = ({ children, added }: MyProps) => {
const e =
Array.isArray(children) ?
children.map((child) => {
return cloneElement(child, { added: added })
}) :
cloneElement(children)
return <>
{e}
</>
}
const Wrapper2 = ({ children, added }:MyProps) => {
const e =
Array.isArray(children) ?
children.map((child) => {
return <child.type {...child.props} added={added} ref={child.ref} />
}) :
children?<children.type {...children.props} added={added} ref={children.ref}/>:null
//console.log("2:",e)
if(!Array.isArray(children))console.log(children.ref)
return <>
{e}
</>
}
const Wrapper3 = ({ children, added }:{children:any,added:any}) => {
return <>
{children(added)}
</>
}
const Mydiv = forwardRef((props: any, ref?: any) =>
<div ref={ref}>Origin:{props.message},Added:{props.added ?? "None"}</div>
)
const Container = () => {
const ref1 = useRef<HTMLDivElement>(null)
const ref2 = useRef<HTMLDivElement>(null)
const ref3 = useRef<HTMLDivElement>(null)
const [refresh, setRefresh] = useState(1)
const setColor = () => {
console.log("ref1:", ref1.current, "ref2:", ref2.current, "ref3:", ref3.current)
if (ref1.current) ref1.current.style.color = "red"
if (ref2.current) ref2.current.style.color = "red"
if (ref3.current) ref3.current.style.color = "red"
}
return <>
<button onClick={setColor}>Change Color</button>
<button onClick={() => setRefresh((n) => n + 1)}>Refresh page</button>
<div>{`state:${refresh}`}---state:{refresh}</div>
<Wrapper1 added="Wrapper1 added">
<Mydiv ref={ref1} message={`MyDiv 1 with Ref,parent state:${refresh}`} />
<Mydiv message={`MyDiv 1 without ref,parent state:${refresh}`} />
</Wrapper1>
<Wrapper2 added="Wrapp2 added">
<Mydiv ref={ref2} message={`MyDiv 2 with Ref,parent state:${refresh}`} />
<Mydiv message={`MyDiv 2 without ref,parent state:${refresh}`} />
</Wrapper2>
<Wrapper3 added="Wrapp3 added">
{(added: any) => (<>
<Mydiv ref={ref3} message={`MyDiv 3 with Ref,parent state:${refresh}`} added={added} />
<Mydiv message={`MyDiv 3 without Ref,parent state:${refresh}`} added={added} />
</>
)}
</Wrapper3>
</>
}
export default Container

React useState async setter doesn't update value passed as props

I have this component in my React project -
const ViewPost = (props: Props) => {
const [listingData, setListingData] = useState<any>({})
const [auctionData, setAuctionData] = useState<any>({})
useEffect(() => {
if (props.listingId) {
getListingData()
}
}, [props.listingId])
const getListingData = async () => {
const { data } = await getListingById(props.listingId)
setListingData(data?.data)
if (data.data.isTimedAuction) {
auctions(data.data.auctionId)
}
}
const auctions = async (auctionId: any) => {
const auction = await getAuctions(auctionId)
console.log('auction', auction.data)
setAuctionData(auction.data)
}
return (
<>
<Navbar />
<div className={classes.viewPostPage}>
<div className={classes.bodyContainer}>
<Details
data={listingData as any}
updateListing={getListingData}
auctionData={auctionData}
/>
</div>
</div>
</>
)
}
export default ViewPost
Basically, I'm getting data from an API and assigning it to auctionData.
console.log(auction.data) shows me the desired result but when I pass auctionData as props into Details I get an empty object which leads to a lot of issues, since useState is async.
How can I overcome this problem?
const [auctionData, setAuctionData] = useState<any>({})
your default value is an empty object, that causes the problems.
should set null or undefined as default value, and hide the Details when not have the data.
Use loading state. Once data is fully fetched from api then pass to child component. I think what is happeing here is that child component is called with empty state variable while data is still being fetched.
const [isLoading, setIsLoading] = useState(true)
const getListingData = async () => {
const { data } = await getListingById(props.listingId)
.then((data) => {setListingData(data)})
.then((data) => {
setTimeout(() => {
setIsLoading(false)
}, 1000)
})
if (data.data.isTimedAuction) {
auctions(data.data.auctionId)
}
}
and then return
if (isLoading) {
return (
<div>
Loading...
</div>
)
}
return (
<>
<Navbar />
<div className={classes.viewPostPage}>
<div className={classes.bodyContainer}>
<Details
data={listingData as any}
updateListing={getListingData}
auctionData={auctionData}
/>
</div>
</div>
</>
)
}

Passing a React.MutableRefObject to sibling elements

I'm sure I'm explaining this wrong but I'm trying to pass a reference to an div that may or may not be there and read that reference in a sibling component.
Let me explain, (test code in it's entirety at the end):
I define a Overlay component that takes a React.MutableRefObject<HTMLDivElement | null> property. It then sets it's returned styled component to this ref. Though this is tricky as I'm only returning the styled div when there are items to display:
return (
overlayItems.length > 0
? (
<StyledOverlay ref={ref}>
{overlayItems && overlayItems.map(i => <p>{i}</p>)}
</StyledOverlay>
)
: (
<div></div>
)
)
I then am trying to read this ref in a sibling component where I simply check if ref.current == null with the actual const refVariable = useRef<HTMLDivElement | null>(null); call in the parent component and refVariablebeing passed into both components.
However, even with items in the overlayItems array, refVariable.current is always equal to null. I'm not sure if this is a side effect of using styled-Components or if this is because I'm passing the reference incorrectly or something else entirely. Can anyone point out my mistake?
(Oh and to add to the matter, I'm using TypeScript)
Full code
import React, { createContext, FC, useCallback, useContext, useRef, useState } from 'react';
import styled from 'styled-components';
export const RefTest = () => {
const refVariable = useRef<HTMLDivElement | null>(null);
return (
<AppState>
<RefReader ref={refVariable} />
<Overlay ref={refVariable} />
</AppState>
)
}
type AppContextValue = {
overlayItems: string[];
addOverlayItem: (newItem: string) => void,
};
const AppContext = createContext<AppContextValue>(undefined!);
const AppState: FC = ({ children }) => {
const [overlayItems, setOverlayItems] = useState<string[]>([])
const addOverlayItem = (newItem: string) => {
setOverlayItems(prev => [...prev, newItem]);
}
return (
<AppContext.Provider value={{
overlayItems,
addOverlayItem,
}} >
{children}
</AppContext.Provider>
)
}
const StyledOverlay = styled.div`
height: 100vh;
width: 100vw;
position: absolute;
top: 0;
left: 0;
`;
type RefProp = {
ref: React.MutableRefObject<HTMLDivElement | null>
}
const Overlay: FC<RefProp> = ({ ref }) => {
const { overlayItems } = useContext(AppContext);
return (
overlayItems.length > 0
? (
<StyledOverlay ref={ref}>
{overlayItems && overlayItems.map(i => <p>{i}</p>)}
</StyledOverlay>
)
: (
<div></div>
)
)
}
const RefReader: FC<RefProp> = ({ ref }) => {
const { addOverlayItem, overlayItems } = useContext(AppContext);
const [result, setResult] = useState("");
const add = () => {
addOverlayItem(`${overlayItems.length + 1} - an item`)
}
const check = () => {
setResult(`the ref current has a current reference: ${!!ref.current}`);
}
return (
<div>
<p>{result}</p>
<button onClick={add} >Add Overlay Item</button>
<button onClick={check} >Check Ref Current</button>
</div>
);
}
The resolution to this ended up being to wrap the two sub components with React.forwardRef(). This caused some havoc with TypeScript trying to understand the reference though and I had to force the type
const RefReader = forwardRef<HTMLDivElement, {}>((_, ref) => {
const { addOverlayItem, overlayItems } = useContext(AppContext);
const [result, setResult] = useState("");
const add = () => {
addOverlayItem(`${overlayItems.length + 1} - an item`)
}
const check = () => {
setResult(`the ref current has a current reference: ${!!(ref as MutableRefObject<HTMLDivElement>)?.current}`);
}
return (
<div>
<p>{result}</p>
<button onClick={add} >Add Overlay Item</button>
<button onClick={check} >Check Ref Current</button>
</div>
);
});

React-redux component rerenders every time when and unconnected state changes in the store . How do I prevent it?

I have a component callled FileHeader. Everytime I resize a column in it, it dispatches an action to changes a state. This triggers a rerender.
ListeHeader.js
const ListHeader = props => {
const dispatch = useDispatch()
let headerRef = useRef(new Array())
const { header_ } = useSelector((state) => state.ui, shallowEqual)
const onResize = ( e, {event, node, size}) => {
dispatch(changeHeaderWidth(node.id, size.width))
}
return (
<HeaderBody>
{header_
.map((header) => {
const getRef = (element) => headerRef.current.push(element)
return (
<ResizableBox
axis="x"
width={header.width}
height={20}
key={header.id}
handle={<DragHandle id={header.id} />}
onResize={onResize}
minConstraints={[50, 20]}
maxConstraints={[300, 20]}
>
<Header
key={header.id}
width={header.width}
handleDrag={handleDrag}
onClick={handleSort(header.id)}
>
<HeaderText ref={getRef}>{header.name}</HeaderText>
</Header>
</ResizableBox>
)
})}
</HeaderBody>
)
}
This is my reducer
export default (state = initial_state, actions) => {
switch (actions.type) {
case consts.CHANGE_HEADER_WIDTH : return {
...state,
headerWidth: state.headerWidth.map((item) =>
item.id === actions.payload.id ? { ...item, width: actions.payload.neWidth}
: item),
}
break;
default: return state;
}
}
I'm not calling headerWidth state in my component it causes a rerender when it changes
From the docs
When passing a callback using dispatch to a child component, you may
sometimes want to memoize it with useCallback. If the child component
is trying to optimize render behavior using React.memo() or similar,
this avoids unnecessary rendering of child components due to the
changed callback reference.
import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
const incrementCounter = useCallback(
() => dispatch({ type: 'increment-counter' }),
[dispatch]
)
return (
<div>
<span>{value}</span>
<MyIncrementButton onIncrement={incrementCounter} />
</div>
)
}
export const MyIncrementButton = React.memo(({ onIncrement }) => (
<button onClick={onIncrement}>Increment counter</button>
))

Resources