-
Notifications
You must be signed in to change notification settings - Fork 47.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Hooks, useImperativeMethods and multiple refs #14072
Comments
The part that looks shady to me is this: let refs = {};
for (let i = 0; i <= panelsCount; i++) {
refs[i] = createRef();
} That should, I think, happen only once. |
A workaround for lazy init is: let ref = useRef();
if (ref.current == null) {
ref.current = ...;
} (Edited to use Edit from @gaearon: see also #14490 (comment) for a potentially nicer pattern. |
You mean |
Is it possible to use |
@gaearon Sorry, edited. |
So is putting refs inside of state a good idea? This is the only way I managed to put it inside of
|
Hmm sounds complicated. Just refs alone should work. Mind posting this as a code sandbox? |
I created a fork of the sandbox: This does not solve the original problem, that we can't really have an array of refs. |
This doesn't preserve old refs if const refs = React.useMemo(() =>
Array.from(
{ length: panelsCount },
() => React.createRef()
),
[panelsCount]
) |
There are really good reasons why hooks can't be used in loops and this seems to try to be clever and workaround it but will inevitably only hit the same problems. The solution is to create a separate component for each items so that each item can have separate state (with keys, not index!). This leaves the problem that you might require two passes to figure out which the currentIndex is. That's what the "Call/Return" thing is supposed to solve but it doesn't exist yet so the best you can do is the two passes. |
How do you envision writing this without lifting state to the parent component? Activating one of the children implies de-activating its siblings. Although the side-effect of scrolling to the child can be done by the child themselves ( |
I guess there is currently no true idiomatic way of dealing with sets of refs. The issue is that the set itself grows or shrinks between updates but the true value changes in the commit phase as a side-effect. I think for most cases, you just want to change the set as a side-effect too. You can do that simply by mutating a persistent Map. If you're dealing with raw children and want a ref to each item in the list, you should use the React.Children helpers to create a list where each item has a unique key. Then you can attach a ref to that which stores the result in a Map based on that key. let refs = useRef(new Map()).current;
let childrenWithKeys = React.Children.toArray(children);
return childrenWithKeys.map(child =>
React.cloneElement(child, {ref: inst => inst === null ? refs.delete(child.key) : refs.set(child.key, inst)})
); If it is a custom data structure like the example above then you can do the same thing but that data structure needs a key as part of it: const panels = [
{key: 'a', name: 'Panel 1' },
{key: 'b', name: 'Panel 2' },
{key: 'c', name: 'Panel 3' },
];
let refs = useRef(new Map()).current;
return panels.map(panel =>
<AccordionPanel
ref={inst => inst === null ? refs.delete(panel.key) : refs.set(panel.key, inst)}
key={panel.key}
label={panel.name}
isOpen={currentKey === panel.key}
onClick={() => onClick(panel.key)}
/>
); |
Regarding lazily initializing |
@gaearon I can't uderstand why I have to use export const SliderWithTabs = ({
sliderItems,
...
}) => {
const slidesCollection = useRef(null);
.......
<Tabs
items={sliderItems}
ref={slidesCollection}
/>
}; And I return array of refs from children components export const Tabs = forwardRef(
({ items }, ref) => {
const tabRef = items.map(() => useRef(null));
useImperativeHandle(ref, () => tabRef.map(({ current }) => current));
return (
<Fragment>
<TabsList>
{items.map(({ key, title }, index) => (
<TabsItem
ref={tabRef[index]}
key={key}
>
{title}
</TabsItem>
))}
</TabsList>
</Fragment>
);
}
); Example above works for me! But I can't understand why I can not use forward ref without useImperativeHandle. Next Example doesn't work: export const SliderWithTabs = ({
sliderItems,
...
}) => {
const slidesCollection = sliderItems.map(() => useRef(null));
.......
<Tabs
items={sliderItems}
ref={slidesCollection}
/>
}; And I return array of refs from children components export const Tabs = forwardRef(
({ items }, ref) => {
return (
<Fragment>
<TabsList>
{items.map(({ key, title }, index) => (
<TabsItem
ref={ref[index]}
key={key}
>
{title}
</TabsItem>
))}
</TabsList>
</Fragment>
);
}
); When children component mount I can get my array of refs and this will be |
I am not sure where to post this question, feel free to delete it if it doesn't belong here.
Here is a small Accordion with AccordionPanels. It uses hooks to manage state and when some panel is opened we need to scroll to it, so we need a ref.
My question is: Is this how hooks are supposed to be used? Do you see some problems with this approach?
App.js
Accordion.js
The text was updated successfully, but these errors were encountered: