Skip to content

Commit 0c6ae77

Browse files
authored
fix: useKeyboardState tweaks (#907)
## 📜 Description Revisited some concepts of `useKeyboardState`/`KeyboardController.state()` etc. ## 💡 Motivation and Context After preparing a new release I went through the changes and realized, that it would be better to take a step back and keep `isVisible`/`state` decoupled but `useKeyboardState` should return data to all variables (i. e. aggregate them). ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### Docs - mention animated hooks alternatives; ### JS - don't include `isVisible` in `.state` method - update types declaration to show that state doesn't return `null` anymore; ## 🤔 How Has This Been Tested? Tested via CI. ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
1 parent 820fcb4 commit 0c6ae77

File tree

6 files changed

+46
-30
lines changed

6 files changed

+46
-30
lines changed

docs/docs/api/hooks/keyboard/use-keyboard-state.mdx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@ sidebar_position: 4
55

66
# useKeyboardState
77

8-
`useKeyboardState` is a hook which gives an access to current keyboard state.
8+
`useKeyboardState` is a hook which gives an access to current keyboard state. This hook combines data from `KeyboardController.state()` and `KeyboardController.isVisible()` methods and makes it reactive (i. e. triggers a re-render when keyboard state/visibility has changed).
99

1010
:::warning
11-
Use this hook only when you need to control `props` of views returned in JSX-markup. If you need to access the keyboard `state` in callbacks or event handlers then consider to use [KeyboardController.state()](../../keyboard-controller.md#state) method instead. This allows you to retrieve values as needed without triggering unnecessary re-renders.
11+
Use this hook only when you need to control `props` of views returned in JSX-markup. If you need to access the keyboard `state` in callbacks or event handlers then consider to use [KeyboardController.state()](../../keyboard-controller.md#state) or [KeyboardController.isVisible()](../../keyboard-controller.md#isvisible) methods instead. This allows you to retrieve values as needed without triggering unnecessary re-renders.
1212

1313
<div className="code-grid">
1414
<div className="code-block">
1515

1616
```tsx title="✅ Recommended 👍"
17-
// use KeyboardController.state()
17+
// use KeyboardController.isVisible()
1818

1919
<Button
2020
onPress={() => {
21-
const state = KeyboardController.state();
22-
if (state.isVisible) {
21+
// read value on demand
22+
if (KeyboardController.isVisible()) {
2323
// ...
2424
}
2525
}}
@@ -51,9 +51,25 @@ const { isVisible } = useKeyboardState();
5151

5252
:::
5353

54+
:::tip
55+
Also make sure that if you need to change style based on keyboard presence then you are using corresponding [animated](./use-keyboard-animation) hooks to offload animation to a native thread and free up resources for JS thread.
56+
:::
57+
5458
## Data structure
5559

56-
`useKeyboardState` returns a [`KeyboardState`](../../keyboard-controller.md#state) object.
60+
The `KeyboardState` is represented by following structure:
61+
62+
```ts
63+
type KeyboardState = {
64+
isVisible: boolean;
65+
height: number;
66+
duration: number; // duration of the animation
67+
timestamp: number; // timestamp of the event from native thread
68+
target: number; // tag of the focused `TextInput`
69+
type: string; // `keyboardType` property from focused `TextInput`
70+
appearance: string; // `keyboardAppearance` property from focused `TextInput`
71+
};
72+
```
5773

5874
## Example
5975

docs/docs/api/keyboard-controller.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,15 @@ if (KeyboardController.isVisible()) {
9393
### `state`
9494

9595
```ts
96-
static state(): KeyboardState;
96+
static state(): KeyboardEventData;
9797
```
9898

9999
This method returns the last keyboard state. It returns `null` if keyboard was not shown in the app yet.
100100

101-
The `KeyboardState` is represented by following structure:
101+
The `KeyboardEventData` is represented by following structure:
102102

103103
```ts
104-
type KeyboardState = {
105-
isVisible: boolean;
104+
type KeyboardEventData = {
106105
height: number;
107106
duration: number; // duration of the animation
108107
timestamp: number; // timestamp of the event from native thread

jest/index.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,18 @@ const focusedInput = {
2929
set: jest.fn(),
3030
},
3131
};
32-
const state = {
33-
isVisible: false,
32+
const lastKeyboardEvent = {
3433
height: 0,
3534
duration: 0,
3635
timestamp: 0,
3736
target: 0,
3837
type: "default",
3938
appearance: "default",
4039
};
40+
const state = {
41+
...lastKeyboardEvent,
42+
isVisible: false,
43+
};
4144

4245
const mock = {
4346
// hooks
@@ -65,7 +68,7 @@ const mock = {
6568
dismiss: jest.fn().mockReturnValue(Promise.resolve()),
6669
setFocusTo: jest.fn(),
6770
isVisible: jest.fn().mockReturnValue(false),
68-
state: jest.fn().mockReturnValue(state),
71+
state: jest.fn().mockReturnValue(lastKeyboardEvent),
6972
},
7073
AndroidSoftInputModes: {
7174
SOFT_INPUT_ADJUST_NOTHING: 48,

src/hooks/useKeyboardState/index.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,31 @@ import { useEffect, useState } from "react";
33
import { KeyboardEvents } from "../../bindings";
44
import { KeyboardController } from "../../module";
55

6+
import type { KeyboardState } from "../../types";
7+
68
const EVENTS = ["keyboardDidShow", "keyboardDidHide"] as const;
79

8-
export const useKeyboardState = () => {
9-
const [state, setState] = useState(() => KeyboardController.state());
10+
const getLatestState = () => ({
11+
...KeyboardController.state(),
12+
isVisible: KeyboardController.isVisible(),
13+
});
14+
15+
export const useKeyboardState = (): KeyboardState => {
16+
const [state, setState] = useState(getLatestState);
1017

1118
useEffect(() => {
1219
const subscriptions = EVENTS.map((event) =>
1320
KeyboardEvents.addListener(event, () =>
1421
// state will be updated by global listener first,
1522
// so we simply read it and don't derive data from the event
16-
setState(KeyboardController.state()),
23+
setState(getLatestState),
1724
),
1825
);
1926

2027
// we might have missed an update between reading a value in render and
2128
// `addListener` in this handler, so we set it here. If there was
2229
// no change, React will filter out this update as a no-op.
23-
setState(KeyboardController.state());
30+
setState(getLatestState);
2431

2532
return () => {
2633
subscriptions.forEach((subscription) => subscription.remove());

src/module.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import type {
44
DismissOptions,
55
KeyboardControllerModule,
66
KeyboardEventData,
7-
KeyboardState,
87
} from "./types";
98

109
let isClosed = true;
11-
let lastState: KeyboardState = {
12-
isVisible: false,
10+
let lastState: KeyboardEventData = {
1311
height: 0,
1412
duration: 0,
1513
timestamp: new Date().getTime(),
@@ -18,21 +16,14 @@ let lastState: KeyboardState = {
1816
appearance: "default",
1917
};
2018

21-
const getKeyboardStateFromEvent = (event: KeyboardEventData): KeyboardState => {
22-
return {
23-
isVisible: event.height > 0,
24-
...event,
25-
};
26-
};
27-
2819
KeyboardEvents.addListener("keyboardDidHide", (e) => {
2920
isClosed = true;
30-
lastState = getKeyboardStateFromEvent(e);
21+
lastState = e;
3122
});
3223

3324
KeyboardEvents.addListener("keyboardDidShow", (e) => {
3425
isClosed = false;
35-
lastState = getKeyboardStateFromEvent(e);
26+
lastState = e;
3627
});
3728

3829
const dismiss = async (options?: DismissOptions): Promise<void> => {

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export type KeyboardControllerModule = {
131131
dismiss: (options?: DismissOptions) => Promise<void>;
132132
setFocusTo: (direction: Direction) => void;
133133
isVisible: () => boolean;
134-
state: () => KeyboardEventData | null;
134+
state: () => KeyboardEventData;
135135
};
136136
export type KeyboardControllerNativeModule = {
137137
// android only

0 commit comments

Comments
 (0)