React returns older state value onClick - javascript

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.

Related

React, updating array to a new array doesn't re-render the dom with the new array

so i'm looping over an array, and every element has a click event :
{optionsCategory.map((c,i) => (
<div className="content" key={i} onClick={(e) => handleCategoryClick(e,c)}>
{c.name}
</div>
))}
upon clicking if the element has a subCategory i want to assing the subCategory array to the optionsCategory
const handleCategoryClick = async (e: React.MouseEvent<HTMLHeadingElement>, c: Category) => {
if(c.subCategories.length > 0) {
setOptionsService([...c.subCategories])
console.log([optionsCategory])
}
else{
setIsCategory(true);
const data = await onActsByServiceIdAndCategoryId(serviceId,c.id);
setActs([...data])
console.log(acts);
}}
in the console the optionsCategory is updated but not in the Dom
You need to put your optionsCategories inside a useState hook, then react will update the dom, for example: https://jsfiddle.net/7u9cqtxd/
In this case, every two seconds i'll push a new value to optionsCategory and react will update the dom.
const [optionsCategory, setCategory] = useState([30, 20, 10]);
setTimeout(() => setCategory([ ...optionsCategory, 50]), 2000);
return (
<div>
{optionsCategory.map((category) =>
<li key={category}>
{category}
</li>
)}
</div>
);
My bad, after looking at it i used setOptionsService instead of setOptionCategory

Rendering Child Component from store

I have a component which has child components, i want to render these child components with different Ids. They are getting their data from store.The problem is they are rendered but with the same item. how can this be solved?
MultiImages Component
const MultiImages: () => JSX.Element = () => {
const values = ['500', '406', '614'];
return (
<div>
{values.map((val, index) => {
return <OneImage key={index} projectID={val} />;
})}
</div>
);
};
export default MultiImages;
OneImage Component
const OneImage: () => JSX.Element = ({ projectID }) => {
const projectData = useProjectDataStore();
const { getProject } = useAction();
useEffect(() => {
getProject(projectID ?? '');
}, []);
return (
<>
<div>
<img
src={projectData.picture}
}
/>
<div>
<a>
{projectData.projectName}
</a>
</div>
</div>
</>
);
};
export default OneImage;
Your issue here - you are calling in a loop, one by one fetch your projects, and each call, as far as we can understand from your example and comments override each other.
Your are doing it implicitly, cause your fetching functionality is inside your Item Component OneImage
In general, the way you are using global state and trying to isolate one from another nodes is nice, you need to think about your selector hook.
I suggest you, to prevent rewriting too many parts of the code, to change a bit your selector "useProjectDataStore" and make it depended on "projectID".
Each load of next project with getProject might store into your global state result, but instead of overriding ALL the state object, you might want to use Map(Dictionary) as a data structure, and write a result there and use projectID as a key.
So, in your code the only place what might be change is OneImage component
const OneImage: () => JSX.Element = ({ projectID }) => {
// making your hook depended on **projectID**
const projectData = useProjectDataStore(projectID);
const { getProject } = useAction();
useEffect(() => {
// No need of usage **projectID** cause it will inherit if from useProjectDataStore
getProject();
}, []);
return (
<>
<div>
<img
src={projectData.picture}
}
/>
<div>
<a>
{projectData.projectName}
</a>
</div>
</div>
</>
);
};
export default OneImage;
And inside of your useProjectDataStore store result into a specific key using projectID.
Your component OneImage will return what's in the return statement, in your case:
<>
<div>
<img
src={projectData.picture}
/>
<div>
<a>
{projectData.projectName}
</a>
</div>
</div>
</>
This tag <></> around your element is a React.fragment and has no key. This is the reason you get this error.
Since you already have a div tag wrapping your element you can do this:
<div key={parseInt(projectID)}>
<img
src={projectData.picture}
/>
<div>
<a>
{projectData.projectName}
</a>
</div>
</div>
You can also change the key to Math.floor(Math.random() * 9999).
Note that passing the prop key={index} is unnecessary, and is not advised to use index as keys in a react list.

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.

What is the React way of inserting an icon into another component?

I'm trying to create an WithIcon wrapper component which would insert a child (icon) into a wrapped component.
Let's say I have a button:
<Button>Add item</Button>
I want to create a component WithIcon which will be used like this:
<WithIcon i="plus"><Button>Add item</Button></WithIcon>
Ultimately what I want to achieve is this:
<Button className="with-icon"><i className="me-2 bi bi-{icon}"></i>Add item</Button>
Notice the added className and the tag within the Button's body.
I'm trying to figure out how the WithIcon component's code should look like. What is the React way of achieving this result?
The hardest part was the rules of using the WithIcon Will we only have one ?
Will we have only it at the leftmost ? Something like that.
But if we follow your example. We can relatively write something like this for the WithIcon
const WithIcon = ({ i, children }) => {
return React.Children.map(children, (child) => {
return (
<>
<i className={`me-2 bi bi-${i}`}></i>
{React.cloneElement(child, { className: "with-icon" })}
</>
);
});
};
Then we can just use it the way you want it
<WithIcon i="plus"><Button>Add item</Button></WithIcon>
What we do is just looping through the children which in react is any nested jsx you throw in it (Button in our case)
You can find my fiddle here : https://codesandbox.io/s/react-font-awesome-forked-321tz?file=/src/index.js
UPDATE
So my previous answer does not fully meet the end result we want. The will need to be the main parent
The idea is still quite the same as before but here we are infering the type of the component we passed inside the WithIcon This also adds a safeguard when we passed a nested component inside the WithIcon
const WithIcon = ({ i, children }) => {
return React.Children.map(children, (child) => {
const MyType = child.type; // So we can get the Button
return (
<MyType className="with-icon">
<i className={`me-2 bi bi-${i}`}></i>
{(React.cloneElement(child, {}), [child.props.children])}
</MyType>
);
});
};
I think I'll go to sleep I'll update the rest of the explanation at later date.
See the fiddle here :
https://codesandbox.io/s/react-font-awesome-forked-y43fx?file=/src/components/WithIcon.js
Note that this code does not preserved the other props of the passed component, but you can relatively add that by adding {...child.props} at the MyComponent which is just (reflection like?) of infering the component.
Of course also have another option like HOC Enhancers to do this but that adds a bit of complexity to your how to declare your component api. So Pick whats best for ya buddy
Maybe try using a higher order component?
const withIcon = (icon, Component) => ({children, ...props}) => {
return (
<Component className="with-icon" {...props}>
<i className=`me-2 bi bi-${icon}` />
{children}
</Component>
);
}
Then the usage is
const ButtonWithIcon = withIcon("your-icon", Button);
<ButtonWithIcon>Add Item</ButtonWithIcon>
From my experience with react it usually comes down to either using a property inside the component like here (https://material-ui.com/api/button/) or higher order component like what I described.
There are two common patterns used in React for achieving this kind of composition:
Higher-Order Components
Start by defining a component for your button:
const Button = ({ className, children }) => (
<button className={className}>{children}</button>
);
Then the higher-order component can be implemented like this:
const withIcon = (Component) => ({ i, className = '', children, ...props }) => (
<Component {...props} className={`${className} with-icon`}>
<i className={`me-2 bi bi-${i}`} />
{children}
</Component>
);
Usage:
const ButtonWithIcon = withIcon(Button);
<ButtonWithIcon i="plus">Add Item</ButtonWithIcon>
Context
Start by defining the context provider for the icon:
import { createContext } from 'react';
const Icon = createContext('');
const IconProvider = ({ i, children }) => (
<Icon.Provider value={i}>{children}</Icon.Provider>
);
and then your component:
import { useContext } from 'react';
const Button = ({ className = '', children }) => {
const i = useContext(Icon);
if (i) {
className += ' with-icon';
children = (
<>
<i className={`me-2 bi bi-${i}`} />
{children}
</>
);
}
return (
<button className={className}>{children}</button>
);
};
Usage:
<IconProvider i="plus"><Button>Add Item</Button></IconProvider>

How to re-render React components without actually changing state

In my React application I have a component called Value, which has several instances on multiple levels of the DOM tree. Its value can be shown or hidden, and by clicking on it, it shows up or gets hidden (like flipping a card).
I would like to make 2 buttons, "Show all" and "Hide all", which would make all these instances of the Value component to show up or get hidden. I created these buttons in a component (called Cases) which is a parent of each of the instances of the Value component. It has a state called mode, and clicking the buttons sets it to "showAll" or "hideAll". I use React Context to provide this chosen mode to the Value component.
My problem: after I click the "Hide All" button and then make some Value instances visible by clicking on them, I'm not able to hide all of them again. I guess it is because the Value components won't re-render, because even though the setMode("hideAll") function is called, it doesn't actually change the value of the state.
Is there a way I can make the Value instances re-render after calling the setMode function, even though no actual change was made?
I'm relatively new to React and web-development, I'm not sure if it is the right approach, so I'd also be happy to get some advices about what a better solution would be.
Here are the code for my components:
const ModeContext = React.createContext()
export default function Cases() {
const [mode, setMode] = useState("hideAll")
return (
<>
<div>
<button onClick={() => setMode("showAll")}>Show all answers</button>
<button onClick={() => setMode("hideAll")}>Hide all answers</button>
</div>
<ModeContext.Provider value={mode}>
<div>
{cases.map( item => <Case key={item.name} {...item}/> ) }
</div>
</ModeContext.Provider>
</>
)
}
export default function Value(props) {
const mode = useContext(ModeContext)
const [hidden, setHidden] = useState(mode === "showAll" ? false : true)
useEffect(() => {
if (mode === "showAll") setHidden(false)
else if (mode === "hideAll") setHidden(true)
}, [mode])
return (
hidden
? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span>
: <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
)
}
You first need to create your context before you can use it as a provider or user.
So make sure to add this to the top of the file.
const ModeContext = React.createContext('hideAll')
As it stands, since ModeContext isn't created, mode in your Value component should be undefined and never change.
If your components are on separate files, make sure to also export ModeContext and import it in the other component.
Example
Here's one way to organize everything and keep it simple.
// cases.js
const ModeContext = React.createContext('hideAll')
export default function Cases() {
const [mode, setMode] = useState("hideAll")
return (
<>
<div>
<button onClick={() => setMode("showAll")}>Show all answers</button>
<button onClick={() => setMode("hideAll")}>Hide all answers</button>
</div>
<ModeContext.Provider value={mode}>
<div>
{cases.map( item => <Case key={item.name} {...item}/> ) }
</div>
</ModeContext.Provider>
</>
)
}
export function useModeContext() {
return useContext(ModeContext)
}
// value.js
import { useModeContext } from './cases.js'
export default function Value(props) {
const mode = useContext(ModeContext)
const [hidden, setHidden] = useState(mode === "showAll" ? false : true)
useEffect(() => {
if (mode === "showAll") setHidden(false)
else if (mode === "hideAll") setHidden(true)
}, [mode])
return (
hidden
? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span>
: <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
)
}
P.S. I've made this mistake many times, too.
You shouldn't use a new state in the Value component. Your components should have an [only single of truth][1], in your case is mode. In your context, you should provide also a function to hide the components, you can call setHidden
Change the Value component like the following:
export default function Value(props) {
const { mode, setHidden } = useContext(ModeContext)
if(mode === "showAll") {
return <span className="hiddenValue" onClick={() => setHidden("hideAll")}></span>
} else if(mode === "hideAll") {
return <span className="value" onClick={() => setHidden("showAll")}>{props.children}</span>
} else {
return null;
}
)
}
P.S. Because mode seems a boolean value, you can switch between true and false.
[1]: https://reactjs.org/docs/lifting-state-up.html
There are a few ways to handle this scenario.
Move the state in the parent component. Track all visible states in the parent component like this:
const [visible, setVisibilty] = useState(cases.map(() => true))
...
<button onClick={() => setVisibilty(casses.map(() => false)}>Hide all answers</button>
...
{cases.map((item, index) => <Case key={item.name} visible={visible[index]} {...item}/> ) }
Reset the mode after it reset all states:
const [mode, setMode] = useState("hideAll")
useEffect(() => {
setMode("")
}, [mode])

Categories

Resources