React hooks: get current value from child component with no constant rerender - javascript

There is counter on page. To avoid re-rendering the entire Parent component every second, the counter is placed in a separate Child component.
From time to time there is need to take current time from counter Child (in this example by clicking Button).
I found solution with execution useEffect by passing empty object as dependency.
Even though it works, I don't feel this solution is correct one.
Do you have suggestion how this code could be improved?
Parent component:
const Parent = () => {
const [getChildValue, setGetChildValue] = useState(0);
const [triggerChild, setTriggerChild] = useState(0); // set just to force triggering in Child
const fooJustToTriggerChildAction = () => {
setTriggerChild({}); // set new empty object to force useEffect in child
};
const handleValueFromChild = (timeFromChild) => {
console.log('Current time from child:', timeFromChild);
};
return (
<>
<Child
handleValueFromChild={handleValueFromChild}
triggerChild={triggerChild}
/>
<Button onPress={fooJustToTriggerChildAction} >
Click to take time
</Button>
</>
);
};
Child component
const Child = ({
triggerChild,
handleValueFromChild,
}) => {
const [totalTime, setTotalTime] = useState(0);
const totalTimeRef = useRef(totalTime); // useRef to handle totalTime inside useEffect
const counter = () => {
totalTimeRef.current = totalTimeRef.current + 1;
setTotalTime(totalTimeRef.current);
setTimeout(counter, 1000);
};
useEffect(() => {
counter();
}, []); // Run time counter at first render
useEffect(() => {
const valueForParent = totalTimeRef.current;
handleValueFromChild(valueForParent); // use Parent's function to pass new time
}, [triggerChild]); // Force triggering with empty object
return (
<>
<div>Total time: {totalTime}</div>
</>
);
};

Given your set of requirements, I would do something similar, albeit with one small change.
Instead of passing an empty object (which obviously works, as {} !== {}), I would pass a boolean flag to my child, requesting to pass back the current timer value. As soon as the value is passed, I would then reset the flag to false, pass the value and wait for the next request by the parent.
Parent component:
const Parent = () => {
const [timerNeeded, setTimerNeeded] = useState(false);
const fooJustToTriggerChildAction = () => {
setTimerNeeded(true);
};
const handleValueFromChild = (timeFromChild) => {
console.log('Current time from child:', timeFromChild);
setTimerNeeded(false);
};
return (
<>
<Child
handleValueFromChild={handleValueFromChild}
timerNeeded={timerNeeded}
/>
<Button onPress={fooJustToTriggerChildAction} >
Click to take time
</Button>
</>
);
};
Child component
const Child = ({
timerNeeded,
handleValueFromChild,
}) => {
const [totalTime, setTotalTime] = useState(0);
const totalTimeRef = useRef(totalTime); // useRef to handle totalTime inside useEffect
const counter = () => {
totalTimeRef.current = totalTimeRef.current + 1;
setTotalTime(totalTimeRef.current);
setTimeout(counter, 1000);
};
useEffect(() => {
counter();
}, []); // Run time counter at first render
useEffect(() => {
if (timerNeeded) {
handleValueFromChild(totalTimeRef.current);
}
}, [timerNeeded]);
return (
<>
<div>Total time: {totalTime}</div>
</>
);
};

Related

How to add a keydown Event Listener for a react functional component

I'm trying to 'move' in my 10x10 grid by updating the activeCellId state. However none of the methods I tried works. This is my code.
const GridCells: React.FC = () => {
const gridArray = [...Array(100).keys()];
const color = [
"bg-slate-50",
"bg-slate-100",
"bg-slate-200",
"bg-slate-300",
"bg-slate-400",
"bg-slate-500",
"bg-slate-600",
"bg-slate-700",
"bg-slate-800",
"bg-slate-900",
];
const [activeCellId, setActiveCellId] = useState(42);
// useEffect(() => {
// document.addEventListener("keydown", updateActiveCellId, false);
// }, []); // this doesn't work. the activeCellId is only incremented once, and afterwards the setActiveCellId doesn't get called at all
const updateActiveCellId = (e: React.KeyboardEvent) => {
// will eventually be a switch case logic here, for handling arrow up, left, right down
console.log(activeCellId);
setActiveCellId(activeCellId + 1);
};
return (
<div
className="grid-rows-10 grid grid-cols-10 gap-0.5"
// onKeyDown={updateActiveCellId} this also doesn't work
>
{gridArray.map((value, id) => {
const colorId = Math.floor(id / 10);
return (
<div
key={id}
className={
"h-10 w-10 "
+ color[colorId]
+ (id === activeCellId ? " scale-125 bg-yellow-400" : "")
}
>
{id}
</div>
);
})}
</div>
);
};
I'm trying to update a state in the react component by pressing certain keys. I've tried UseEffect with [] dep array and tried onKeyDown and it also doesn't work. I also tried following this useRef way it doesn't work too.
const innerRef = useRef(null);
useEffect(() => {
const div = innerRef.current;
div.addEventListener("keydown", updateActiveCellId, false);
}, []); // this doesn't work at all
const updateActiveCellId = (e: React.KeyboardEvent) => {
console.log(activeCellId);
setActiveCellId(activeCellId + 1);
};
return (
<div
className="grid-rows-10 grid grid-cols-10 gap-0.5"
ref={innerRef}
>
...
)
Try this:
useEffect(() => {
document.addEventListener("keydown", updateActiveCellId, false);
return () => {
document.removeEventListener("keydown", updateActiveCellId, false);
}
}, [activeCellId]);
The [activeCellId] is the dependency of useEffect. Everytimes activeCellId changes, the function inside useEffect will run.
You had an empty dependency, so it ran on initial component mount only.
The returned function containing removeEventListner is executed when the component unmounts (See cleanup function in the docs). That is to ensure you have only one event listener runnign at once.
Documentation

React function doesn't updated with state

I'm trying to execute a function that is using react state but when state changes the function doesn't updates with the state value.
const {useState, useEffect} = React;
function Example() {
const [count, setCount] = useState(0);
const testFunction = function(){
setInterval(() => {
console.log(count)
}, 3000)
}
useEffect(() => {
let fncs = [testFunction];
fncs.forEach(fnc => fnc.apply(this));
}, [])
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.getElementById('other-div').innerHTML = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
ReactDOM.render( <Example />, document.getElementById('root') );
Exmaple:
https://jsfiddle.net/bzyxqkwt/4/
just so you understand, im passing functions to another component and he execute those functions on some event like this
fncs.forEach(fnc => fnc.apply(this, someEventParams));
I'm just guessing that the setInterval is just for demo purposes and needs to be stopped/removed, but basically useEffect captures the initial state. So if you want to catch inside the inner function updated state, then you should pass all the values that you need (in this case count parameter). Something like:
const testFunction = function(){
// setInterval(() => {
console.log(count); // it should log the updated value
// }, 3000)
}
useEffect(() => {
let fncs = [testFunction];
fncs.forEach(fnc => fnc.apply(this));
}, [count]); // <-- this makes the trick

Wait for change of prop from parent component after changing it from a child in React

I have rewritten a Child class component in React to a functional component. Here is the simplified code example.
For sure, as so often, this is a simplified code and more things are done with the value in the parent component. That's why we have and need it there.
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue) => {
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
}
return (
<Child value={value} onChange={handleChange}/>
);
}
const Child = (props) => {
const {value} = props;
// This solution does not work for me,
// because it's always triggered, when
// `value` changes. I only want to trigger
// `logValueFromProp` after clicking the
// Button.
useEffect(() => {
logValueFromProp();
}, [value]);
const handleClick = () => {
// some calculations to get `newValue`
// are happening here
props.onChange(newValue);
logValueFromProp();
}
const logValueFromProp = () {
console.log(prop.value);
}
return (
<Button onClick={handleClick} />
);
}
What I want to do is to log a properties value, but only if it got changed by clicking the button. So just using a useEffect does not work for me.
Before changing the child component to a functional component, the property had its new value before I was calling logValueFromProp(). Afterwards it doesn't. I guess that's cause of some timing, and I was just lucky that the property was updated before the function was called.
So the question is: How would you solve this situation? One solution I thought of was a state in the child component which I set when the button is clicked and in the useEffect I only call the function when the state is set and then reset the state. But that doesn't feel like the optimal solution to me...
Three possible solutions for you
Pass logValueFromProp the value directly — but in a comment you've explained that the value might be modified slightly by the parent component before being set on the child, which would make this not applicable.
Use a flag in a ref. But if the parent doesn't always change the prop, that would be unreliable.
Have the parent accept a callback in its handleChange.
#1
If possible, I'd pass the value directly to logValueFromProp when you want to log it. That's the simple, direct solution:
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue);
logValueFromProp(newValue);
};
const logValueFromProp = (newValue = prop.value) {
console.log(newValue);
};
return (
<Button onClick={handleClick} />
);
};
But in a comment you've said the new value may not be exactly the same as what you called props.onChange with.
#2
You could use a ref to remember whether you want to log it when the component function is next called (which will presumably be after it changes):
const Child = (props) => {
const {value} = props;
const logValueRef = useRef(false);
if (logValueRef.current) {
logValueFromProp();
logValueRef.current = false;
}
const handleClick = () => {
props.onChange(newValue);
logValueRef.current = true;
};
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
Using a ref instead of a state member means that when you clear the flag, it doesn't cause a re-render. (Your component function is only called after handleClick because the parent changes the value prop.)
Beware that if the parent component doesn't change the value when you call prop.onChange, the ref flag will remain set and then your component will mistakenly log the next changed value even if it isn't from the button. For that reason, it might make sense to try to move the logging to the parent, which knows how it responds to onChange.
#3
Given the issues with both of the above, the most robust solution would seem to be to modify Parent's handleChange so that it calls a callback with the possibly-modified value:
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue, callback) => {
// ^^^^^^^^^^−−−−−−−−−−−−−−−−− ***
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
if (callback) { // ***
callback(newChangedValue); // ***
} // ***
};
return (
<Child value={value} onChange={handleChange}/>
);
};
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue, logValueFromProp);
// ^^^^^^^^^^^^^^^^^^−−−−−−−−−−−−−− ***
}
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
This answer is based upon the answer of T.J. Crowder (#2).
You can create a custom hook that accepts a callback and dependencies. And returns a function that will trigger a re-render (by using useState instead of useContext) calling the callback in the process.
I've enhanced his answer by allowing you to pass a dependency array which will be used to determine if the callback is called. If the dependency array is omitted, the callback is always called. When passed, the callback is only called if there was a change in the dependency array.
I went for the name useTrigger in the example below, but depending on preference you might like another name better. For example useChange.
const { useState, useCallback } = React;
const useTrigger = (function () {
function zip(a1, a2) {
return a1.map((_, i) => [a1[i], a2[i]]);
}
// compares 2 arrays assuming the length is the same
function equals(a1, a2) {
return zip(a1, a2).every(([e1, e2]) => Object.is(e1, e2));
}
return function (callback, deps) {
const [trigger, setTrigger] = useState(null);
if (trigger) {
if (!deps || !equals(deps, trigger.deps)) {
callback(...trigger.args);
}
setTrigger(null);
}
return useCallback((...args) => {
setTrigger({ args, deps });
}, deps);
}
})();
function Parent() {
const [value, setValue] = useState(null);
function handleChange(newValue) {
// Sometimes the value is changed, triggering `logValueFromProp()`.
// Sometimes it isn't.
if (Math.random() < 0.66) newValue++;
setValue(newValue);
}
return <Child value={value} onChange={handleChange} />;
}
function Child({ value, onChange }) {
const logValueFromProp = useTrigger(() => {
console.log(value);
}, [value]);
function handleClick() {
onChange(value || 0);
logValueFromProp();
}
return (
<button onClick={handleClick}>
Click Me!
</button>
);
}
ReactDOM.render(<Parent />, document.querySelector("#demo"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="demo"></div>

How to use setTimeout within a .map() in reactJS

I have an array of players which I map through. The user is constantly adding new players to this array. I map through the array and return a set of elements. I'd like each player from the list to render one at a time with a 500ms interval in between. Here's the code I have so far:
export const ShowDraftedPlayers = props => {
const {
draftedPlayers,
getPlayerProfile,
teams,
draftPos,
myTeam } = props;
let playersDraftedList = draftedPlayers.map((player, index) => {
return (
<div key={index} className='drafted'>
<p style={style}>TEAM {player.teamDraftedBy} </p>
<b className='player-lastName'> {player.displayName} </b>
{player.position}
</p>
</div>
)
})
return (
<div className='draftedPlayers-container'>
{playersDraftedList}
</div>
)
}
You can use an effect to create an interval which updates a counter every 500ms; your render logic can then render a growing slice of the array each time.
export const ShowDraftedPlayers = props => {
const {
draftedPlayers,
getPlayerProfile,
teams,
draftPos,
myTeam
} = props;
const [count, setCount] = useState(0);
useEffect(() => {
let counter = count;
const interval = setInterval(() => {
if (counter >= draftedPlayers.length) {
clearInterval(interval);
} else {
setCount(count => count + 1);
counter++; // local variable that this closure will see
}
}, 500);
return () => clearInterval(interval);
}, [draftedPlayers]);
let playersDraftedList = draftedPlayers.slice(0, count).map((player, index) => {
return (
<div key={index} className='drafted'>
<p style={style}>TEAM {player.teamDraftedBy} </p>
<b className='player-lastName'> {player.displayName} </b>
{player.position}
</p>
</div>
)
})
return (
<div className='draftedPlayers-container'>
{playersDraftedList}
</div>
)
}
You don't. setTimeout is asynchronous, as so is JavaScript and React. Components are rendered as soon as possible so that JavaScript can jump to do something else. The best way is to create a hook that reacts (pun intended) to some change in value, in this case props.draftedPlayers, setTimeout for 500ms, then add that new player to an internal players state and let the component renders based on it.
Here is how the component will reflect a change in props.draftedPlayers with a delay of 500ms:
export const ShowDraftedPlayers = props => {
const {
draftedPlayers,
getPlayerProfile,
teams,
draftPos,
myTeam
} = props;
const [players, setPlayers] = useState([]);
// Set internal `players` state to the updated `draftedPlayers`
// only when `draftedPlayers` changes.
useEffect(() => {
const delay = setTimeout(() => {
setPlayers(draftedPlayers);
}, 500);
return () => clearTimeout(delay);
}, [draftedPlayers]);
let playersDraftedList = players.map((player, index) => {
return (
<div key={index} className='drafted'>
<p style={style}>TEAM {player.teamDraftedBy} </p>
<b className='player-lastName'> {player.displayName} </b>
{player.position}
</p>
</div>
);
};
};
Whenever draftedPlayers prop is changed (players added or removed) from the outer context, the component will update its players array after a delay of 500ms.

React w/hooks: prevent re-rendering component with a function as prop

Let's say I have:
const AddItemButton = React.memo(({ onClick }) => {
// Goal is to make sure this gets printed only once
console.error('Button Rendered!');
return <button onClick={onClick}>Add Item</button>;
});
const App = () => {
const [items, setItems] = useState([]);
const addItem = () => {
setItems(items.concat(Math.random()));
}
return (
<div>
<AddItemButton onClick={addItem} />
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
);
};
Any time I add an item, the <AddItemButton /> gets re-rendered because addItem is a new instance. I tried memoizing addItem:
const addItemMemoized = React.memo(() => addItem, [setItems])
But this is reusing the setItems from the first render, while
const addItemMemoized = React.memo(() => addItem, [items])
Doesn't memoize since items reference changes.
I can'd do
const addItem = () => {
items.push(Math.random());
setItems(items);
}
Since that doesn't change the reference of items and nothing gets updated.
One weird way to do it is:
const [, frobState] = useState();
const addItemMemoized = useMemo(() => () => {
items.push(Math.random());
frobState(Symbol())
}, [items]);
But I'm wondering if there's a better way that doesn't require extra state references.
The current preferred route is useCallback, which is the same as your useMemo solution, but with additional possible optimizations in the future. Pass an empty array [] to make sure the function will always have the same reference for the lifetime of the component.
Here, you also want to use the functional state update form, to make sure the item is always being added based on the current state.
const addItem = useCallback(() => {
setItems(items => [...items, Math.random()]);
}, []);

Categories