array map react component call child function - javascript

How can we call child function, if we are using array map in react? I have found a way on googling by using refs but it does not call actual component rather than the last component it registered.
Index.jsx
setActiveChat = (ID) => {
this.refs.chateditor.activateChat(ID);
}
{
this.state.users.map((user, index) => <ChatEditor ref="chateditor"/>)
}
ChatEditor.jsx
activateChat = (ID) => {
alert("Hi i am here!");
}
Thanks #Mayank Shukla
By inspiring from his solution and to avoid usage of refs according to DOC
I have come up with a solution if anyone wants to use it.
Index.jsx
setActiveChat = (ID) => {
this[`editor${ID}`](ID);
}
{
this.state.users.map((user, index) => <ChatEditor initChat={edtr =>
(this[`editor${user.ID}`] = edtr)} />
}
ChatEditor.jsx
constructor(props) {
super(props);
props.initChat(this.activateChat);
}
activateChat = (ID) => {
alert('Hey, I m here')
}

Because you are assigning the same ref (ref name) to all the Child component, so and the end of the loop, ref will have the reference of last Child component.
Solution is, use the unique name for refs for each child, one way to achieve that is, put the index of element with the ref.
Like this:
this.state.users.map((user, index) => <ChatEditor ref={`chateditor${index}`} />)
Now use:
this.refs[`chateditor${index}`] //replace index with 0,1,2...
To access the specific child element.
Suggestion, As per DOC:
If you worked with React before, you might be familiar with an older
API where the ref attribute is a string, like "textInput", and the DOM
node is accessed as this.refs.textInput. We advise against it because
string refs have some issues, are considered legacy, and are likely to
be removed in one of the future releases. If you’re currently using
this.refs.textInput to access refs, we recommend the callback pattern
instead.
So use ref callback method instead of string refs.
Like this:
this.state.users.map((user, index) =>
<ChatEditor ref={el => (this[`chateditor${index}`] = el)} />)
Now use this to access the Child component:
this[`chateditor${index}`] //replace index with 0,1,2...

Related

React functional components in Array.map are always rerendering when getting passed a function as props

I am trying to render multiple buttons in a parent component that manages all child states centrally. This means that the parent stores e.g. the click state, the disabled state for each button in his own state using useState and passes it as props to the childs. Additionally the onClick function is also defined inside of the parent component and is passed down to each child. At the moment I am doing this like the following:
const [isSelected, setIsSelected] = useState(Array(49).fill(false));
...
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
...
(In the render function:)
return isSelected.map((isFieldSelected, key) => {
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
})
To try to prevent the child component from rerendering I am using...
... useCallback to make react see that the onClick function always stays the same
... React.Fragment to make react find a component again because otherwise a child would not have a unique id or sth similar
The child component is exported as:
export default React.memo(TheChildComponent, compareEquality) with
const compareEquality = (prev, next) => {
console.log(prev, next);
return prev.isSelected === next.isSelected;
}
Somehow the log line in compareEquality is never executed and therefore I know that compareEquality is never executed. I don't know why this is happening either.
I have checked all blogs, previous Stackoverflow questions etc. but could not yet find a way to prevent the child components from being rerendered every time that at least one component executes the onClick function and by doing that updated the isSelected state.
I would be very happy if someone could point me in the right direction or explain where my problem is coming from.
Thanks in advance!
This code will actually generate a new onClick function every render, because useCallback isn't given a deps array:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
});
The following should only create one onClick function and re-use it throughout all renders:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
Combined with vanilla React.memo, this should then prevent the children from re-rendering except when isSelected changes. (Your second argument to React.memo should have also fixed this -- I'm not sure why that didn't work.)
As a side note, you can simplify this code:
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
to the following:
<TheChildComponent key={key}
isSelected={isFieldSelected}
onClick={onClick}
/>
(assuming you indeed only need a single component in the body of the map).
Turns out the only problem was neither useCallback, useMemo or anything similar.
In the render function of the parent component I did not directly use
return isSelected.map(...)
I included that part from a seperate, very simple component like this:
const Fields = () => {
return isSelected.map((isFieldSelected, i) => (
<TheChildComponent
key={i}
isSelected={isFieldSelected}
onClick={onClick}
/>
));
};
That is where my problem was. When moving the code from the seperate component Fields into the return statement of the parent component the rerendering error vanished.
Still, thanks for the help.

Setting multiple child refs in state

I am retrieving DOM nodes of my child components by passing a callback into its ref prop as shown.
Parent component:
setElementRef = (name, element) => {
if (element) {
this.setState({
...this.state,
menuItems: {
...this.state.menuItems,
[name]: prop,
},
});
}
};
render() {
return <Child refCallback={(node) => this.setElementRef("child", node)} />
}
Child component:
render() {
return (
<div ref={this.props.refCallback}/>
}
The information in the nodes such as getBoundingClientRect() are needed. However, I am unable to setState as it exceeds the maximum update depth when multiple child components trigger the callback. Is there a way of storing multiple DOM nodes in the state, or should I avoid setting the state completely and use a class variable instead?
Theoertically said, reference is not state. Therefore you should not use state to store component reference.
In your case, you just need to create object on class to keep your references (Unline setState it won't trigger re-render, and it will be accessible from within your component, or you can pass it as prop)
childRefs = {}
setElementRef = (name, element) => {
this.childRefs.current[name] = element;
}
// ... and use it like this
someOperation = () => {
const { child } = this.childRefs;
if (child) {
const rect = child.getBoundingClientRect();
}
}
Original answer - to be used with functional components and hooks
If you need to work with references, it is recommended to use useRef (It allows you to update it's value without rerendering component) to keep actual reference, or to keep one single object, and just set properties with your reference. Then you can work with those refernces in callbacks or useEffect.

How to test if a component is rendered with the right props when using react-testing-library?

I have some components that are rendering another component (FetchNextPageButton) that is already tested in isolation, like these ones:
const News = () => (
<div>
<h1>News</h1>
...
<FetchNextPageButton query={NEWS_QUERY} path="viewer.news" />
</div>
)
const Jobs = () => (
<div>
<h1>Jobs</h1>
...
<FetchNextPageButton query={JOBS_QUERY} path="viewer.jobs" />
</div>
)
const Posts = () => (
<div>
<h1>Posts</h1>
...
<FetchNextPageButton query={POSTS_QUERY} path="viewer.posts" />
</div>
)
The thing is that I'd not like having to add tests on each of these components for a functionality that is already tested somewhere else, so I think that should be enough just to test that the component is rendered and that I'm passing the right props to it.
I'd have been able to test this easily with Enzyme with something like this:
expect(wrapper.find('FetchNextPageButton').props()).toMatchObject({
query: NEWS_QUERY,
path: "viewer.news"
})
So I'm wondering what's the best approach to test it by using React testing library instead.
This is the approach that Kent C. Dodds (the creator of RTL) shared with me after discussing it with him:
import FetchNextPageButton from 'FetchNextPageButton'
jest.mock('FetchNextPageButton', () => {
return jest.fn(() => null)
})
// ... in your test
expect(FetchNextPageButton).toHaveBeenCalledWith(props, context)
Don't believe it's possible. RTL looks like focusing on validating against DOM not React's components tree.
The only workaround I see is to mock FetchNextPageButton to make it rendering all props into attributes.
jest.mock("../../../FetchNextPageButton.js", () =>
(props) => <div data-test-id="FetchNextPageButton" {...props} />);
....
const { getByTestId } = render(<YourComponent />);
expect(getByTestId("FetchNextPageButton")).toHaveAttribute("query", NEWS_QUERY);
expect(getByTestId("FetchNextPageButton")).toHaveAttribute("path", "viewer.news");
Sure, this is smoothly only for primitive values in props, but validating something like object or function would be harder.
Think, it's not RTL-way, but I agree it would be massive work to check that in scope of each container(and completely ignoring that would be rather a risk).
PS toHaveAttribute is from jest-dom
In my case, I wanted to test that a Higher Order Component (HOC), correctly enhances the component that is passed to the HOC.
What I needed to do, is make the actual component a mock and pass it to the HOC. Like described in the existing answer, you can then just expect the properties, added by the HOC.
// after 'Component' get's passed into withSelectionConstraint, it should have an id prop
const Component = jest.fn(() => <h1>Tag Zam</h1>);
const WithConstraint = withSelectionConstraint(Component, ["instance"], true);
render(<WithConstraint />);
// passing the jest mock to the HOC, enables asserting the actual properties passed by the HOC
expect(Component).toHaveBeenCalledWith(
expect.objectContaining({ ids: mockInstanceRows.map(x => x.id) }),
expect.anything()
)
Based on Ben's answer, I wrote a version which doesn't raise any error :
jest.mock(
'path/to/your/component',
() => {
const MockedComponent = (props: any) => {
const cleanedProps = Object.keys(props).reduce<Record<string, unknown>>(
(a, b) => {
// Needed because html attributes cannot be camel cased
a[b.toLowerCase()] = props[b].toString();
return a;
},
{}
);
return (
<div data-testid="any-test-id" {...cleanedProps} />
);
};
return MockedComponent;
}
);
Note that the attributes values (expect(getByTestId('any-test-id')).toHaveAttribute('attribute','value')) will be stringified.

How to get React children from Enzyme

I've implemented a "slot" system in React from this article: Vue Slots in React. However, I'm running into trouble when trying to test the component due to a "mismatch" between the Enzyme wrapper's children and React's children.
This is the function to get a "slot" child from React children. The function works as expected within a app component when provided with the children prop, but doesn't work during testing as the "children" isn't the same format as React.children.
const getSlot = (children, slot) => {
if (!children) return null;
if (!Array.isArray(children)) {
return children.type === slot ? children : null;
}
// Find the applicable React component representing the target slot
return children.find((child) => child.type === slot);
};
The TestComponent isn't directly used in the tests, but is intended to show an example of how the "slots" would be implemented in a component.
const TestComponent = ({ children }) => {
const slot = getSlot(children, TestComponentSlot);
return (
<div id="parent">
<div id="permanentContent">Permanent Content</div>
{slot && <div id="optionalSlot">{slot}</div>}
</div>
);
};
const TestComponentSlot = () => null;
TestComponent.Slot = TestComponentSlot;
This is the basics of the tests I am trying to write. Essentially, creating a super basic component tree and then checking if the component's children contained the expected "slot" component. However, the getSlot function always returns null as the input isn't the same as the input provided by React children when used within the app.
it("Finds slots in React children", () => {
const wrapper = mount(
<div>
<TestComponent.Slot>Test</TestComponent.Slot>
</div>
);
// Unsure how to properly get the React children to test method.
// Below are some example that don't work...
// None of these approaches returns React children like function expects.
// Some return null and other return Enzyme wrappers.
const children = wrapper.children();
const { children } = wrapper.instance();
const children = wrapper.children().instance();
// TODO: Eventually get something I can put into function
const slot = getSlot(children, TestComponentSlot);
});
Any help or insights would be greatly appreciated!
The problem here is that when you're using enzyme's children() method it returns ShallowWrapper[1]. In order to get the children as a React component you have to get them directly from the props method.
So, derive the children in this way:
const children = wrapper.props().children;
CodeSandbox example.

How to handle in class component passed additional parameters from parent to child event handler

I have a parent class component that should handle child's component onChange event. This is the class method that does that:
editTopic(index, value) {
this.setState(prevState => {
const class = {...prevState.class};
class.topic =
[...class.topic.slice(0, index), value, ...class.topic.slice(index +1)];
const newClass = {...prevState.class, class};
return {class: newClass};
})
}
I am passing this method as a prop to a first child like this:
editTopic={(index, value) => this.editTopic(index, value)}
I am passing further down this prop all the way to a child component where I can get the index. There I am using this function callback like this to give it index and pass it down to select component that is going to give it a value:
<AutocompleteSelect handleChange={() => editTopic(index)}/>
And in AutocompleteSelect component I am using this callback like this in the onChange handler:
onChange={(event) => this.props.handleChange(event.target.value)}
But, this is not working, how should I pass props like this properly, without having AutocompleteSelect component knowing which index should be passed?
Update
I have tried by changing the class method to a currying method like this:
editTema(index) {
return value => {
this.setState(prevState => {
const class = {...prevState.class};
class.topic = [...class.topic.slice(0, index), value, ...class.topic.slice(index +1)];
const newClass = {...prevState.class, class};
return {class: newClass};
})
}
}
Then I am passing it down like this:
editTopic ={(index) => this.editTopic(index)}
And then in the child component that is passing the handler to AutocompleteSelect to this:
handleChange={editTema(index)}
And finally in AutocompleteSelect to this:
onChange={(event) => this.props.handleChange(event.target.value)}
But, it is still not changing the state of the class component, I don't see in the console that the function is even called.
It should be
editTopic={(index) => (value) => this.editTopic(index, value)}
then
<AutocompleteSelect handleChange={editTopic(index)}/>
lastly
onChange={(event) => this.props.handleChange(event.target.value)}
But it is not a good pattern, as you may notice for someone who are not familiar with your code, he probably cannot easily figure out which value should be provided in certain layer. Besides, in future, you will figure out it is quite difficult to switch to react-hooks.
I would suggest passing the parameter instead.
editTopic={(index, value) => this.editTopic(index, value)}
then
<AutocompleteSelect handleChange={editTopic} index={index}/>
lastly
const { handleChange, index } = this.props;
...
onChange={(event) => this.props.handleChange(index, event.target.value)}
When you are passing a Handler down from a Parent Component to Child Component,
you should be passing a reference to the handler and not the function call actually.
So in your case when passing editTopic() you should just be passing
<Component editTopic = {this.editTopic}></Component>

Categories

Resources