I'm trying to write a component that injects an ID into it's child component. Something like:
<Id id='someId'>
<div>Hello!</div>
</Id>
With an expected output of: <div id='someId'>Hello!</div>
I have it working whenever I use standard HTML components like <div />, <span />, etc. I can't, however, get it to work whenever I use a custom component.
const Foo = ({ children }) => (
<span>{children}</span>
)
<Id id='someId'><Foo>Child of Foo</Foo></Id>
The above will not work and I'm not sure why. Here's what the <Id /> component looks like:
import * as React from 'react'
export const Id = ({ children, id }) => (
React.cloneElement(children, { id })
)
export default Id
UPDATE: Answer is I can't do it.
When you are doing React.cloneElement(children, { id });, you are actually creating a react component and pass props into it. In this case { id } is the same as { id: id }, which mean you are passing the value of the variable id as a props named id to Foo. You can access the value in Foo from this.props.id.
You should retrieve the id passed as props in Foo, then pass it as to the span.
const Foo = ({ children, id }) => (
<span id={id}>{children}</span>
);
About what you asked in the comment, you can achieve it by telling Foo to pass all props, no matter what it is. Something like:
const Foo = props => {
return <span {...props}>{props.children}</span>;
};
Check this updated codesandbox.
I don't think there is any other way for you to achieve it if you don't want the component to be aware that it needs to handle id. AFAIK, most npm packages that works with HOC need to be aware about what are the names of the props that need to be handled.
Related
I have this following ESLint warning :
React Hook "useBuilderFeatureFlagContext" is called in function "Slide.RenderBuilder" that is neither a React function component nor a custom React Hook function.
and this is the following component :
Slide.RenderBuilder = ({ children }) => {
const flags = useBuilderFeatureFlagContext();
return (
<>
<SlideWrapper $flags={flags}>
{children}
</SlideWrapper>
<ImageSetter attributeName="containerBackgroundImage" />
</>
);
};
How do I write a rule that can whitelist this specific case ?
If you can, define the component first, then add it to your object.
const RenderBuilder = ({ children }) => {
const flags = useBuilderFeatureFlagContext();
return (/**/);
};
Slide.RenderBuilder = RenderBuilder;
That way, the rule properly check for hooks, and you have the structure you're looking for.
Make sure to use it as a component <Slide.RenderBuilder /> otherwise you might end up breaking rules of hooks.
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.
In following scenario I have a very simple child component (arrow component) that acts like a counter. The counter can increment a simple value. What if the parent wants to set that counter to x? The obvious answer would be just have a parameter that you set to x. But as my variable in the parent component is a hook, it would be read only. Would you then have to add a parameter for both the getter and setter variable on the child? And then the child would work on those two variables? Or what is best practice here? Or is there some way to transfer the complete hook to the child? It is really not beautiful that you must add two parameters to the child in my world.
const App = props => {
const resetChildCounter = () => {
alert("what to do here");
};
return (
<div className="App">
<Child />
<button onClick={resetChildCounter}>
Reset child counter from parent
</button>
<button onClick={resetChildCounter}>Set child counter to 5</button>
</div>
);
};
const Child = props => {
const [counter, setCounter] = useState(0);
return (
<div>
<span>Counter is: {counter}</span>
<button
onClick={() => {
setCounter(counter + 1);
}}
>
Increment
</button>
</div>
);
};
The Child component acts like a "hybrid" component, it acts as a controlled component and an uncontrolled component.
A simple implementation of a hybrid component looks like this:
const NOOP = () => {}
const Child = ({ initial = 0, onChange = NOOP }) => {
const [counter, setCounter] = useState(initial);
useEffect(() => {
setCounter(initial);
}, [initial]);
useEffect(() => {
onChange(counter);
}, [onChange, counter]);
return (
<div>
<span>Counter is: {counter}</span>
<button onClick={() => setCounter(p => p + 1)}>Increment</button>
</div>
);
};
export default Child;
In this case, as stated by the OP:
I would like to make a self-maintained child component with much more stuff than a simple button.
You get a stand-alone component with a controlled feature.
You're correct.
If you need to set it from the parent, you need to store it as a state of the parent, not the child. And if you need to use it on the child, you need to pass it down to the child. Finally, if you need to set it from the child as well, you don't do it directly, but through a setter prop passed down from the parent.
It is really not beautiful that you must add two parameters to the child in my world.
If you have a state in either parent or child, and then if it's possible for you to mutate it from both sides, that is more ugly. It will break the philosophy of React.
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.
Say I have
const BaseComponent = (props) => {
const classNames = ['base-component', props.className];
return (
<div className={classNames.join(' ')}>
{props.children}
</div>
)
};
const SomeComponent = () => {
return (
<BaseComponent
className="foo-bar"
>
Hello
</BaseComponent>
);
}
The rendered dom here would be <div class="base-component foo-bar">Hello</div>
Now, if I shallow mount SomeComponent and test the classes, only foo-bar is available:
const dom = shallow(<SomeComponent/>);
console.log(dom.hassClass('base-component')); // comes out as false
I understand that only foo-bar was passed as the class to SomeComponent but how do I validate all other classes here too?
What if you use the .prop API.
expect(dom.find('div').prop('className'))
.to.be.equal('base-component foo-bar');
shallow() does only render the top level component which does not include your other class. You can use dive() to let it render the one component one level deeper. dive() renders the only one non-DOM child of the wrapper it was called on.
const dom = shallow(<SomeComponent/>);
console.log(dom.dive().hasClass('base-component')); // now comes out as true
If you want inspect the whole DOM you will have to use render() instead of shallow().
Also not that a call to html() on a ShallowWrapper will always return the markup of a full render no matter if it was a shallow render or not.