Component's forwardRef children all point to the same ref in an array - javascript

I have a function component that takes a variable amount of child (forwardRef) function components. What I would like to achieve is having a ref to each of the child components for animations when a child component is clicked. I have a semi-working solution by creating an array of refs and then cloning all the children and passing an indexed ref to each of them. The only issue is that all of the refs in the index point to the same (last) child.
Here are my components:
const Navbar = ({children}) => {
const [activeLink, setActiveLink] = useState(0);
const linkRefs = Array(children.length).fill(React.createRef(null));
const handleActiveLinkChange = (index) => {
setActiveLink(index);
console.log(linkRefs[index].current);
}
return (
<nav>
{React.Children.map(children, (child, index) => React.cloneElement(child, {index: index, active: index === activeLink, handleActiveLinkChange, key: "child" + index, ref: linkRefs[index]}))}
</nav>
)
}
const Link = React.forwardRef(({children, active, index, handleActiveLinkChange}, ref) => {
return (
<a href="#" style={linkStyle} onClick={() => handleActiveLinkChange(index)} ref={ref}>
{active ? <b>{children}</b> : children}
</a>
)
});
And assuming I use the components in the following way:
<Navbar>
<Link>One</Link>
<Link>Two</Link>
<Link>Three</Link>
<Link>Four</Link>
<Link>Five</Link>
</Navbar>
I expect the refs to be:
Ref array index
Ref current
0
One
1
Two
2
Three
3
Four
4
Five
But the refs I get are:
Ref array index
Ref current
0
Five
1
Five
2
Five
3
Five
4
Five
I'm assuming it's something to do with variable scope but I just can't figure out the cause of the issue. I've tried many variations of loops and functions but I'd rather understand the cause than blindly try to find a solution.

The issue is with the following line. It creates only one ref and all the array indices refer to that single ref.
const linkRefs = Array(children.length).fill(React.createRef(null));
Instead of the above use the following line which creates new refs for each child as you expect.
const linkRefs = Array.from({ length: children.length }, () =>
React.createRef(null)
);

Related

Output each child with a wrapper from React.Children array

I have this reusable component.
export const ListItems = ({ controls, children }) => {
const content = controls ? <PrivateComponent>{ children }</PrivateComponent> : children;
return <ul>{ content }</ul>
}
Seems pretty-straight forward. The idea is that, in PrivateComponent, I wanna wrap each of the children with an extra wrapper, something like this:
export const PrivateComponent = ({ children }) => {
const _children = React.Children.toArray(children);
return (
<div>{ _children.map(child => <SomeWrapper>{ child }</SomeWrapper> ) }</div>
);
}
My question is, is it correct to render child this way, or should I use cloneElement? Also, what should I use for the key of SomeWrapper in the map function?
is it correct to render child this way, or should I use cloneElement?
You can use the child instance directly, there is no problem there. You need cloneElement only if you want to change / add any props or swap out its own children, or if you need a duplicate because you intend to insert "the same child" in two different location in your DOM.
what should I use for the key of SomeWrapper in the map function?
You can use React.Children.map(children, function[(thisArg)]) (doc) which will automatically add keys.

How can you create a Tree display using dynamic data from an API in React?

So, I've got a bit of a doozy here. I've looked into trees and the like in react, and I'm fairly confident I can implement one, given the right data structure. The problem I'm running into is that I'm getting my data from an API that doesn't, at least natively, have the structure for a tree, so I'm trying to create that structure on the fly.
This is what the data I'm getting back from the API looks like:
const category = //could have children
{
"object_name":"B2B",
"data_provider_key":"bluekai",
"object_key": "bluekai-31",
"object_type":"category",
};
const segment = //will only be a child
{
"object_name":"B2B > Role/Title > Admin Exec",
"data_provider_key":"bluekai",
"object_key": "bluekai-1145",
"object_type":"segment",
"cpm_cost":2.500
};
And this is the logic that I'm using to try and manipulate the data from the API to add children/create parents, etc.
const asyncView = async function (segTree: string | undefined) {
const categoryDataCall = api.getBeeswaxSegmentView(categoryBody);
const segmentDataCall = api.getBeeswaxSegmentView(segmentBody);
const data = await Promise.all([categoryDataCall, segmentDataCall]);
const parent = categoryData.find( (el: any) => el.object_key === segTree);
const categories = data[0].payload;
if (categories.length >= 1) {
for (let i = 0; i < categories.length; i++) {
categories[i].children = [];
}
}
parent.children = categories.concat(data[1].payload);
setCategoryData(parent.children);
setParent(parent);
}
asyncView(e.currentTarget.dataset.segment_tree);
}
return (
<>
<div>PARENT: {parent.object_name}</div>
{categoryData.length === 0
? <div>No category data</div>
: categoryData.map((e: any) => {
if (e.object_type === 'segment') {
return (
<div data-segment_tree={`${e.object_key || "NULL"}`}
data-provider_key={`${e.data_provider_key}`}
>
{`Name: ${e.object_name} (${e.object_key}, $${parseFloat(e.cpm_cost).toFixed(2)} CPM)`}
</div>
)
}
return (
<div data-segment_tree={`${e.object_key || "NULL"}`}
data-provider_key={`${e.data_provider_key}`}
onClick={getCategoryAndSegmentData}
>
{`Name: ${e.data_provider_name || e.object_name}`}
</div>
)
})
}
</>
);
}
I haven't implemented the Tree part yet, but that's because I am fairly confident I'm not creating the relations between elements correctly in my logic/the logic breaks if there are multiple 'trees'/categories on a page (which there will be.)
Sorry if this is a bit much, but any help or just ideas on dynamically modifying the data from the API to fit the tree structure of child/parent relationships would be appreciated!
Edit in response to Ray Hatfield:
What's the relationship between a category and a segment?
Segments will always be children of Categories, and will never have children of their own. Categories can have other categories as children.
How do you establish which category a segment belongs to?
The object_key property from the Category object gets passed to the API call(s) (two calls are made: one for segments, and one for categories). This is the only relation between segments and categories - nothing else in the return data ties them together.
What is e?
I assume you mean in the e.currentTarget.dataset.segment_tree line.
e is the event object, which I'm using to create the queries and firing them off on click events. I'm storing the object_key in a data-attribute in the HTML, and then passing it to a handler to generate the categoryBody and segmentBody used in the asyncView() function.
For some reason I have to explicitly pass the e.currentTarget.dataset.segment_tree as an argument to the async function even though they're in the same scope, but all it's doing is allowing me to find the Category that was clicked in the existing array of data in state.
What is categoryData?
categoryData is the array of values ( that is currently in state. So, each time I hit the API I update category data to re-render everything.
Effectively, I'm finding the parent (category that was clicked) firing off the API calls to get all the subcategories/segments associated with the clicked categories object_key, and then adding a children prop to any incoming categories, and then setting the children of the last clicked element equal to the returned segments + categories, and then rendering.
I put together this working demo on jsfiddle. Here are the highlights:
The Core Idea
The core idea is a Category component that's responsible for loading and rendering its own segments and subcategories. The subcategories get rendered using the same Category component, resulting in a recursive tree structure.
The Category Component
const Category = ({item}) => {
const [data, setData] = React.useState();
const onClick = data
? () => setData(null) // discard data (collapse) on subsequent click
: () => load(item.object_key).then(setData);
return (
<div className="category">
<div
className={`category-name ${data ? 'open' : ''}`}
onClick={onClick}
>
{item.object_name}
</div>
{data && (
<ul>
{ data.map((child, i) => (
<li key={i}><Node item={child}/></li>
))}
</ul>
)}
</div>
)
}
This component takes a single item prop representing the category. The component expects item to have object_key and object_name fields, like the category object in your example.
Initially the component has no information other than what's in the item, so it renders the category's name with an onClick handler that makes API calls to fetch the category's children and then stores the result in the component's state:
const [data, setData] = React.useState();
const onClick = () => load(item.object_key).then(setData);
On the subsequent render the Category component renders its children (segments and subcategories) in addition to the category name. Subcategories are rendered using the same Category component, resulting in a recursive tree structure.
The Segment Component
const Segment = ({item: {object_name}}) => (
<div className="segment">{object_name}</div>
);
Simple component for rendering segments. Just returns the segment name here, but you could of course expand it to do whatever you need it to do.
The Node Component
const Node = ({item}) => {
const Cmp = item.object_type === 'category' ? Category : Segment;
return <Cmp item={item} />;
};
Convenience component for rendering a <Segment /> or <Category /> for the given item according to its type.
The rest of the example code is just hand waving to simulate the API calls and generate mock data.
load function
const load = async (parentKey) => {
const [categories, segments] = await Promise.all([
mockApiRequest('category'),
mockApiRequest('segment')
]);
return [
...categories,
...segments
];
}
Given a category's object_key, this makes the api calls to get the segments and subcategories, merges and returns the results as a single array.
mockApiRequest
const mockApiRequest = (type) => (
new Promise((resolve) => {
setTimeout(() => resolve(fakeData(type)), 200);
})
)
Simulates the API request. Waits 200ms before resolving with mock data.
fakeData
// generate mock response data
const fakeData = (type) => {
// copy the list of names
const n = [...names];
// plucks a random name from the list
const getName = () => (
n.splice(Math.floor(Math.random() * n.length), 1)[0]
);
// generate and return an array of data
return Array.from(
{length: Math.floor(Math.random() * 5) + 1},
(_, i) => ({
...samples[type],
object_name: getName()
})
)
};
Generates mock category or segment data by copying the sample and choosing a random name.

how to change state of 1 out of 3 components in react

I have the following layout.
<ParentComponent>
|ComponentA
|ComponentA
|ComponentA
Basically I call the same Component 3 times in the parent component because they are very similar but just have different values. I was told I need to put a button on the parent component to change the values of just one out of the 3 ComponentA. ComponentA is called 3 times to give me the same thing 3 times, if I make a button to set the state of one, it will go to all 3. How can I single out one component?
Thanks!
Make an array of props you want to send to your components, and then change the content of that array on some button/input events, as your application demands it.
For example:
const arrOfProps = [{someProp:'aaa'}, null, null]
const someFunc = (num)=>{
arrOfProps.forEach((x, index)=>{
if (index === num) {x = {someProp:'aaa'}}
else { x = null}
})
}
render(){
return(
<div>
<button onClick={()=>someFunc(0)}>One</button>
<button onClick={()=>someFunc(1)}>Two</button>
<button onClick={()=>someFunc(2)}>Three</button>
<ParentComponent>
<div>{arrOfComponents.map((comp, index)=>{
return <ComponentA key={index} prop={comp}/>
})}</div>
</ParentComponent>
</div>)
}

How to use setState to splice into an array in the state?

My state.events is in array that is made up of the component instance: EventContainer.
I want my setState to place a new EventContainer in the state.events array. However, I want that EventContainer to go in the index immediately after the specific EventContainer that made the setState call.
I'm looking for help with making the adjustments necessary to my approach or, if my entire approach is bad, a recommendation on how to go about this. Thank you very much.
I'm developing an itinerary builder which is made up of rows/EventContainers that represent an activity on a given day.
Each EventContainer has a button that needs to offer the user the ability to onClick an additional row immediately after that EventContainer.
class DayContainer extends React.Component {
constructor(props){
super(props);
this.state = {
events: [],
};
this.pushNewEventContainerToState = this.pushNewEventContainerToState.bind(this);
}
pushNewEventContainerToState (index){
let newEvent = <EventContainer />;
this.setState(prevState => {
const events = prevState.events.map((item, j) => {
if (j === index) {
events: [...prevState.events.splice(index, 0, newEvent)]
}
})
})
}
render(){
return (
<>
<div>
<ul>
{
this.state.events === null
? <EventContainer pushNewEventContainerToState= .
{this.pushNewEventContainerToState} />
: <NewEventButton pushNewEventContainerToState={this.pushNewEventContainerToState} />
}
{this.state.events.map((item, index) => (
<li
key={item}
onClick={() =>
this.pushNewEventContainerToState(index)}
>{item}</li>
))}
</ul>
</div>
</>
)
}
}
My goal in setState was to splice newEvent into this.state.events immediately after the index (the parameter in pushNewEventContainerToState function).
I'm getting this error but I'm guessing there's more going on than just this: Line 23:22: Expected an assignment or function call and instead saw an expression no-unused-expressions.
I can see at least 2 issues with the code.
- Splice will mutate the array in place
- You are not returning the updated state.
You can instead use slice to build the new array.
pushNewEventContainerToState(index) {
let newEvent = < EventContainer / > ;
this.setState(prevState => {
const updatedEvents = [...prevState.events.slice(0, index], newEvent, ...prevState.events.slice(index + 1];
return {
events: updatedEvents
})
})
}
As I'm fairly new to coding, it took me awhile but I was able to compile the full answer. Here is the code, below. Below that, I explain, point by point, what the problem was and how the updated code addresses that.
class DayContainer extends React.Component {
constructor(props){
super(props);
this.state = {
events: [{key:0}],
};
this.pushNewEventContainerToState = this.pushNewEventContainerToState.bind(this);
}
pushNewEventContainerToState(index) {
let newEvent = {key: this.state.events.length};
this.setState(prevState => {
let updatedEvents = [...prevState.events.slice(0, index + 1), newEvent, ...prevState.events.slice(index + 1)];
return {
events: updatedEvents
};
})
}
render(){
return (
<>
<div>
<ul>
{this.state.events.map((item, index) => (
<li key={item.key}>
< EventContainer pushNewEventContainerToState={() => this.pushNewEventContainerToState(index) } / >
</li>
))}
</ul>
</div>
</>
)
}
}
Setup
Starting with state.events, instead of starting with an empty array, I'm starting with one object, including a key starting at 0, because I always want the user to start with one EventContainer.
Regarding pushNewEventContainerToState, #Sushanth made a great recommendation. Please refer directly to that function in my latest code. The refinement I made has to do with the way I separate the EventContainer being passed to this.state.events. I've moved the EventContainer from pushNewEventContainerToState down to the render() element. I've given it a prop of key={item.key} and wrapped the component instance in a li. The very first EventContainer will have a key of 0 (see state.events[0]). Now, each new EventContainer passed to state.events will have a key that's based off the latest .length() of the state.events array (refer to the latest value of the let newEvent variable in pushNewEventContainerToState).
All of that allowed me to fix a big problem I was facing: I needed the newest EventContainer to be placed in the index immediately after the index of the EventContainer calling pushNewEventContainerToState. The main reason this was happening was because I wasn't properly passing the index to the EventContainer inside of render(). Now that I have the actual EventContainer there, I can pass it a prop in the right manner (please refer EventContainer's prop in render). Now I'm calling pushNewEventContainerToState with the correct index.

How can I give a key in JSX the value of a variable depending on conditions

I'm learning React by implementing a front-end interface for the note app API that I created. I have succeeded in having a list of all the note titles in my database appear. I want to be able to click on a title and have the note expand into the text of the note. The easiest way I've found for this is to give the "key" attribute of the 'li' as a variable and to also declare the same variable in the JSX { } object because they have the same name.
I've been looking for an answer for this for a few days and have been unable to find this exact problem. You can put a variable in a normal JSX expression but I need to do it on the 'li' which means technically in the HTML.
Here's some code to understand what I'm saying.
const NoteData = () => {
const [titles, setTitles] = useState([]);
const [open, setOpen] = useState(false);
useEffect(() => {
//AXIOS CALL
setTitles(response.data[0]);
});
}, []);
//^^^^^add the array there to stop the response.data from repeating WAY TOO MANY TIMES
let listTitles = titles.map(titles => (
<li className="noteTitles" key={titles.title}>
{titles.title}
</li>
));
let showText = titles.map(titles => (
<li className="openText" key= {titles.text_entry}>
{titles.text_entry}
</li>
))
let openNote = () => {
setOpen(open => !open);
if (open) {
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{showText}
</ul>
</div>
);
}
if (!open) {
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{listTitles}
</ul>
</div>
);
}
};
return { openNote };
};
export default NoteData;
That is the code I currently have. Here's showing a more simplified version of the openNote function that maybe makes more sense and shows what I'm trying to do:
VariableHere = "";
let openNote = () => {
setOpen(open => !open);
open ? (VariableHere = titles.text_entry) : (VariableHere = titles.title);
};
let listNotes = titles.map(titles => (
<li className="noteTitles" key={VariableHere}>
{VariableHere}
</li>
));
return (
<div>
<ul onClick={openNote}>
{listNotes}
</ul>
</div>
);
On click of each element there should be a switch of the key elements so if the element is 'open' the key variable and given variable in the JSX object should be mapped to titles.text_entry and on '(!open)' the key and JSX should be mapped to titles.title.
first of all, you're using a ternary in a weird way:
open ? (VariableHere = titles.text_entry) : (VariableHere = titles.title);
Ternaries are meant to be expressions whose value is conditional, but you're using it like a shorthand if/else. Try something like
VariableHere = open ? titles.text_entry : titles.title;
which is both shorter and more readable.
Second of all, keys in an array of elements are meant to help React determine which elements to update, if an item represents the same object, its key shouldn't change. In this case, regardless of what you're displaying, an item in the array represents the same note. Always using the title as the key should be fine provided items can't have the same title. If they can, use some sort of unique ID instead. If the order of the items doesn't change throughout the life of the component, using the array index as the key is fine.
Lastly, what you seem to want to do is called "conditional rendering". There are many ways to achieve this in react, one such way is to use the pre-cited ternary operator. Here is a minimal working example:
const listNotes = titles.map(note => (
<li className="noteTitles" key={note.title}>
{open ? note.title : note.text_entry}
</li>
));
const openNote = () => {
setOpen(!open);
}
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{listNotes}
</ul>
</div>
)
You could also use a ternary in the key expression, but as I talked about above, it's not a good idea to do so.
Given your data-structure, I think you can simplify your code a bit. There is no need to create separate arrays for titles and contents. It sounds like you just want to expand and collapse a note when it is selected.
Here is a really simplified version on how you an do this. I'll use a sample data-set since we don't have access to your API.
const NoteData = () => {
const [titles, setTitles] = useState([]);
const [currentNote, setCurrentNote] = useState({});
useEffect(() => {
//AXIOS CALL
// setTitles(response.data[0]);
let data = [
{ id: 1, title: "a", text_entry: "what" },
{ id: 2, title: "b", text_entry: "is" },
{ id: 3, title: "c", text_entry: "up?" }
];
setTitles(data);
}, []);
const handleClick = noteId => {
let selectedTitle = titles.find(title => title.id == noteId);
//"collapse" if already selected
if (noteId === currentNote.id) {
setCurrentNote({});
} else {
setCurrentNote(selectedTitle);
}
};
let listTitles = titles.map(title => (
<li
className="noteTitles"
key={title.title}
onClick={() => handleClick(title.id)}
>
{title.title}
{title.id === currentNote.id && <div>{title.text_entry}</div>}
</li>
));
return (
<div>
Click on link item
<ul>{listTitles}</ul>
</div>
);
};
See working sandbox: https://codesandbox.io/s/old-silence-366ne
The main updates:
You don't need to have an "open" state. To be more succinct and
accurate, you should have a currentNote state instead, which is
set when clicking on a list item.
Have your handleClick function accept a noteId as an argument.
Then use that noteId to find the corresponding note in your titles
state. Set that found note as the currentNote. If the selected
note was already the currentNote, simply set currentNote to an
empty object {}, thus creating our expanding/collapsing effect.
In the JSX, after the title, use a ternary operator to conditionally
display the currentNote. If the note being mapped matches the
currentNote, then you would display a div containing the
text_entry.

Categories