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.
Related
I am trying to create ref in functional component .But getting different output as compare to to class based component.
here is simple class based component .In class based component I am getting correct refs
here is class based component
https://stackblitz.com/edit/react-vh86ou?file=src%2Ftabs.js
see ref mapping (correct ref mapping)
Same mapping I am trying to do with functional component but getting different output why ?
here is my function component
https://stackblitz.com/edit/react-nagea2?file=src%2FApp.js
export default function App() {
useEffect(() => {
console.log('---', tabRefs);
}, []);
const getTabProps = ({ title, key, selected, tabIndex }) => ({
selected,
children: title,
key: tabPrefix + key,
id: tabPrefix + key,
ref: e => (tabRefs[tabPrefix + key] = e),
originalKey: key
});
I am getting this output
Why I am getting Ref from HTMLLIELEMENT instead of TAB component ?
Update :
I am trying to get offsetWidth still not able to get
https://stackblitz.com/edit/react-nagea2?file=src%2FApp.js
useEffect(() => {
console.log('---', tabRefs);
Object.keys(tabRefs).forEach(key => {
console.log(key);
if (tabRefs[key]) {
const width = tabRefs[key].tab.offsetWidth;
console.log(width);
}
});
}, []);
As explained by #TabW, function components do not have refs. But you don't need a ref to the Tab component itself. You can get the offsetWidth of the underlying li element just by forwarding the ref, without having to use useImperativeHandle.
Attach your forwarded ref directly on the li:
const Tab = React.forwardRef(({ children }, ref) => {
return <li ref={ref}>{children}</li>;
});
And remove the .tab property from where you access it.
You can't add the ref attribute to functional component for reasons as mentioned here.
You can use forwardRef function combined with useImperativeHandle hook to emulate class component ref, but you can't add ref to your Tab component as long as Tab is a function component.
For example, you can add some code to your Tab component like below.
const Tab = React.forwardRef(({ children }, ref) => {
const liRef = useRef(null);
React.useImperativeHandle(ref, () => ({
tab: liRef.current
}));
return <li ref={liRef}>{children}</li>;
});
export default Tab;
If you want to store all refs of all Tab component and call some functions of it later, you can add functions inside useImperativeHandle hook.
const Parent = () => {
const [thing, setThing] = useState('a string');
// code to update thing
return <Child thing={thing} />
}
const Child = props => {
return <div>I want {props.thing} to be initial value without updating</div>
}
If I want 'thing' to be passed from parent to child but not update when parent changes it, how do I accomplish this?
I've tried useEffect, tried cloning 'thing' to a constant within Child...
I would use useEffect with the empty [] for dependencies, so that it only runs once.
From Reactjs.org:
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
const Child = props => {
let thing = props.thing;
let [initialValue, setInitialValue] = useState("");
useEffect(() => {
setInitialValue(thing);
}, []);
return (
<div>
I want <strong>{initialValue}</strong> to be initial value without
updating
</div>
);
};
CodeSandbox
Maybe you can try setting the thing to a variable in the child component when it's null. So when the parent update, you don't have to update the child.
let childThing = null;
const Child = props => {
if(childThing === null)
childThing = props.thing
return <div>I want {childThing} to be initial value without updating</div>
}
I am using Hooks + Render Props in my Preact/React application. Instead of passing simple state to the render(), I am passing a component function that can be used by the user of this component.
Also, my component is Hybrid in a sense that that SortDropdown is not UI-Less like we typically have with Render Prop components. It provides some basic UI in the form of anchor element which triggers the actual drop down and shows the custom content via Render Props render function. This is how I have implemented it:
export function SortDropdown(props: SortDropdownProps) {
const { render, onSelect, value } = props;
const anchor = (ref: Ref<Element>, open: VoidFunction) => (
<SortButton buttonElm={ref} onClick={open} />
);
const surfaceContent = (ref: Ref<any>, select: SortSelectFn) => {
// NESTED INNER COMPONENT - IS IT RIGHT?
function SortMenuItem(props: SortMenuItemProps) {
const { children, field } = props;
// THE HOOK IN QUESTION
const [someState, setSomeState] = useState(false);
const isSelected = value.key === field.key;
const onClick = () => select(field);
return (
<SortMenuButton canTrigger={someState} selected={someState} onClick={onClick}>
{children}
</SortMenuButton>
);
}
return (
<div ref={ref}>{render(SortMenuItem, select)}</div>
);
};
return (
<BaseDropdown anchor={anchor} surface={surfaceContent} onSelect={onSelect as any} />
);
}
Here I have two questions. First, does defining an inner nested component, here - SortMenuItem which is then passed to the render function violates the rules of Hooks? The component may be called n-times or may not be used at all by the calling component.
Second, is it a right practice to define a nested Higher-Order Component, here - SortMenuItem being an abstraction over SortMenuButton?
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...
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.