React - useImperativeHandle exposed properties are overlapped - javascript

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} ... />

Related

Best way to update the local state of a component in react on some external event

Here's the scenario. I have a app and a search component.
const App = () => {
const [search, setSearch] = useState("initial query");
// ... other code
return (
<Search search={search} setSearch={setSearch} />
...other components
);
};
const Search = ({ search, setSearch }) => {
const [localSearch, setLocalSearch] = useState(search);
const debouncedSetSearch = useMemo(() => debounce(setSearch, 200), [setSearch]);
const handleTextChange = useCallback((e) => {
setLocalSearch(e.target.value);
debouncedSetSearch(e.target.value);
}, [setLocalSearch]);
return (
<input value={localSearch} onChange={handleTextChange} />
);
}
It's all good until this point. But I want to know what's the best way to change the search text on an external event. So far, the best approach I've found is using events.
const App = () => {
const [search, setSearch] = useState("initial query");
// ... other code
useEffect(() => {
onSomeExternalEvent((newSearch) => {
setSearch(newSearch);
EventBus.emit("updateSearch", newSearch);
});
}, []);
return (
<Search search={search} setSearch={setSearch} />
...other components
);
};
const Search = ({ search, setSearch }) => {
const [localSearch, setLocalSearch] = useState(search);
const debouncedSetSearch = useMemo(() => debounce(setSearch, 200), [setSearch]);
const handleTextChange = useCallback((e) => {
setLocalSearch(e.target.value);
debouncedSetSearch(e.target.value);
}, [setLocalSearch]);
useEffect(() => {
EventBus.subscribe("updateSearch", (newSearch) => {
setLocalSearch(newSearch);
});
}, []);
return (
<input value={localSearch} onChange={handleTextChange} />
);
}
Is there a better (correct) way of doing this?

Extend React forwardRef component with methods

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>;
});

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]);

How to fix TypeError: navigation.setOptions is not a function

I'm trying to implement react native test library with jest to my app.
For now I have a problem with the navigation on my component.
When I'm running the test, I've got an error :
TypeError: navigation.setOptions is not a function
Here is my component:
const initialState: StateTypes = {
toShowAsGridLayout: false,
isLoadingMoreContacts: false
};
export const Main: FC<Props> = ({ navigation }) => {
const dispatch = useDispatch();
const { data } = useSelector((state: RootState) => state.appReducer);
const [state, setState] = useState<StateTypes>(initialState);
useLayoutEffect(() => {
navigation.setOptions({
title: state.isLoadingMoreContacts ? strings.LOADING : strings.ALL_CONTACTS + ' - ' + data.length,
headerRight: () => (
<TouchableOpacity style={styles.changeLayoutButton} onPress={changeLayout}>
<Text style={styles.changeLayoutText}>{state.toShowAsGridLayout ? strings.LIST : strings.GRID}</Text>
</TouchableOpacity>
)
});
}, [state.isLoadingMoreContacts, state.toShowAsGridLayout])
return (
<View style={styles.container}>
{renderLayout()}
</View>
);
};
Here is a router:
const SplashStack = createStackNavigator();
const MainStack = createStackNavigator();
export const RootNavigator = () => {
const { isDataLoading } = useSelector((state: RootState) => state.appReducer);
return (
isDataLoading
? <SplashStack.Navigator>
<SplashStack.Screen name={'SplashStack'} component={Splash} />
</SplashStack.Navigator>
: <MainStack.Navigator>
<MainStack.Screen name={'Main'} component={Main} />
<MainStack.Screen name={'ContactDetails'} component={ContactDetails} />
</MainStack.Navigator>
);
};
And a test itself:
import React from 'react';
import { render } from '#testing-library/react-native';
import { Main } from '../Main';
import * as redux from 'react-redux';
import strings from '../../constants/strings';
import mocks from '../../mocks';
describe('dispatch mock', () => {
it('should dispatch mock', () => {
const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
const useSelectorSpy = jest.spyOn(redux, 'useSelector');
const mockDispatchFn = jest.fn();
useDispatchSpy.mockReturnValue(mockDispatchFn);
useSelectorSpy.mockReturnValue({ data: mocks });
const { getByText } = render(<Main navigation={({})} />);
getByText(strings.ALL_CONTACTS);
});
});
How can i fix this error ? What should I pass to navigation props in line :
const { getByText } = render(<Main navigation={({})} />);
You need to pass object with setOptions method.
const { getByText } = render(<Main navigation={
{
setOptions: (props: { title: string, headerRight: React.FC }) => void
}
} />);
This answer might be relevant
For my purpose solution was very easy, I just added ?. at the end of setOptions
useLayoutEffect(() => {
navigation.setOptions?.({ // <--- CHANGED HERE
title: state.isLoadingMoreContacts ? strings.LOADING : strings.ALL_CONTACTS + ' - ' + data.length,
headerRight: () => (
<TouchableOpacity style={styles.changeLayoutButton} onPress={changeLayout}>
<Text style={styles.changeLayoutText}>{state.toShowAsGridLayout ? strings.LIST : strings.GRID}</Text>
</TouchableOpacity>
)
});
}, [state.isLoadingMoreContacts, state.toShowAsGridLayout])
I am also face the same issue. With below code changes I have resolved my issue.
For more info please check this answer
const createTestProps = (props: Object) => ({
navigation: {
navigate: jest.fn(),
setOptions: jest.fn()
},
...props
});
const props = createTestProps({});
const { toJSON } = render(<MainScreen {...props} />);
You have to mock the properties and function that your are using in your screen has to be mocked like above. For more info please check this answer

How can I pass default props as function in functional component

I am using functional components in application. Below is my code.
const Modal = forwardRef((props, ref) => {
const { handleClose, children, ...rest } = props;
const [open, setOpen] = useState(false);
useImperativeHandle(ref, () => ({
handleOpen() {
setOpen(true);
},
handleClose() {
setOpen(false);
}
}));
const modalHandleClose = () => {
ref.current.handleClose();
};
return (
<Dialog
open={open}
onClose={handleClose}
scroll="body"
{...rest}
disableBackdropClick
>
{children}
</Dialog>
);
});
In above component I want pass a default prop for handleClose as below
Modal.defaultProps = {
handleCLose: modalHandleClose
};
but I am getting error that "modalHandleClose not defined". What can I try to resolve this?
React's defaultProps can't do that, since it's a static property of the component. It cannot access anything that only will exist once the component renders.
I would instead recommend using default values when destructuring the props:
const Modal = forwardRef((props, ref) => {
const modalHandleClose = () => {
ref.current.handleClose();
};
const {
handleClose = modalHandleClose, // <----- default value
children,
...rest
} = props;

Categories

Resources