Parent's methods in array dependencies in useCallback - javascript

Let say we have parent and children components like this:
const {useState, useCallback} = React;
const ComponentB = (props) => {
const [text, setText] = useState('')
const { onClick } = props
const handleChange = useCallback((event) => {
setText(event.target.value)
}, [text])
const handleClick = useCallback(() => {
onClick(text)
}, [onClick, text]) // Should I to take into account 'onClick' props?
return (
<div>
<input type="text" onChange={ handleChange } />
<button type="button" onClick={ handleClick }>Save</button>
</div>
)
}
const ComponentA = () => {
const [stateA, setStateA] = useState('')
const handleSetStateA = useCallback((state) => {
setStateA(state)
}, [stateA])
return (
<div>
<ComponentB onClick={ handleSetStateA } />
{ stateA && `${ stateA } saved!` }
</div>
)
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<ComponentA />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
React documentation says that:
every value referenced inside the callback should also appear in the dependencies array
And I'm wondering if I need to put onClick method to array dependencies in useCallback? And if so, why I should do that?

And I'm wondering if I need to put onClick method to array dependencies in useCallback?
Yes.
And if so, why I should do that?
Because it's possible that your component will get re-rendered with a new and different function for the onClick prop that behaves differently from the old one. Without saying it's a dependency, you'll continue using the old value.
In fact, in your given code, it's not just possible but definite: you create a new handleSetStateA function every time stateA changes.
That said, in ComponentA:
There's no reason to have stateA as a dependency in your useCallback creating handleSetStateA; handleSetStateA never uses stateA. (It uses the state setter function for it, but that's not the same thing.)
There's not really any reason for handleSetStateA at all; just pass setStateA directly as onClick. But I'm assuming you do more than just setting the state in that function and just left things out for the purposes of the question.
(Similarly, in ComponentB there's no reason for text to be a dependency on the useCallback for handleChange; handleChange doesn't use text.)
But even if you change ComponentA to pass setStateA directly (or at least provide a stable function), ComponentB shouldn't rely on onClick being unchanging between renders, so you'd use onClick in your useCallback dependency list regardless.
Finally: There's not much point in using useCallback with functions you're passing to unmemoized components. For it to be useful, the component you're providing the callback function to should be memoized (for instance, via React.memo or, for a class component, via shouldComponentUpdate). See my answer here for details on that.
Here's an updated version of your snippet using React.memo and only the necessary dependencies; I've left handleSetStateA in (I added a console.log so it isn't just a wrapper):
const { useState, useCallback } = React;
const ComponentB = React.memo(({ onClick }) => {
const [text, setText] = useState("");
const handleChange = useCallback((event) => {
setText(event.target.value);
}, []);
const handleClick = useCallback(() => {
console.log(`Handling click when text = "${text}"`);
onClick(text);
}, [onClick, text]);
return (
<div>
<input type="text" onChange={handleChange} />
<button type="button" onClick={handleClick}>
Save
</button>
</div>
);
});
const ComponentA = () => {
const [stateA, setStateA] = useState("");
const handleSetStateA = useCallback((state) => {
console.log(`Setting stateA to "${state}"`);
setStateA(state);
}, []);
return (
<div>
<ComponentB onClick={handleSetStateA} />
{stateA && `${stateA} saved!`}
</div>
);
};
ReactDOM.createRoot(document.getElementById("root")).render(<ComponentA />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

Related

React's Context API re-renders all components that are wrapped inside the Context

This is a very common performance problem while using the Context API. Essentially whenever a state value in the context changes, the entire components that are wrapped between the provider re-renders and causes performance slowdown.
If I have a the wrapper as this:
<CounterProvider>
<SayHello />
<ShowResult />
<IncrementCounter />
<DecrementCounter />
</CounterProvider>
And the value props as:
<CounterContext.Provider value={{increment, decrement, counter, hello }} >
{children}
</CounterContext.Provider>
Everytime I increment the count value from the IncrementCounter component, the entire set of wrapped components re-renders as it is how the Context API is supposed to work.
I did a bit of research and came across these solutions:
Split the Context into N number of Context according to the use-case : This solution works as expected.
Wrap the value provider using React.Memo: I saw a lot of articles suggesting to the React.Memo API as follows:
<CounterContext.Provider
value={useMemo(
() => ({ increment, decrement, counter, hello }),
[increment, decrement, counter, hello]
)}
>
{children}
</CounterContext.Provider>
This however doesn't work as expected. I still can see all the components getting re-rendered. What I'm doing wrong while using the Memo API? Dan Abramov does recommend to go by this approach in an open React issue
If anyone can help me out on this one. Thanks for reading.
"Essentially whenever a state value in the context changes, the entire components that are wrapped between the provider re-renders and causes performance slowdown."
The above statement is true if a context is used like in the below example where components are directly nested in the provider. All of them re-render when count changes, no matter wether they are called useContext(counterContext) or not.
const counterContext = React.createContext();
const CounterContextProvider = () => {
const [count, setCount] = React.useState(0);
return (
<counterContext.Provider value={{ count, setCount }}>
<button onClick={() => setCount((prev) => prev + 1)}>Change state</button>
<ComponentOne/>
<ComponentTwo />
</counterContext.Provider>
);
};
const ComponentOne = () => {
console.log("ComponentOne renders");
return <div></div>;
};
const ComponentTwo = () => {
console.log("ComponentTwo renders ");
return <div></div>;
};
function App() {
return (
<CounterContextProvider/>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
"Essentially whenever a state value in the context changes, the entire components that are wrapped between the provider re-renders and causes performance slowdown."
The statement is false if you are consuming nested components with children. This time when count changes CounterContextProvider renders, but since it's rendering because its state has changed and not because of its parent rendering, and because a component cannot mutate its props, React won't render children. That's it if it was a normal component.
But since there is a context involved here, React will find all components that contain useContext(counterContext) and render them.
const counterContext = React.createContext();
const CounterContextProvider = ({ children }) => {
const [count, setCount] = React.useState(0);
return (
<counterContext.Provider value={{ count, setCount }}>
<button onClick={() => setCount((prev) => prev + 1)}>Change state</button>
{children}
</counterContext.Provider>
);
};
const ComponentOne = () => {
const { count } = React.useContext(counterContext);
console.log("ComponentOne renders");
return <div></div>;
};
const ComponentTwo = () => {
console.log("ComponentTwo renders ");
return <div></div>;
};
function App() {
return (
<CounterContextProvider>
<ComponentOne />
<ComponentTwo />
</CounterContextProvider>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In the above example only ComponentOne renders when count changes, which is normal cause he is consuming it. Every component that calls useContext(counterContext) renders if one value of the context changes.
Even with useMemo wrapping the context object as you did, that's the behavior you get as soon as one variable in its dependency array changes.

Memorize react change handler

I know that react component will render everytime state changes and useCallBack memorize function depending on the dependency array provided.
But If I have this:
<input type="text" name="name" value={values.name} onChange={handleChange} />
<input type="text" name="phone" value={values.phone} onChange={handleChange} />
Whats the difference between these two handleChange implementation?
const handleChange = e => {
setValues(values => ({ ...values, [e.target.name]: e.target.value }))
}
and
const handleChange = useCallback(e => {
setValues(values => ({ ...values, [e.target.name]: e.target.value }))
}), [])
Code snippet needed in comment
const Child = () => {
console.log("Child rerendered");
return <div>Hello World </div>;
};
export default function App() {
const [count, setCount] = useState(0);
console.log("Parent rerendered");
return (
<div className="App">
<Child />
<button
onClick={() => {
setCount((x) => x + 1);
}}
>
{count}
</button>
</div>
);
}
The difference is that with the second one using useCallback, you'll keep using the first handleChange function you create; with the first one where you don't use useCallback, you're creating a new handler function each time. (Technically, you create a new one every time in both cases, but useCallback will only return the first one that gets created, because you've provided an empty dependency array.) More in the documentation.
It doesn't matter much when giving the handler function to input or other native HTML elements, or even simple components. If you were providing this handler function to a complex component, it might be better to use useCallback if that complex component optimizes re-rendering (for instance, with React.memo, PureComponent, or shouldComponentUpdate).

Can't pass useState() 'set' function to grand child

I'm having issues trying to get my useState variable to work. I create the state in my grandparent then pass it into my parent. Here's a simplified version of my code:
export function Grandparent(){
return(
<div>
const [selectedID, setSelectedID] = useState("0")
<Parent setSelectedID2={setSelectedID} .../> //(elipses just mean that I'm passing other params too)
<div />
)}
Parent:
const Parent = ({setSelectedID2 ...}) => {
return(
<div>
{setSelectedID2("5")} //works
<Child setSelectedID3={setSelectedID2} />
</div>
)
}
From the parent I can use 'setSelectedID2' like a function and can change the state. However, when I try to use it in the child component below I get an error stating 'setSelectedID3' is not a function. I'm pretty new to react so I'm not sure if I'm completely missing something. Why can I use the 'set' function in parent but not child when they're getting passed the same way?
Child:
const Child = ({setSelectedID3 ...}) => {
return(
<div >
{setSelectedID3("10")} //results in error
</div>
);
};
In React you make your calculations within the components/functions (it's the js part) and then what you return from them is JSX (it's the html part).
export function Grandparent(){
const [selectedID, setSelectedID] = useState("0");
return(
<div>
<Parent setSelectedID2={setSelectedID} .../> //(elipses just mean that I'm passing other params too)
<div />
)}
You can also use (but not define!) some js variables in JSX, as long as they are "renderable" by JSX (they are not Objects - look for React console warnings).
That's your React.101 :)
Here's a working example with everything you have listed here. Props are passed and the function is called in each.
You don't need to name your props 1,2,3.., they are scoped to the function so it's fine if they are the same.
I moved useState and function calls above the return statement, because that's where that logic should go in a component. The jsx is only used for logic dealing with your display/output.
https://codesandbox.io/s/stupefied-tree-uiqw5?file=/src/App.js
Also, I created a working example with a onClick since that's what you will be doing.
https://codesandbox.io/s/compassionate-violet-dt897?file=/src/App.js
import React, { useState } from "react";
export default function App() {
return <Grandparent />;
}
const Grandparent = () => {
const [selectedID, setSelectedID] = useState("0");
return (
<div>
{selectedID}
<Parent setSelectedID={setSelectedID} selectedID={selectedID} />
</div>
);
};
const Parent = ({ selectedID, setSelectedID }) => {
setSelectedID("5");
return (
<div>
{selectedID}
<Child setSelectedID={setSelectedID} selectedID={selectedID} />
</div>
);
};
const Child = ({ selectedID, setSelectedID }) => {
setSelectedID("10");
return <div>{selectedID}</div>;
};
output
10
10
10
const [selectedID, setSelectedID] = useState("0")
should be outside return

export Hooks in React for Nested Components?

I'm exporting hooks with nested components so that the parent can toggle state of a child. How can I make this toggle work with hooks instead of classic classes or old school functions?
Child Component
export let visible;
export let setVisible = () => {};
export const ToggleSwitch = () => {
const [visible, setVisibile] = useState(false);
return visible && (
<MyComponent />
)
}
Parent
import * as ToggleSwitch from "ToggleSwitch";
export const Parent: React.FC<props> = (props) => {
return (
<div>
<button onClick={() => ToggleSwitch.setVisible(true)} />
</div>
)
}
Error: Linter says [setVisible] is unused variable in the child... (but required in the parent)
You can move visible state to parent like this:
const Child = ({ visible }) => {
return visible && <h2>Child</h2>;
};
const Parent = () => {
const [visible, setVisible] = React.useState(false);
return (
<div>
<h1>Parent</h1>
<Child visible={visible} />
<button onClick={() => setVisible(visible => !visible)}>
Toggle
</button>
</div>
);
};
If you have many child-components you should make more complex logic in setVisible. Put object to useState where properties of that object will be all names(Ids) of child-components
as you know React is one-way data binding so if you wanna pass any props or state you have only one way to do that by passing it from parent to child component and if the logic becomes bigger you have to make it as a global state by using state management library or context API with react hooks use reducer and use effect.

How can I access a state value done in react hooks in another js file

I am working on a search bar in react which in another file will make a call to the unsplash-api,
I have a search bar component and I am thinking of doing the api call in the main file or if other wise advised in another file in the src folder
So far I have setup a component and setup the initial hook but I dont know how to go forward
import React, { useState } from 'react';
import './SearchBar.css';
const SearchBar = () => {
const [search, setSearch] = useState('');
return (
<form>
<input className="Search" placeholder="Search Images" />
<button type="submit" id="submit" className="search-button">
<i className="icon">search</i>
</button>
</form>
);
};
export default SearchBar;
See if that's what you're looking for. I replaced the form for a div to avoid submit behavior in the snippet. But the logic is the same. You'll need to do event.preventDefault on the submission event.
function mockSearchAPI(searchValue) {
return new Promise((resolve,reject) => {
setTimeout(() => resolve('Search results for: ' + searchValue), 2000);
});
}
function App() {
const [searchResults,setSearchResults] = React.useState('');
const [loading, setLoading] = React.useState(false);
function doSearch(searchValue) {
setLoading(true);
mockSearchAPI(searchValue)
.then((results) => {
setSearchResults(results);
setLoading(false);
});
}
return(
<React.Fragment>
<SearchBar
doSearch={doSearch}
/>
{loading &&
<div>Loading...</div>
}
{searchResults &&
<div>{searchResults}</div>
}
</React.Fragment>
);
}
const SearchBar = (props) => {
const [search, setSearch] = React.useState('');
function onClick() {
console.log(search);
props.doSearch(search);
}
return (
<div>
<input
//className="Search"
placeholder="Search Images"
onChange={(event)=>setSearch(event.target.value)}
/>
<button
//type="submit"
//id="submit"
//className="search-button"
onClick={onClick}
>
<i className="icon">search</i>
</button>
</div>
);
};
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
From my understanding you should be able to pass the state in Hooks (from the search bar) to the parent component in these methods.
Use Redux and create a global store with the required state
variable. Then from the search component you will need to update the
state variable in the Redux store, then load this state variable in
the parent component.
Create a state variable from the parent component and have it
passed to the search component as a prop. Then from the search
component, you will update this prop.
I am not not 100% sure if method 2 will work as expected but I am pretty sure method 1 will work.

Categories