Extend React forwardRef component with methods - javascript

I want to create an extended component with some additional functions added.
Let's say I have an ExtendedButton component which has a button that is forwardRef:ed, but which also has a doubleClick method. I know this is a silly example, but something like this:
const ExtendedButton = forwardRef<HTMLButtonElement, React.HTMLAttributes<HTMLButtonElement>>((props, ref) => {
const btnRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => btnRef?.current as HTMLButtonElement);
const doubleClick = () => {
btnRef.current?.click();
btnRef.current?.click();
};
return <button {...props} ref={btnRef}></button>;
});
I want to be able to get the doubleClick method, as well as all the methods on the button, from a consumer component like this:
export const Consumer = () => {
const ref = useRef<HTMLButtonElement>(null);
ref.current.doubleClick();
ref.current.click();
return <ExtendedButton ref={ref}></ExtendedButton>;
};
I feel I should probably remove the forwardRef so the ref is pointing to ExtendedButton instead of button, but how can I get the button methods then?
Thanks!

useImperativeHandle should expose all the methods you want to access:
type ExtendedButtonType = HTMLButtonElement & { doubleClick: () => void }
const ExtendedButton = forwardRef<ExtendedButtonType, React.HTMLAttributes<HTMLButtonElement>>(
(props, ref) => {
const btnRef = useRef<HTMLButtonElement>(null)
const doubleClick = (): void => {
btnRef.current?.click()
btnRef.current?.click()
}
useImperativeHandle(
ref,
() =>
({
...btnRef.current,
doubleClick,
} as ExtendedButtonType),
)
return <button {...props} ref={btnRef} />
},
)
export const Consumer: FC = () => {
const ref = useRef<ExtendedButtonType>(null)
ref.current?.doubleClick()
ref.current?.click()
return <ExtendedButton ref={ref} />
}

add the method inside the useImperativeHandle
const ExtendedButton = forwardRef<HTMLButtonElement, React.HTMLAttributes<HTMLButtonElement>>((props, ref) => {
const btnRef = useRef<HTMLButtonElement>( );
useImperativeHandle(ref, () => ({
...btnRef.current,
doubleClick: () => {
btnRef.current?.click();
btnRef.current?.click();
};
}));
return <button {...props} ref={btnRef}></button>;
});

Related

What is the different between useImperativeHandle and useRef?

As I understand, useImperativeHandle helps parent component able to call function of its children component. You can see a simple example below
const Parent = () => {
const ref = useRef(null);
const onClick = () => ref.current.focus();
return <>
<button onClick={onClick} />
<FancyInput ref={ref} />
</>
}
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);
but it can be easy achieved by using only useRef
const Parent = () => {
const ref = useRef({});
const onClick = () => ref.current.focus();
return <>
<button onClick={onClick} />
<FancyInput ref={ref} />
</>
}
function FancyInput(props, ref) {
const inputRef = useRef();
useEffect(() => {
ref.current.focus = inputRef.current.focus
}, [])
return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);
So what is the true goal of useImperativeHandle. Can someone give me some advices?. Thank you
Probably something similar to the relationship between useMemo and useCallback where useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Sometimes there is more than one way to accomplish a goal.
I'd say in the case of useImperativeHandle the code can be a bit more succinct/DRY when you need to expose out more than an single property.
Examples:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
property,
anotherProperty,
... etc ...
}), []); // use appropriate dependencies
...
}
vs
function FancyInput(props, ref) {
const inputRef = useRef();
useEffect(() => {
ref.current.focus = inputRef.current.focus;
ref.current.property = property;
ref.current.anotherProperty = anotherProperty;
... etc ...
}, []); // use appropriate dependencies
...
}
Not a big difference, but the useImperativeHandle is less code.
it can be easy achieved by using only useRef
No, you need at least another useEffect or probably better useLayoutEffect?
And even then it does a teeny tiny bit more than your code.
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
is more likely equivalent to:
// using a function.
// no need to create this object over and over if there is no `ref`,
// or no need to update the `ref`.
const createRef = () => ({
focus: () => {
inputRef.current.focus();
}
});
useLayoutEffect(() => {
// refs can be functions!
if (typeof ref === "function") {
ref(createRef());
// when the ref changes, the old one is updated to `null`.
// Same on unmount.
return () => {
ref(null);
}
}
// and the same thing again for ref-objects
if (typeof ref === "object" && ref !== null && "current" in ref) {
ref.current = createRef();
return () => {
ref.current = null;
}
}
}, [ref]);

React forwardRef - access ref within component, and in parent

I need to access the ref to a textarea inside a component. Within the component, its easy enough:
const MyComponent = () => {
const inputRef = useRef();
return <textarea ref={inputRef} />
}
Now the ref is available within MyComponent and I can use it for some internal logic.
There are cases where I need to access the ref from the parent component as well. In that case, I can use forwardRef:
const MyComponent = React.forwardRef((props, ref) => {
return <textarea ref={ref} />
})
// In some parent
const MyParent = () => {
const inputRefFromParent = useRef();
return <MyComponent ref={inputRefFromParent} />
}
Now I can access to ref of the textarea from the parent component, and use it for logic within the parent component.
I find myself in a situation where I need to do some internal logic with the ref within MyComponent, but I may also need to get that ref from MyParent. How can I do this?
You can keep a ref in the MyComponent and expose what you would need in the parent component using useImperativeHandle hook using the ref passed from the MyParent.
Try like below. It exposes the focus method in the textarea to parent. And you can do any other internal things with the access to textAreaRef.
import { useRef, forwardRef, useImperativeHandle } from "react";
const MyComponent = forwardRef((props, ref) => {
const textAreaRef = useRef();
// all the functions or values you can expose here
useImperativeHandle(ref, () => ({
focus: () => {
textAreaRef.current.focus();
}
}));
const internalFunction = () => {
// access textAreaRef
};
return <textarea ref={textAreaRef} />;
});
// In some parent
const MyParent = () => {
const inputRefFromParent = useRef();
// you can call inputRefFromParent.current.focus(); in this compoenent
return <MyComponent ref={inputRefFromParent} />;
};
In addition to Amila's answer, I found another way to do it, by using a ref callback:
const MyComponent = React.forwardRef((props, parentRef) => {
const localRef = useRef();
return <textarea ref={ref => {
parentRef.current = ref;
localRef.current = ref;
}} />
})
So the callback ref keeps finer grain control of the ref to the textarea, and simply assigns its value to both the local ref and the parent ref.
You could do also the following:
const MyComponent = React.forwardRef((props, externalRef) => {
const internalRef = useRef<HTMLElement>();
const ref = useMemo(
() => externalRef || internalRef,
[externalRef, internalRef]
) as React.MutableRefObject<HTMLElement>;
return <textarea ref={ref} />
})

React - useImperativeHandle exposed properties are overlapped

I am creating a component "UsernameInput", that renders my custom "TextInput" component:
const UsernameInput = forwardRef(
(
{
...
},
ref
) => {
...
return <TextInput ref={ref} />
});
My TextInput component exposes some functionality to its parents:
const TextInput = forwardRef(
(
{
...
},
ref
) => {
...
useImperativeHandle(
ref,
() => ({
getText: () => text,
setText,
focus,
blur,
}),
[text]
);
...
});
Now... in my UsernameInput component, I want to expose other functionalities too...
const UsernameInput = forwardRef(
(
{
...
},
ref
) => {
...
useImperativeHandle(
ref,
() => ({
getUsernameInformation: () => usernameInformation,
}),
[usernameInformation]
);
return <TextInput ref={ref} />
});
The main problem I am experiencing is that, if I do the following:
const usernameInputRef = useRef(null);
const handleOnSubmit = () => {
usernameInputRef.current.blur()
}
return (
<UsernameInput ref={usernameInputRef} ...
);
The code throws me an exception ".blur() is undefined".
It seems that, because of having two useImperativeHandle, the exposed functionalities are overlapped.
Any ideas on how to solve this problem?
Based on #Amila Senadheera comment:
const textInputRef = useRef(null);
...
useImperativeHandle(
ref,
() => ({
getUsernameInformation: () => usernameInformation,
...textInputRef.current,
}),
[usernameInformation]
);
...
return <TextInput ref={textInputRef} ... />

React Component calling method to a different component

I have a page with the following structure
const Upload = (props) => {
return (
<BaseLayout>
<ToolbarSelection />
<Box>
<FileDropArea />
</Box>
</BaseLayout>
)
}
I have a method which works in the component <FileDropArea />
This is the method used as example
const allSelection = () => {
setFiles((files) =>
files.map((file) => {
file.checked = true;
return file;
})
);
};
In React how can i call this method allSelection from the <ToolbarSelection /> component, where i have my simple button like <Button>All Selection</Button>
You need to use React Context like this:
//create a fileContext.js
const fileContext = React.createContext();
const useFileContext = () => React.useContext(fileContext);
const FileContextProvider = ({ children }) => {
const [files, setFiles] = useState([]);
const allSelection = () => {
setFiles((files) =>
files.map((file) => {
file.checked = true;
return file;
})
);
};
// if you have other methods which may change the files add them here
return (
<fileContext.Provider
value={{
files,
setFiles,
allSelection,
}}
>
{children}
</fileContext.Provider>
);
};
use fileContextProvider in your upload file
const Upload = (props) => {
return (
<FileContextProvider>
<BaseLayout>
<ToolbarSelection />
<Box>
<FileDropArea />
</Box>
</BaseLayout>
</FileContextProvider>
);
};
use it, for example in ToolbarSelection like this:
const ToolbarSelection = () => {
const {files, allSelection} = useFileContext();
// do other stuff
}
React Hooks
I assume you are looking to make the allSelection function reusable. Hooks are a great way to make logic reusable across components.
Create a custom hook useAllSelection. Note that hooks should have a use prefix.
const useAllSelection = (files) => {
const [files, setFiles] = useState([]);
const handleAllSelection = () => {
setFiles((files) =>
files.map((file) => {
file.checked = true;
return file;
})
);
};
return { handleAllSelection };
};
const ToolbarSelection = () => {
// import the hook and use
const { handleAllSelection } = useAllSelection();
return (
<button onClick={handleAllSelection}>All Selection</button>
)
}
ReactJS allows to perform this scenario in a different way. Let me explain it: if you press a button in the ToolbarSelection, pass the value of the new state of that button to FileDropArea as props. Then, in the FileDropArea render, call the method or not depending on the value of that property
const Upload = (props) => {
return (
<BaseLayout>
<ToolbarSelection
onSelectionClick={(value) => setSelected(value)}
/>
<Box>
<FileDropArea
selected = { /* state of a button in the Toolbar */}
/>
</Box>
</BaseLayout>
)
}
Note how the callback in the Toolbar changes the state, and how this new state is passed to FileDropArea as property

How to use useeffect hook in react?

i want to return a function that uses useEffect from the usehook and i am getting error "useeffect is called in a function which is neither a react function component or custom hook.
what i am trying to do?
i have addbutton component and when user clicks add button i want to call the function requestDialog.
below is my code within addbutton file
function AddButton () {
const count = useGetCount();
const requestDialog = useRequestDialog(); //using usehook here
const on_add_click = () => {
requestDialog(count); //calling requestDialog here
}
return (
<button onClick={on_add_click}>add</button>
);
}
interface ContextProps {
trigger: (count: number) => void;
}
const popupContext = React.createContext<ContextProps>({
trigger: (availableSiteShares: number) => {},
});
const usePopupContext = () => React.useContext(popupContext);
export const popupContextProvider = ({ children }: any) => {
const [show, setShow] = React.useState(false);
const limit = 0;
const dismiss = () => {
if (show) {
sessionStorage.setItem(somePopupId, 'dismissed');
setShow(false);
}
};
const isDismissed = (dialogId: string) =>
sessionStorage.getItem(dialogId) === 'dismissed';
const context = {
trigger: (count: number) => {
if (!isDismissed(somePopupId) && count <= limit) {
setShow(true);
} else if (count > limit) {
setShow(false);
}
},
};
return (
<popupContext.Provider value={context}>
{children}
{show && (
<Popup onHide={dismiss} />
)}
</popupContext.Provider>
);
};
export function useRequestDialog(enabled: boolean,count: number) {
return function requestDialog() { //here is the error
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}
}, [count, trigger]);
}
How to solve the error ""useEffect is called in a function which is neither a react function component or custom hook."
i am not knowing how to use useeffect and the same time use it in the addbutton component.
could someone help me with this. thanks
useEffect method is like, useEffect(() => {}, []), But your usage in requestDialog is wrong. Try changing with following.
function requestDialog() {
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}, [count, trigger]);
}

Categories

Resources