React - functional components keep re-render when passing functions as props - javascript

i have an issue in my react app and i dont know how to solve it;
i have an array with values and chosen list
and a function to add item to the chosen list
import React, { useState } from "react";
import Parent from "./Parent";
export default function App() {
const [chosenList, setChosenList] = useState([]);
const array = ["dsadas", "dasdas", "dasdasd"];
const addToChosenList = string => {
setChosenList([...chosenList, string]);
};
return (
<div className="App">
<Parent
arr={array}
chosenList={chosenList}
addToChosenList={addToChosenList}
/>
</div>
);
}
Parent component that mapping through the array
and give the Nested component the props: item, addToChosenList, inList
import React from "react";
import Nested from "./Nested.js";
export default function Parent({ arr, addToChosenList, chosenList }) {
return (
<div className="App">
{arr.map((item, index) => (
<Nested
key={index}
item={item}
addToChosenList={addToChosenList}
inList={chosenList.findIndex(listitem => listitem === item) > -1}
/>
))}
</div>
);
}
Nested component that displays the item and giving it the addToChosenList function to add the item to the chosen list
import React, { memo } from "react";
export default memo(function Parent({ item, addToChosenList, inList }) {
const childFunctionToAddToChosenList = () => {
addToChosenList(item);
};
return (
<div className="App" onClick={childFunctionToAddToChosenList}>
<div>{item}</div>
{inList && <div>in List</div>}
</div>
);
});
every Nested component keeps re-render after i clicked only one item in the list
i believe it renders because of the function addToChosenList that changes when i change the state
anyone knows how to solve it ??
thanks :)

addToChosenList will point to a new reference on every re-render, wrap it in useCallback which will keep the same reference across re-renders unless one of the variables inside of the dependencies array has changed, if we pass an empty array the function will keep the same reference across the entire component lifecycle.
you will also need to use a functional update to avoid stale state due to the closure
const addToChosenList = useCallback(string => {
setChosenList(prevState => [...prevState, string]);
}, []);

Related

React child callback not being executed after being passed down twice

I am working on the following project https://github.com/codyc4321/react-udemy-course section 11 the videos app. The udemy course is found at https://www.udemy.com/course/react-redux/learn/lecture/12531374#overview.
The instructor is passing a callback down to multiple children and calling it in the lowest videoItem and the code is supposed to console log something out. I have no console log in my browser even though I've copied the code as written and double checked for spelling errors.
At the main level is App.js:
import React from 'react';
import youtube from '../apis/youtube';
import SearchBar from './SearchBar';
import VideoList from './VideoList';
class App extends React.Component {
state = {videos: [], selectedVideo: null};
onTermSubmit = async term => {
const response = await youtube.get('/search', {
params: {
q: term
}
});
// console.log(response.data.items);
this.setState({videos: response.data.items});
};
onVideoSelect = video => {
console.log('from the app', video);
}
render() {
return (
<div className="ui container">
<SearchBar onFormSubmit={this.onTermSubmit} />
<VideoList
onVideoSelect={this.onVideoSelect}
videos={this.state.videos} />
</div>
)
}
}
export default App;
videoList.js
import React from 'react';
import VideoItem from './VideoItem';
const VideoList = ({videos, onVideoSelect}) => {
const rendered_list = videos.map(video => {
return <VideoItem onVideoSelect={onVideoSelect} video={video} />
});
return <div className="ui relaxed divided list">{rendered_list}</div>;
};
export default VideoList;
the videoItem.js
import React from 'react';
import './VideoItem.css';
const VideoItem = ({video, onVideoSelect}) => {
return (
<div onClick={() => onVideoSelect(video)} className="item video-item">
<img
src={video.snippet.thumbnails.medium.url}
className="ui image"
/>
<div className="content">
<div className="header">{video.snippet.title}</div>
</div>
</div>
);
}
export default VideoItem;
The code that isn't running is
onVideoSelect = video => {
console.log('from the app', video);
}
My guess is that it has something to do with a key prop not being present in the map - I'm not super well versed with class components but I can't find anything else funky so maybe try adding a unique key prop in the map.
When rendering components through a map react needs help with assigning unique identifiers to keep track of re-renders etc for performance, that also applies to knowing which specific instance called a class method.
If you don't have a unique ID in the video prop you can use an index in a pinch, although ill advised, it can be found as the second parameter in the map function. The reason it's ill advised to use an index is if there are multiple children with the same index in the same rendering context, obviously the key parameter could be confused.
Okay-ish:
const rendered_list = videos.map((video, index) => {
return <VideoItem key={index} onVideoSelect={onVideoSelect} video={video} />});
Better:
const rendered_list = videos.map((video, index) => {
return <VideoItem key={video.id} onVideoSelect={onVideoSelect} video={video} />});

React useState within mapped parent component

Im new to React and im trying to understand useState.
So im pulling in an array of products and then mapping the array to display each product card:
export default function ProductList({ products }) {
return (
<Grid templateColumns='repeat(3, 1fr)' columnGap={6} rowGap={10}>
{
products.map(product => (
<ProductCard key={product.node.id} product={product} productID={product.node.id} />
))
}
</Grid>
)
}
and inside of my ProductCard I have a bunch of useState, to handle product options, variants, etc.
const [available, setAvailable] = useState(true)
const [selectedVariant, setSelectedVariant] = useState('')
const [selectedOptions, setSelectedOptions] = useState('')
const { addToCart } = useContext(CartContext)
So my question is, do each of the ProductCard actually share the same useState, even though they are being mapped?
You map collection of separate components, they won't share logic or state. State is hermetic and other components wont have access to it unless allowed to.

Implementing a function to swap array elements in React without mutating the original array

I am coding a react app in which a user can click a button to swap an item in an array with the item to its left. I wrote a function to implement this without mutating the original items array that is rendering on the page, but this function is not doing anything to my code, nor is it returning any errors.
Here is my app component, which defines the function swapLeft then passes that function down to the Item component as props:
import React, { useState } from "react";
import Form from "./components/Form";
import Item from "./components/Item";
import { nanoid } from "nanoid";
import './App.css';
function App(props) {
const [items, setItems] = useState(props.items);
function deleteItem(id) {
const remainingItems = items.filter(item => id !== item.id);
setItems(remainingItems);
}
function swapLeft(index) {
const index2 = index - 1;
const newItems = items.slice();
newItems[index] = items[index2];
newItems[index2] = items[index];
return newItems;
}
const itemList = items
.map((item, index) => (
<Item
id={item.id}
index={index}
name={item.name}
key={item.id}
deleteItem={deleteItem}
swapLeft={swapLeft}
/>
));
function addItem(name) {
const newItem = { id: "item-" + nanoid(), name: name };
setItems([...items, newItem]);
}
return (
<div className="form">
<Form addItem={addItem} />
<ul className="names">
{itemList}
</ul>
</div>
);
}
export default App;
And the Item component:
import React from "react";
import { Button, Card, CardContent, CardHeader } from 'semantic-ui-react'
export default function Item(props) {
return (
<Card>
<CardContent>
<CardHeader> {props.name}</CardHeader>
<Button onClick={() => props.deleteItem(props.id)}>
Delete <span className="visually-hidden"> {props.name}</span>
</Button>
</CardContent>
<CardContent style={{ display: 'flex' }}>
<i className="arrow left icon" onClick={() => props.swapLeft(props.index)} style={{ color: 'blue'}}></i>
<i className="arrow right icon" style={{ color: 'blue'}}></i>
</CardContent>
</Card>
);
}
Is there a better way for me to write this function and implement this? I suppose I could do something with the React setState hook, but this seemed like an easier solution. I am new to React so any insight would be helpful
The way React knows if the state has changed is whether the state is refers to an entirely different address in memory. In case of arrays, if you want React to rerender the page because the array in the state changed, you need to provide it an entirely new array. Modifying the existing array will not trigger the render process.
Basically, what you need to do is changed the last line of swapLeft function to
setItems(newItems)
If you want the changes to take effect immediately (which is what I guess you want to do here)
You can also use the return value from the function and change the state in another component, FYI.
EDIT:
I looked at this again, and your implementation of swap is also wrong, but even if you corrected it you still wouldn't see a change, unless you did what I mentioned above
The full correct function would be
function swapLeft(index) {
const index2 = index - 1;
const newItems = items.slice();
const temp = items[index];
newItems[index] = items[index2];
newItems[index2] = temp;
setItems(newItems);
}
Just to maybe clarify the previous one. If you don't call setState, your component doesn't rerender. This means that no matter what you do with those arrays, it won't be visible on the screen.

react-window stop unnecessary rendering of child FixedSizeList rows when scrolling parent FixedSIzeList

In my react project I am using react-window package to render nested lists. Each parent FixedSizeList row renders a component which uses another FixedSizeList. Parent List doesn't have more than 14 rows at the moment. But the child List may contain upto 2000 rows. Now my problem is, when I try to scroll through the parent List, all the child list items in the viewport seem to re rendering. This is a little bit problematic for me because in my child list item I am using d3js to draw bar chart with transition effect. So these unnecessary re rendering is giving a overall weird UI. Can anyone help me how can I stop these unnecessary renders.
Here is codesandbox link to a very simple example of my problem.
Please open the console log. After initial load the topmost log should be like this: initial console log.
Then if you clear the console and scroll the parent list, you will see log like this: console log after parent scrolling. Here you can see that the child list items of child list 0 is re rendering which is not needed for me.
Can anyone give me a solution that can stop these re rendering?
*P.S. I am not using memo since every row is updating the dom on its own.
Edit
I think this problem would solve if the parent list would stop propagating scroll event to child. I tried to add event.stopPropagation() and event.stopImmediatePropagation() in the parent list row but the output was the same as earlier.
We can use memo to get rid of components being re-rendered unnecessarily for same set of props. And use useCallback to prevent re-creation of a function and thus secure child components being re-rendered. Applying those, we can get this solution:
import "./styles.css";
import { FixedSizeList as List } from "react-window";
import { memo, useCallback } from "react";
const Row = memo(({ index: parentIndex, style: parentStyle }) => {
console.log("rendering child list", parentIndex);
const InnerRow = useCallback(({ index, style }) => {
console.log("rendering child list item", index, "of parent ", parentIndex);
return <div style={style}>Child Row {index}</div>;
}, []);
return (
<div style={parentStyle}>
<List height={200} itemCount={1000} itemSize={35} width={270}>
{InnerRow}
</List>
</div>
);
});
const Example = () => {
console.log("rendering parent list");
return (
<List height={400} itemCount={16} itemSize={300} width={300}>
{Row}
</List>
);
};
export default function App() {
return (
<div className="App">
<Example />
</div>
);
}
although the above code works fine, it can be optimized more if we use areEqual method from react-window as react memo dependency. And for more if we want to use other react hooks inside InnerRow component, we must add a middleware component of InnerRow. The full example is given below:
import { FixedSizeList as List, areEqual } from "react-window";
import { memo, useCallback } from "react";
const Row = memo(({ index: parentIndex, style: parentStyle }) => {
console.log("mounting child list", parentIndex);
const data = new Array(15).fill(new Array(500).fill(1));
const InnerRowCallback = useCallback(
({ index, style }) => {
return <InnerRow index={index} style={style} />;
},
[data]
);
const InnerRow = ({ index, style }) => {
console.log("mounting child list item", index, "of parent ", parentIndex);
return <div style={style}>Child Row {index}</div>;
};
return (
<div style={parentStyle}>
<List height={200} itemCount={1000} itemSize={35} width={270}>
{InnerRowCallback}
</List>
</div>
);
}, areEqual);
const Example = () => {
console.log("mounting parent list");
return (
<List height={400} itemCount={16} itemSize={300} width={300}>
{Row}
</List>
);
};
export default function App() {
return (
<div className="App">
<Example />
</div>
);
}
Here I am passing data array as useCallBack dependency because I want to re render the InnerRow component if data gets changed.

React Architecture: I have a common middle component that needs to render custom child components

I'm making a dashboard that uses a common grid component. The grid has its own functionality separate and is used in other areas of the app. It needs to render a custom component within each grid item and has an active component that also renders a custom component, these custom components use functions from the Grids parent component, whatever is rendering it, below is how I do it current, but I'm pretty sure there is a better way of doing it.
Parent component that renders grid and passes down components and functions
import Grid from './common/grid'
class dashboard extends Component {
gridItemSpecificFunction() { console.log('success') }
activeFunction() { console.log('success again') }
render() {
return <Grid
CustomComponent={ CustomComponent }
ActiveComponent={ ActiveComponent }
activeFunctions={ {activeFunction} }
gridItemFunctions={ { gridItemSpecificFunction:this.gridItemSpecificFunction } }
/>
}
}
Grid that renders custom active and grid items based on data its passed
class Grid extends Component {
render() {
const {CustomComponent} = this.props
return (
<GridWrapper>
{ this.props.dynamicData.map( data => (
<GridItemWrapper>
<CustomComponent { ...data } functions={ this.props.gridItemFunctions } />
</GridItemWrapper>
) )
{ active && < ActiveComponent { ...activeData }
functions={ this.props.activeFunctions }/> }
</GridWrapper>
}
)
}
}
example of custom component that is using function passed through grid item
class CustomComponent extends Component {
render() {
const {gridItemSpecificFunction} = this.props.functions
return (
<div onClick={ gridItemSpecificFunction }>
{ this.props.text }
<div>
}
)
}
}
You actually doing great, of course there is another way to do this, probably is better just because become easier to modify, so you probably should use Context hook to get this done, so great packages based their functionalities in Context API Hook, so a great approach would be this
import React, { useContext, createContext, useMemo } from 'react';
const FunctionalityContext = createContext({}); // just leave it as empty state
// create a hook that return the context function
const useGridFunctions = () => {
const functions = useContext(FunctionalityContext);
return functions;
};
const CustomComponent = () => {
const functions = useGridFunctions();
return (
<div>
<button onClick={functions.gridItemSpecificFunction}>I am a custom component</button>
</div>
);
}
const ActiveComponent = () => {
const functions = useGridFunctions();
return (
<div>
<button onClick={functions.activeFunction}>I am a active component</button>
</div>
);
}
const ParentGrid = ({ functions }) => {
const functions = useMemo(() => functions, [functions]);
// the pass functions object to our context provider to get accesso through dom tree
return (
<FunctionalityContext.Provider value={functions}>
<Grid
CustomComponent={CustomComponent}
ActiveComponent={ActiveComponent}
/>
</FunctionalityContext.Provider>
);
}
As you can see you still keep your code almost the same, but you are adding a extra layer that will store the functionalities that will be used by components, so Context Api will help you to achieve this as you want.

Categories

Resources