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}/>
Related
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?
I have a hook, and 2 components. Component App.js has a function that changes the state in hook, but the value is not updated in Component New.js, why? I think I've missed something but can't figure it out.
App.js
export const useToggle = () => {
const [onOff, setOnOff] = useState(false);
return [onOff, () => setOnOff((prev) => !prev)];
};
export default function App() {
const [onOff, setOnOff] = useToggle();
return (
<div className="App">
<h1>{onOff.toString()}</h1>
<button onClick={setOnOff}>toggle</button>
</div>
);
}
New.js
import { useToggle } from "./App.js";
export default function New() {
const [onOff] = useToggle();
return (
<div className="App">
<hr />
<h1>NEW:</h1>
<pre>{onOff.toString()}</pre>
</div>
);
}
https://codesandbox.io/s/musing-fire-rjude?file=/src/App.js
Each useToggle hook is its own entity with its own state. The useToggle that you are toggling in App isn't the same useToggle that is rendered/used in New.
This means they are toggled independently of any other hooks and state. They don't share "state".
If you are wanting to create a useToggle hook that does have shared state then I would suggest implementing it via a React context and the useContext hook so each useToggle hook can toggle the same shared state held in the context.
Update
Global useToggle hook.
togglecontext.js
import { createContext, useContext, useState } from 'react';
export const ToggleContext = createContext([false, () => {}]);
const ToggleProvider = ({ children }) => {
const [onOff, setOnOff] = useState(false);
const toggle = () => setOnOff(t => !t);
return (
<ToggleContext.Provider value={[onOff, toggle]}>
{children}
</ToggleContext.Provider>
);
}
export const useToggle = () => useContext(ToggleContext);
export default ToggleProvider;
index - provide the context
...
import ToggleProvider from "./toggle.context";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<ToggleProvider>
<App />
<New />
</ToggleProvider>
</StrictMode>,
rootElement
);
App
import "./styles.css";
import { useToggle } from "./toggle.context";
export default function App() {
const [onOff, setOnOff] = useToggle();
return (
<div className="App">
<h1>{onOff.toString()}</h1>
<button onClick={setOnOff}>toggle</button>
</div>
);
}
New
import { useToggle } from "./toggle.context";
export default function New() {
const [onOff] = useToggle();
return (
<div className="App">
<hr />
<h1>NEW:</h1>
<pre>{onOff.toString()}</pre>
</div>
);
}
Note that the only thing that changed in the App and New components was the import, where the useToggle hook is defined.
I'm trying to wrap all components under the app in a context to provide the things that I want
(as You can see it's my UserContext component)
enter code here
import React, { useState, createContext, useContext } from 'react'
const Context = createContext();
export let useUserContext = () => useContext(Context);
export default function UsersContext({ children }) {
const [users, setUsers] = useState();
const createUser = (user) => {
if (!user.name || !user.email || !user.password) return;
const newUsers = [...users, user];
setUsers(newUsers);
}
return (
<Context.Provider value={{ users, createUser }}>
{children}
</Context.Provider>
)
}
(it is my app component)
enter code here
import Nav from "./components/nav/Nav";
import Container from "./components/container/Container";
import { BrowserRouter } from "react-router-dom";
import UsersContext from "./components/contexts/UserContext";
function App() {
return (
<UsersContext>
<BrowserRouter>
<Nav />
<Container />
</BrowserRouter>
</UsersContext>
);
}
export default App;
It's used to be like this in my projects and I didn't have any problem but now
the error I'm getting "TypeError: (destructured parameter) is undefined" also says that it's because of the children in UserContext In my opinion it shouldn't happen maybe you can help me to find the problem I can't see.
Try: <Context.Provider value={[ users, { createUser } ]}>
instead of: <Context.Provider value={{ users, createUser }}>
edit:
also might try:
instead of
const newUsers = [...users, user];
setUsers(newUsers);
do
setUsers((currentUsers) => [...currentUsers, user]);
I found the problem. it was because of the useState. it was undefined and I was calling the property that related to useState at the UserContext.
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;