Wrap function in useCallback - javascript

In the code snippet below, I'd like to move this function out of jsx and wrap into useCallback.
{suggestedTags.length ? (
<div className={classes.tagSuggestionWrapper}>
{suggestedTags.map((tag) => {
return (<div key={tag}
onClick={() => { selectTag(tag) }}>{tag}</div>
);
})}
</div>
) : null }
Otherwise, a new function is created for every element on every render.
I understand that this may complicate the code, and may not be advisable. But I have to do it. I ask for your advice

You can do:
const selectTag = useCallback((tag) => {
setTags((prevState) => [...prevState, tag]);
setSuggestedTags([]);
setInput("");
}, [])
Little about useCallback
Bare in mind that if you had used any state variable, or prop, you should have included that in the dependency array. An empty dependency array makes sure selectTag reference will stay the same once the component mounts.
And no dependency array is the same as not using useCallback at all
Removing the arrow function
You can remove the arrow function by passing the value by using the onClick event function:
const selectTag = (event) => {
const tag = event.target.name
setTags((prevState) => [...prevState, tag]);
setSuggestedTags([]);
setInput("");
}
return (
{suggestedTags.length ? (
<div className={classes.tagSuggestionWrapper}>
{suggestedTags.map((tag) => {
return (<div key={tag}
name={tag}
className={classes.tagSuggestion}
onClick={selectTag}>{tag}</div>
);
})}
</div>
) : null }
</div>
);

Related

Is there any diference between using ref callback as inline function or as a stable function with useCallback?

I have a component which take a children as props and render it inside a div which has a callback ref.
const ShowMoreText = (props: ShowLessOrMoreProps) => {
const { children } = props;
const [showMore, setShowMore] = useState<boolean>(false);
const [showButton, setShowButton] = useState<boolean>(false);
const [childrenHeight, setChildrenHeight] = useState<number>(9.5);
const measureRef = useCallback((node: HTMLDivElement) => {
if (node) {
setChildrenHeight(node.getBoundingClientRect().height);
}
}, []);
useEffect(() => {
console.log(childrenHeight)// when use measureRef function it gives me 0 when using inline version this gives me correct height and I don't know why
if (childrenHeight > 45) {
setShowButton(true);
}
}, [childrenHeight]);
return (
<Stack
sx={{
alignItems: "flex-start",
}}
>
<Collapse in={showMore} collapsedSize={45}>
<div
// ref={(node: HTMLDivElement) => {
// setChildrenHeight(node?.getBoundingClientRect().height);
// }}
ref={() => measureRef} // if I comment this and use the above commmented version everything works fine
>
{children}
</div>
</Collapse>
{showButton && ( //when using measureRef function this button won't display
<Button onClick={() => setShowMore((prev) => !prev)}>
{showMore ? "Show less" : "Show more"}
</Button>
)}
</Stack>
);
};
the problem is that when I use stable measureRef function the console log inside useeffect prints 0 but in inline ref version everythings works fine. can anyone explain me why ?
ref={(node: HTMLDivElement) => {
// setChildrenHeight(node?.getBoundingClientRect().height);
// }}
You return a function but ref shoulb be an Object (of certain type)
Hence You probably return void function.
if You want to pass (node: HTMLDivElement) => { // setChildrenHeight(node?.getBoundingClientRect().height); // } use unreserved prop then.
props.ref is reserved for useRef() hook .
Finally :
If for some reason Yuu want to save such contruction, do thi that way:
(node: HTMLDivElement) => {
return setChildrenHeight(node?.getBoundingClientRect().height);
}}
Where setChildrenHeight() returns correct object type proper po ref.
I found the solution and share it here in case anyone is curious or have a same problem.
The problem was that the children that I passed to this component was something like this:
<ShowMoreText>
<div>{result?.items[0]?.snippet?.description}</div>
</ShowMoreText>
and because of the delay of request the div height was actually 0 at the beginning. so the actual code for callback ref worked correct and the problem raised because of the api call delay. the solution for me and the mistake that i had been made was not to render the above code conditionally like this:
{dataIsLoading &&
<ShowMoreText>
<div>{result?.items[0]?.snippet?.description}</div>
</ShowMoreText>
}
hopefully this can help someone else.

Avoid infinite loop when updating

I have to update a state of items and see the changes right away. When I'm doing this below, I get an infinite loop.
useEffect(() => {
const playersInFight = gameData.characters.filter((x) =>
fightData.currentPositionInfoDTOS.some((y) => y.entityId == x.id)
);
let orderedCharacters = addPlayers(
playersInFight,
fightData.currentPositionInfoDTOS,
fightData.turnOrder
);
setAllCharacterCards(orderedCharacters);
}, [fightData, gameData, fightInfo, allCharacterCards, allCharacters]);
I have tried also like this:
useEffect(() => {
const playersInFight = gameData.characters.filter((x) =>
fightData.currentPositionInfoDTOS.some((y) => y.entityId == x.id)
);
let orderedCharacters = addPlayers(
playersInFight,
fightData.currentPositionInfoDTOS,
fightData.turnOrder
);
setAllCharacterCards(orderedCharacters);
}, [fightData, gameData, fightInfo, allCharacterCards, allCharacters]);
useEffect(() => {
setAllCharacterCards(allCharacterCards);
}, [allCharacterCards]);
And thanks to that I don't have an infinite loop, but to see the changes I have to refresh the page...
How can I solve this?
Update:
Here is my return:
return (
<section>
<div className="player-fight">
{allCharacterCards ? (
<div>
{allCharacterCards.map((c, idx) => {
return (
<li key={idx} className="player-fight-bottom__player-card">
<CardComponentPlayerCard
id={c.id}
name={c.name}
money={c.money}
health={c.health}
maxHealth={c.maxHealth}
mine={false}
description={c.description}
statuses={c.statuses}
image={c.photoPath}
/>
</li>
);
})}
</div>
) : (
<LoadingSpinner />
)}
</div>
</section>
);
To avoid infinite looping you need to remove allCharacterCards from useEffect dependency array.
useEffect(() => {
const playersInFight = gameData.characters.filter((x) =>
fightData.currentPositionInfoDTOS.some((y) => y.entityId == x.id)
);
let orderedCharacters = addPlayers(
playersInFight,
fightData.currentPositionInfoDTOS,
fightData.turnOrder
);
setAllCharacterCards([...orderedCharacters]);
}, [fightData, gameData, fightInfo, allCharacters]);
Explanation (after question edited)
setAllCharacterCards([...orderedCharacters]);
This should fix your problem.
Since allCharacterCards is an array of objects you need to use spread operator ... to let react know that you are updating state everytime with new values.
Read more about - Deep copy vs shallow copy.

Problems using useRef / useImperativeHandle in mapping components

I have a dashboard with different components. Everything is working with a separate start-button on each component, now I need to have a common start-button, and for accessing the children's subfunctions from a parent, I understand that in React you should use the useRef.(but its perhaps not correct, but I'm struggling to see another way). I would like to have the flexibility to choose which component to start from this "overall start-button"
I have a component list that i map through shown below.
return(
{ComponentsList.map((item) => {
return (
<Showcomponents
{...item}
key={item.name}
/>
)
This works fine, but I would like, as mentioned, to access a function called something like "buttonclick" in each of the children, so I tested this with a pressure-gauge component
The function "exposed" via the forwardRef and the useImparativeHandle
const ShowRadialGauge = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
buttonclick() {
setStart(!start);
},
}));
)
then in my dashboard I changed to :
const gaugepressure = useRef();
return(
<div>
<Button onClick={() => gaugepressure.current.buttonclick()}>
Start processing
</Button>
<ShowRadialGauge ref={gaugepressure} />
<div>
)
This works fine if I use the useRef from the dashboard and instead of mapping over the components, I add them manually.
I understand the useRef is not a props, but its almost what I want. I want to do something like this:
return(
{ComponentsList.map((item) => {
return (
<Showcomponents
{...item}
key={item.name}
**ref={item.ref}**
/>
)
where the ref could be a part of my component array (as below) or a separate array.
export const ComponentsList = [
{
name: "Radial gauge",
text: "showradialgauge",
component: ShowRadialGauge,
ref: "gaugepressure",
},
{
name: "Heatmap",
text: "heatmap",
component: Heatmap,
ref: "heatmapstart",
},
]
Anyone have any suggestions, or perhaps do it another way?
You are on the right track with a React ref in the parent to attach to a single child component. If you are mapping to multiple children though you'll need an array of React refs, one for each mapped child, and in the button handler in the parent you will iterate the array of refs to call the exposed imperative handle from each.
Example:
Parent
// Ref to hold all the component refs
const gaugesRef = React.useRef([]);
// set the ref's current value to be an array of mapped refs
// new refs to be created as needed
gaugesRef.current = componentsList.map(
(_, i) => gaugesRef.current[i] ?? React.createRef()
);
const toggleAll = () => {
// Iterate the array of refs and invoke the exposed handle
gaugesRef.current.forEach((gauge) => gauge.current.toggleStart());
};
return (
<div className="App">
<button type="button" onClick={toggleAll}>
Toggle All Gauges
</button>
{componentsList.map(({ name, component: Component, ...props }, i) => (
<Component
key={name}
ref={gaugesRef.current[i]}
name={name}
{...props}
/>
))}
</div>
);
Child
const ShowRadialGauge = React.forwardRef(({ name }, ref) => {
const [start, setStart] = React.useState(false);
const toggleStart = () => setStart((start) => !start);
React.useImperativeHandle(ref, () => ({
toggleStart
}));
return (....);
});
The more correct/React way to accomplish this however is to lift the state up to the parent component and pass the state and handlers down to these components.
Parent
const [gaugeStarts, setGaugeStarts] = React.useState(
componentsList.map(() => false)
);
const toggleAll = () => {
setGaugeStarts((gaugeStarts) => gaugeStarts.map((start) => !start));
};
const toggleStart = (index) => {
setGaugeStarts((gaugeStarts) =>
gaugeStarts.map((start, i) => (i === index ? !start : start))
);
};
return (
<div className="App">
<button type="button" onClick={toggleAll}>
Toggle All Guages
</button>
{componentsList.map(({ name, component: Component, ...props },, i) => (
<Component
key={name}
start={gaugeStarts[i]}
toggleStart={() => toggleStart(i)}
name={name}
{...props}
/>
))}
</div>
);
Child
const ShowRadialGauge = ({ name, start, toggleStart }) => {
return (
<>
...
<button type="button" onClick={toggleStart}>
Toggle Start
</button>
</>
);
};
#Drew Reese
Thx Drew,
you are off course correct. I'm new to React, and I'm trying to wrap my head around this "state handling".
I tested your suggestion, but as you say, its not very "React'ish", so I lifted the state from the children up to the parent.
In the parent:
const [componentstate, setComponentstate] = useState([
{ id:1, name: "pressuregauge", start: false},
{ id:2, name: "motormap", start: false },
{ id:3, name: "heatmapstart", start: false},
]);
then in the component ShowRadialGauge, I did like this:
const ShowRadialGauge = ({ props, componentstate })
and if we need to keep the button in each component, I have the id in the componentstate object that is desctructured, so I can send that back.
.
First of all, why do you need refs to handle click when you can access it via onClick. The most common use case for refs in React is to reference a DOM element or store value that is persist between renders
My suggestion are these
First, try to make it simple by passing a function and then trigger it via onClick
Second if you really want to learn how to use imperativeHandle you can reference this video https://www.youtube.com/watch?v=zpEyAOkytkU.

How can I render an array of components in react without them unmounting?

I have an array of React components that receive props from a map function, however the issue is that the components are mounted and unmounted on any state update. This is not an issue with array keys.
Please see codesandbox link.
const example = () => {
const components = [
(props: any) => (
<LandingFirstStep
eventImage={eventImage}
safeAreaPadding={safeAreaPadding}
isActive={props.isActive}
onClick={progressToNextIndex}
/>
),
(props: any) => (
<CameraOnboarding
safeAreaPadding={safeAreaPadding}
circleSize={circleSize}
isActive={props.isActive}
onNextClick={progressToNextIndex}
/>
),
];
return (
<div>
{components.map((Comp, index) => {
const isActive = index === currentIndex;
return <Comp key={`component-key-${index}`} isActive={isActive} />;
})}
</div>
)
}
If I render them outside of the component.map like so the follow, the component persists on any state change.
<Comp1 isActive={x === y}
<Comp2 isActive={x === y}
Would love to know what I'm doing wrong here as I am baffled.
Please take a look at this Codesandbox.
I believe I am doing something wrong when declaring the array of functions that return components, as you can see, ComponentOne is re-rendered when the button is pressed, but component two is not.
You should take a look at the key property in React. It helps React to identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity
I think there are two problems:
To get React to reuse them efficiently, you need to add a key property to them:
return (
<div>
{components.map((Comp, index) => {
const isActive = index === currentIndex;
return <Comp key={anAppropriateKeyValue} isActive={isActive} />;
})}
</div>
);
Don't just use index for key unless the order of the list never changes (but it's fine if the list is static, as it appears to be in your question). That might mean you need to change your array to an array of objects with keys and components. From the docs linked above:
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. Check out Robin Pokorny’s article for an in-depth explanation on the negative impacts of using an index as a key. If you choose not to assign an explicit key to list items then React will default to using indexes as keys.
I suspect you're recreating the example array every time. That means that the functions you're creating in the array initializer are recreated each time, which means to React they're not the same component function as the previous render. Instead, make those functions stable. There are a couple of ways to do that, but for instance you can just directly use your LandingFirstStep and CameraOnboarding components in the map callback.
const components = [
{
Comp: LandingFirstStep,
props: {
// Any props for this component other than `isActive`...
onClick: progressToNextIndex
}
},
{
Comp: CameraOnboarding,
props: {
// Any props for this component other than `isActive`...
onNextClick: progressToNextIndex
}
},
];
then in the map:
{components.map(({Comp, props}, index) => {
const isActive = index === currentIndex;
return <Comp key={index} isActive={isActive} {...props} />;
})}
There are other ways to handle it, such as via useMemo or useCallback, but to me this is the simple way — and it gives you a place to put a meaningful key if you need one rather than using index.
Here's an example handling both of those things and showing when the components mount/unmount; as you can see, they no longer unmount/mount when the index changes:
const {useState, useEffect, useCallback} = React;
function LandingFirstStep({isActive, onClick}) {
useEffect(() => {
console.log(`LandingFirstStep mounted`);
return () => {
console.log(`LandingFirstStep unmounted`);
};
}, []);
return <div className={isActive ? "active" : ""} onClick={isActive && onClick}>LoadingFirstStep, isActive = {String(isActive)}</div>;
}
function CameraOnboarding({isActive, onNextClick}) {
useEffect(() => {
console.log(`CameraOnboarding mounted`);
return () => {
console.log(`CameraOnboarding unmounted`);
};
}, []);
return <div className={isActive ? "active" : ""} onClick={isActive && onNextClick}>CameraOnboarding, isActive = {String(isActive)}</div>;
}
const Example = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const progressToNextIndex = useCallback(() => {
setCurrentIndex(i => (i + 1) % components.length);
});
const components = [
{
Comp: LandingFirstStep,
props: {
onClick: progressToNextIndex
}
},
{
Comp: CameraOnboarding,
props: {
onNextClick: progressToNextIndex
}
},
];
return (
<div>
{components.map(({Comp, props}, index) => {
const isActive = index === currentIndex;
return <Comp key={index} isActive={isActive} {...props} />;
})}
</div>
);
};
ReactDOM.render(<Example/>, document.getElementById("root"));
.active {
cursor: pointer;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

React returns older state value onClick

I am adding a component onclick and keeping track of the components using useState Array. However when I go to remove one of the added components, it doesn't recognize the full component Array size, only the state that was there when that component was initially added.
Is there a way to have the current state recognized within that delete function?
https://codesandbox.io/s/twilight-water-jxnup
import React, { useState } from "react";
export default function App() {
const Span = props => {
return (
<div>
<span>{props.index}</span>
<button onClick={() => deleteSpan(props.index)}>DELETE</button>
Length: {spans.length}
</div>
);
};
//set initial span w/ useState
const [spans, setSpans] = useState([<Span key={0} index={Math.random()} />]);
//add new span
const addSpan = () => {
let key = Math.random();
setSpans([...spans, <Span key={key} index={key} />]);
};
//delete span
const deleteSpan = index => {
console.log(spans);
console.log(spans.length);
};
//clear all spans
const clearInputs = () => {
setSpans([]);
};
return (
<>
{spans}
<button onClick={() => addSpan()}>add</button>
<button onClick={() => clearInputs()}>clear</button>
</>
);
}
UPDATE - Explaining why you are facing the issue descibed on your question
When you are adding your new span on your state, it's like it captures an image of the current values around it, including the value of spans. That is why logging spans on click returns you a different value. It's the value spans had when you added your <Span /> into your state.
This is one of the benefits of Closures. Every <Span /> you added, created a different closure, referencing a different version of the spans variable.
Is there a reason why you are pushing a Component into your state? I would suggest you to keep your state plain and clean. In that way, it's also reusable.
You can, for instance, use useState to create an empty array, where you will push data related to your spans. For the sake of the example, I will just push a timestamp, but for you might be something else.
export default function App() {
const Span = props => {
return (
<div>
<span>{props.index}</span>
<button onClick={() => setSpans(spans.filter(span => span !== props.span))}>DELETE</button>
Length: {spans.length}
</div>
);
};
const [spans, setSpans] = React.useState([]);
return (
<>
{spans.length
? spans.map((span, index) => (
<Span key={span} index={index} span={span} />
))
: null}
<button onClick={() => setSpans([
...spans,
new Date().getTime(),
])}>add</button>
<button onClick={() => setSpans([])}>clear</button>
</>
);
}
I hope this helps you find your way.

Categories

Resources