I'm trying to understand useCallback() a bit better by creating a simple React app and playing around with it.
I tried wrapping handleClick function in useCallback() statement and my expectation was that ItemList component should only be re-rendered if I click the count button, but not when I change the theme. However, it's rerendering on either of those button clicks and I'm not exactly sure why.
This is my code (GitHub repo available below):
index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import SebComponent from './SebComponent';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.Fragment>
<App />
<SebComponent />
</React.Fragment>
);
App.js
function App() {
return (
<div>
<p>This is App</p>
{console.log("App compoent rendered")}
</div>
);
}
export default App;
SebComponent.js
import React, { useCallback, useState } from "react";
import ItemList from "./itemList";
import "../src/sebstyle.css"
function SebComponent(){
console.log("rendering SebComponent...")
const [count, setCount] = useState(1)
const [dark, setDark] = useState(false)
let inlineStyle = {backgroundColor: dark ? "green" : "white"}
const handleClick = useCallback(() => {
setCount(count + 1)
}, [count])
return(
<div className="sebComponent" style={inlineStyle}>
<p>This is SebComponent</p>
<button onClick={handleClick}> {count} </button>
<br/>
<br/>
<button onClick={() => {setDark(x => !x)}}> change theme </button>
<ItemList count={count}/>
</div>
)
}
export default SebComponent;
itemList.js:
import React from "react";
export default function ItemList(props){
console.log("rendering item list...")
let myArray = [];
for (let index = 1; index < props.count; index++) {
myArray.push( 'item' + index);
}
console.log(myArray);
return(
<div>
<p>hello</p>
{myArray.map(
x => {
return (
<p key={"item" + x}> {x} </p>
)
}
)
}
</div>
)
}
sebstyle.css:
.sebComponent{
border: 2px black solid;
display:block;
align-items: center;
padding: 5px;
}
.sebComponent > button{
margin-left: 10px;
width: 100px;
height: 40px;
}
I tried creating something similar to what this guy did in his video. This is my GitHub repo created to play around with this.
If you want to skip rendering ItemList, then ItemList needs to use React.memo. This will make it so if ItemList's props have not changed, then ItemList will not rerender:
import { memo } from 'react';
function ItemList(props){
// ...
}
export default memo(ItemList);
The only role useCallback serves in preventing rendering is to make sure that props do not change. That in turn can allow memo to do its job. But handleClick is never being passed to ItemList, so nothing is happening to item list by memomizing handleClick
When one of those hook is invoked ( const [count, setCount] = useState(1)
const [dark, setDark] = useState(false) ) React re-render all the page. You can work-around with some library as redux that let manage way more better hook.
for more info read this: How to prevent re-rendering of components that have not changed?
Related
I want to render the component if its props are updated others don't need to render. But in my code, I can see it's rendering with unexpected behavior.
React Version 17.X.X
See below code
index.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
app.js file
import React, { useState, useCallback} from 'react'
import ListItem from './ListItem'
function App() {
const [itemList, setItemList] = useState([]);
const [counter, setCounter] = useState(0);
const increase = useCallback(() => setCounter(prevState => prevState + 1), counter);
return (
<div className="App">
<ListItem itemList={itemList} addItem={(val) => setItemList(prev => [...prev,val])}/>
<button onClick={increase}> Increment Count </button>
</div>
);
}
export default App;
ListItem.js
import React, {memo} from 'react';
const ListItem = memo(props) => {
console.log('listItem render...');
return (
<div>{props.itemList.map((c) => <span>{c}</span>)}</div>
<button onClick={() => props.addItem(props.itemList.length + 1)}>+ Add Item</button>
)
}
export default ListItem;
You can see my code and now I want to understand how I can avoid unexpected rendering. Because useCallback using I can avoid re-initiation of that method. but the ListItem component if you can see it's rendering Even if you are adding counter value.
Let me know if you have any questions.
Thanks for the help.
It is because addItem has a method type prop so you need to create a useCallback() for that method which you have passed as a prop in addItem. This will work for you:
const onAddItem = useCallback((val) => {
setItemList(prev => [...prev,val]);
}, [itemList]);
<ListItem itemList={itemList} addItem={onAddItem}/>
I am having two buttons in a component thats under App component. Even though it is dispatching actions and store is updated, App is not re-rendered. Ideally on button press, I add some dummy name that will be put inside array and then a different component is rendered based on number items inside this array.
Can someone tell what is going wrong, I am a beginner to react and redux
Sandbox: https://codesandbox.io/s/panel-item-with-redux-hxmihq?file=/src/PanelAdder.js
App.jsx
import React from "react";
import "./styles.css";
import { Panel } from "./Panel";
import PanelAdder from "./PanelAdder";
import { useSelector } from "react-redux";
export default function App() {
const panels = useSelector((state) => state.panelItems);
return (
<>
<PanelAdder />
<div className="cards-container">
{panels || [].map((name) => <Panel {...{ name }} />)}
</div>
</>
);
}
PanelAdder.jsx
import React from "react";
import { useDispatch } from "react-redux";
import { addPanel } from "./actions/counterActions";
export default function PanelAdder() {
const dispatch = useDispatch();
const handleClick = (name) => {
dispatch(addPanel(name));
};
return (
<>
<button onClick={() => handleClick("Panel 1")}> Add Panel 1</button>
<button onClick={() => handleClick("Panel 2")}> Add Panel 2</button>
</>
);
}
Panel.jsx
import React from "react";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import {
faSquareMinus,
faWindowMaximize,
faRectangleXmark
} from "#fortawesome/free-solid-svg-icons";
export const Panel = ({ name }) => {
return (
<>
<div className="card">
<div className="card-actions">
<FontAwesomeIcon icon={faSquareMinus} />
<FontAwesomeIcon icon={faWindowMaximize} />
<FontAwesomeIcon icon={faRectangleXmark} />
</div>
<div className="card-body">{name}</div>
</div>
</>
);
};
You have 2 issues:
First in your combineReducers function you are adding the reducer called panels. Redux will add this to the state object. So when you are referencing it you need to refrence the panels object that is nested inside your state object. You need to get the following in your useSelector useSelector((state) => state.panel.panelItems)
Second you are doing the following in your app component {panels || [].map((name) => <Panel {...{ name }} />)}. So what you are doing here is displaying the panels array or mapping through an empty array but you never actually map through the panels array. But what you need to do is map through the panels array. So you can just map the panels array {panels.map((name) => <Panel {...{ name }} />)}
So your app component should look like the following:
import React from "react";
import "./styles.css";
import { Panel } from "./Panel";
import PanelAdder from "./PanelAdder";
import { useSelector } from "react-redux";
export default function App() {
const panels = useSelector((state) => state.panel.panelItems);
return (
<>
<PanelAdder />
<div className="cards-container">
{panels.map((name) => <Panel {...{ name }} />)}
</div>
</>
);
}
Consider the code :
APP.JS
import React, { useState, useMemo } from 'react';
import Counter from './components/Counter';
import './App.css';
function App() {
const [countA, setCountA] = useState(0);
const incrementA = () => {
setCountA(countA + 1);
};
// const memoCounter = useMemo(() => {
// return <Counter />;
// }, []);
return (
<div className='App'>
<h1>Incrementing CountA from APP.JS : {countA}</h1>
<p>
<button onClick={incrementA}>Increment A</button>
</p>
{/* {memoCounter} */}
<Counter />
</div>
);
}
export default App;
Counter.js :
import React, { useEffect } from 'react';
let renderCount = 1;
const Counter = () => {
useEffect(() => {
renderCount++;
});
return (
<div>
<h1>Rendering Counter component : {renderCount}</h1>
</div>
);
};
export default Counter;
When the user hits the button and increments , React renders Counter component all over again , even though I don't pass anything to it.
However when I put useMemo it doesn't.
Why ?
By default when a parent component renders (App), it renders all its children (Counter).
To overload the default behaviour, use React API like React.memo:
const Counter = () => {...}
export default React.memo(Counter);
Having this error when running the program:
Invalid hook call. Hooks can only be called inside of the body of a
function component. This could happen for one of the following
reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same ap
import React, { Component } from "react";
import {useEffect,useState} from "react";
import ReactDOM from 'react-dom';
const App = () => {
const APP_ID = "";
const APP_KEY = "";
const exapmle = "https://api.edamam.com/search?
q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}";
useEffect(()=>{
console.log("effect has been");
});
const [counter,setCounter] = useState(0);
return (
<div>
<h1>hello world</h1>
<form classname="search-form">
<input classname="search_bar" type="text"></input>
<button classname="search-button" type="submit">Search</button>
</form>
<h1 onClick = {()=> setCounter(counter+1)}> {counter}</h1>
</div>);};
export default App();
Try export default App instead of export default App().
I made a few other tweaks you can test in CodeSandbox, namely:
Using backticks to embed template literals in example
Limiting useEffect to a single execution by adding a second argument of []
import React from 'react';
import { useEffect, useState } from 'react';
const App = () => {
const APP_ID = '';
const APP_KEY = '';
const example = `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`;
useEffect(() => {
console.log('useEffect will run once if I pass it a second argument of []');
console.log(example);
// eslint-disable-next-line
}, []);
const [counter, setCounter] = useState(0);
return (
<div>
<h1>hello world</h1>
<form className='search-form'>
<input className='search_bar' type='text'></input>
<button className='search-button' type='submit'>
Search
</button>
</form>
<h1 onClick={() => setCounter(counter + 1)}> {counter}</h1>
</div>
);
};
export default App;
I was trying to add a css class on mouse over just as mentioned in the react documentation but I am not sure why it is not working.
Please help
index.js:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
let className = "";
return (
<div className="App">
<h1
className={className}
onMouseOver={() => {
if (!className.includes("colorRed")) {
className += " colorRed";
}
}}
>
Hello CodeSandbox
</h1>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
style.css
.App {
font-family: sans-serif;
text-align: center;
}
.colorRed{
color:red;
}
Link: https://codesandbox.io/s/xjx72w5kkw?fontsize=14
This issue here is that you're mutating the className variable but this alone doesn't cause React to re-render your component.
You'll need to store the className in the state so that when you change it, React knows to re-render your component.
Here's a version of your sandbox making use of state: https://codesandbox.io/s/m51qoq72pp
// First we're importing useState from react
// This is part of the new Hooks mechanism
// More about hooks: https://reactjs.org/docs/hooks-intro.html
// You could also use a class and this.setState instead
import React, { useState } from "react";
// Then we set up our piece of state
const [className, setClassName] = useState("");
// Your event now uses setState
onMouseOver={() => {
if (!className.includes("colorRed")) {
setClassName(current => current + " colorRed");
}
}}