How to update a useState hook in a child component? - reactjs

I am pretty new to React Hooks and could not understand how to place a useState() function inside a child component, both in a child's body and in a child's inner function, like this:
function Parent () {
const [counter, setCounter] = React.useState();
return (
<Child counter={counter} setCounter={setCounter}/>
)
}
function Child (props) {
const counter = props.counter;
const setCounter = props.setCounter;
React.useEffect(() => {
setCounter(0);
})
const increment = () => {
setCounter(1);
}
return (
<div> {counter} <button onClick={increment}>Increase Count</button> </div>
)
}
My code keeps reupdating the state to 0. What's wrong?
Please check my fiddle.
Thanks!

You missed the second argument in useEffect.
useEffect will trigger on every render if the second argument is not there and it will reset the changed value back to 0 again
React.useEffect(() => {
setCounter(0);
},[])
Full code sample
function Parent () {
const [counter, setCounter] = React.useState();
return (
<Child counter={counter} setCounter={setCounter}/>
)
}
function Child (props) {
const counter = props.counter;
const setCounter = props.setCounter;
React.useEffect(() => {
setCounter(0);
}, [])
const increment = () => {
setCounter(counter + 1);
}
return (
<div> {counter} <button onClick={increment}>Increase Count</button> </div>
)
}

Your code works: https://codesandbox.io/s/billowing-bird-p9xzp?file=/src/App.js
But there are several things to consider here:
1- you can initialize the state to 0 in const [counter, setCounter] = React.useState(0);
2- You dont need useEffect in child component

Your useEffect hook is missing a dependency array, so it is being called every time the child component renders. This means every time you click increment, the state gets set to 1, but then your useEffect immediately fires again and resets the count to 0. Fix this by placing an empty dependency array at the end of your useEffect (this means it will only fire once when the component is first mounted):
React.useEffect(() => {
setCounter(0);
}, [])
Also if you are want your button to actually increment the count by one every time you have to setCounter to 1 + the previous state, otherwise the counter will just stay at 1 every time it is clicked:
const increment = () => {
setCounter(prevCounter => prevCounter + 1);
}

Related

useEffect rerender the whole table when counter changing

I have some problem with useEffect. When the counter changes it causes the whole table to be rerendered, but i dont pass timer as props in table. How i can prevent this behavior?
function App() {
const dispatch = useDispatch();
const data = useSelector(state => state.data);
const [error, setError] = useState("");
const [counter, setCounter] = useState();
useEffect(() => {
const fetchData = async (setError, setCounter) => {
try {
const response = await axios(url, token);
dispatch(getData(response.data.value));
setError("");
setCounter(180);
} catch(e) {
setError("Error!");
setCounter(180);
}}
fetchData(setError, setCounter);
const interval = setInterval(() => {
fetchData(setError, setCounter);
}, timeToReload * 1000);
const countInterval = setInterval(() =>
setCounter((prev) => prev - 1), 1000)
return () => {
clearInterval(interval);
clearInterval(countInterval);
}
},[dispatch])
const dataForTable = selectorData([...data], {name: sortArrow.columnName, order: sortArrow.sortOrder, type: sortArrow.type})
return (
<div className="App">
<div className="headerWrapper">
<div
className={error ? "LoadingStatus disconnect": "LoadingStatus connect"}>
{error && <div>{error}</div>}
{isFinite(counter) && <div>{"Reload " + counter + " sec"}</div> }
</div>
</div>
<Table specialCategory={specialCategory} data={dataForTable} sortArrow={sortArrow} setSortArrow={setSortArrow}/>
</div>
);
}
export default App;
I trued to useRef without useState, but nothing has changed. Maybe another props in Table component trigger the change?
Imptortant notice: only the body of the table is changed.
When you update the state (e.g. setCounter(...)) of the App component, it causes the entire component with all of it's child-components, including <Table/>, to be re-rendered.
You can either create a new component for everything except the table and put the states which are changing (error and counter) into that file, or memoize the Table component like this:
import { memo } from "react";
function Table(props) {
return (
// [...] Your Table component.
);
}
export default memo(Table);
And import it just as you do already. This will avoid re-rendering the table unless its props change.
See Here the Reasons of React Re-rendering:
If a Parent Component Re-renders, the Children re-render automatically. & because your counter state is on the Table Components parent, It will re-render every time Counter changes.

Why does child component re-render even if I have used useCallback?

const Root = () => {
/** useState */
const [count, setCount] = useState(1);
const [times, setTimes] = useState(1);
const handleCount = useCallback(() => {
setCount(count * 2);
}, []);
const handleTimes = useCallback(() => {
setTimes(times + 2);
}, []);
return (
<div>
<div>Root. counts: {count}</div>
<Counter onClick={handleCount} value={count} />
<Counter onClick={handleTimes} value={times} />
</div>
);
};
The child component will re-render when parent component re-renders, so I don't know why I should use useCallback. When will the function in useCallback change without my component re-rendering?
I suggest you to use React.memo in your child component.
This code will help you,
export const Counter = React.memo(props => {
return ...
})
Here is the doc link.
First, you haven't defined a dependency array for either of your useCallbacks. The child components would rerender if the function were rebuilt, or if the value were to change. Use should also be using the lazy set method of state.
const handleCount = useCallback(() => {
setCount(prev => prev * 2);
}, [setCount]);

how can useRef be used for storing previous state

function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count; });
const prevCount = prevCountRef.current;
return <h1>Now: {count}, before: {prevCount}</h1>;
}
In the above snippet from the React Hoks FAQS useRef is used for saving the count. However whenever render is called, will not the prevCount be set to the current count as the useEffect will be called on each render, so how are count and prevCount different ?
That's because the useEffect callback function will be called after the rendering time of Counter Component has finished. and that's why prevCount is always one tick behind.
one point to consider is the change in in a React ref won't cause a rerender in React Component only change in state and props will cause a rerender.
see the working example here: https://codesandbox.io/s/lucid-butterfly-y66tc?file=/src/App.js
export default function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
console.log('me socond')
});
console.log('me first')
const prevCount = prevCountRef.current;
return (
<h1>
Now: {count}, before: {prevCount}
<div onClick={() => setCount((v) => v + 1)}>click me</div>
</h1>
);
}
you see me first then me second in the console

React custom Hook using useRef returns null for the first time the calling component Loads?

I have created a custom hook to scroll the element back into view when the component is scrolled.
export const useComponentIntoView = () => {
const ref = useRef();
const {current} = ref;
if (current) {
window.scrollTo(0, current.offsetTop );
}
return ref;
}
Now i am making use of this in a functional component like
<div ref={useComponentIntoView()}>
So for the first time the current always comes null, i understand that the component is still not mounted so the value is null . but what can we do to get this values always in my custom hook as only for the first navigation the component scroll doesn't work . Is there any work around to this problem .
We need to read the ref from useEffect, when it has already been assigned. To call it only on mount, we pass an empty array of dependencies:
const MyComponent = props => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return <div ref={ref} />;
};
In order to have this functionality out of the component, in its own Hook, we can do it this way:
const useComponentIntoView = () => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return ref;
};
const MyComponent = props => {
const ref = useComponentIntoView();
return <div ref={ref} />;
};
We could also run the useEffect hook after a certain change. In this case we would need to pass to its array of dependencies, a variable that belongs to a state. This variable can belong to the same Component or an ancestor one. For example:
const MyComponent = props => {
const [counter, setCounter] = useState(0);
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, [counter]);
return (
<div ref={ref}>
<button onClick={() => setCounter(counter => counter + 1)}>
Click me
</button>
</div>
);
};
In the above example each time the button is clicked it updates the counter state. This update triggers a new render and, as the counter value changed since the last time useEffect was called, it runs the useEffect callback.
As you mention, ref.current is null until after the component is mounted. This is where you can use useEffect - which will fire after the component is mounted, i.e.:
const useComponentIntoView = () => {
const ref = useRef();
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop );
}
});
return ref;
}

React Hooks - Removing component after use

I'm trying to wrap my head around this. My custom hook is supposed to create a simple popup with the desired input and remove after 3 seconds. Of course, currently, it re-renders every time the counter has reset. How can I make it render only once and then be removed from the dom?
export function createPopup(content, popupType) {
const [message, setMessage] = useState(content)
const [type, setType] = useState(popupType)
const [value, setCounter] = useState(3)
const myTimer = setTimeout(() => {
return setCounter(value - 1)
}, 1000)
useLayoutEffect(() => {
const id = setTimeout(() => {
setCounter(value + -1);
}, 1000);
return () => {
clearTimeout(id);
};
}, [value])
return (
<>
{value !== 0 &&
<PopupMolecule type={type}>
{message}
</PopupMolecule>
}
</>
)
}
I think you want something more like this:
export function createPopup(content, popupType) {
const [visible, setVisible] = useState(true);
useEffect(() => {
setTimeout(() => setVisible(false), 3000);
}, []);
return (
<>
{visible &&
<PopupMolecule type={popupType}>
{content}
</PopupMolecule>
}
</>
)
}
There are still some improvements that need to made here (i.e. fading out on exit or some transition, and the way this is setup you can't use it more than once), but this should fix the problem you stated.
This will show your popup for three seconds on mount, then make your popup invisible and unmount it from the DOM. The empty array in the useEffect hook lets it know to only trigger on mount (and unmount if you return a value). You also don't need the other state variables that you're not updating. Those can just be passed in as function parameters.

Resources