Repository: ant-design/ant-design-mobile
Pull request #5456 (2022-07-08 02:04:13)

feat(Switch): deprecate `beforeChange` prop and `onChange` supports promise
Switch 弃用了 `beforeChange` 属性，而直接把异步处理的能力合并到 `onChange` 里了

这样，如果用户需要拦截和处理报错的话，就可以手动在 `onChange` 里 `try/catch` 了


Patch:
diff --git a/src/components/switch/demos/demo2.tsx b/src/components/switch/demos/demo2.tsx
index bf8fba1ac..e21a3d740 100644
--- a/src/components/switch/demos/demo2.tsx
+++ b/src/components/switch/demos/demo2.tsx
@@ -1,15 +1,9 @@
-import React from 'react'
+import React, { useState } from 'react'
 import { Space, Switch } from 'antd-mobile'
 import { DemoBlock } from 'demos'
 
 export default () => {
-  const beforeChange = (): Promise<void> => {
-    return new Promise(resolve => {
-      setTimeout(() => {
-        resolve()
-      }, 1000)
-    })
-  }
+  const [checked, setChecked] = useState(false)
 
   return (
     <>
@@ -26,9 +20,23 @@ export default () => {
 
       <DemoBlock title='异步'>
         <Space wrap>
-          <Switch defaultChecked beforeChange={() => beforeChange()} />
+          <Switch
+            checked={checked}
+            onChange={async val => {
+              await mockRequest()
+              setChecked(val)
+            }}
+          />
         </Space>
       </DemoBlock>
     </>
   )
 }
+
+const mockRequest = (): Promise<void> => {
+  return new Promise(resolve => {
+    setTimeout(() => {
+      resolve()
+    }, 1000)
+  })
+}
diff --git a/src/components/switch/index.en.md b/src/components/switch/index.en.md
index 2d877b4b3..988d4fa7a 100644
--- a/src/components/switch/index.en.md
+++ b/src/components/switch/index.en.md
@@ -17,16 +17,16 @@ switch selector.
 
 ### Props
 
-| Name           | Description                                 | Type                              | Default |
-| -------------- | ------------------------------------------- | --------------------------------- | ------- |
-| beforeChange   | Execute before change                       | `(val: boolean) => Promise<void>` | -       |
-| checked        | Specify whether it is currently opened      | `boolean`                         | `false` |
-| checkedText    | Selected text                               | `ReactNode`                       | -       |
-| defaultChecked | Whether to open initially                   | `boolean`                         | `false` |
-| disabled       | Disabled status                             | `boolean`                         | `false` |
-| loading        | Loading status                              | `boolean`                         | `false` |
-| onChange       | Callback function when the value is changed | `(val: boolean) => void`          | -       |
-| uncheckedText  | Non-selected text                           | `ReactNode`                       | -       |
+| Name           | Description                                                                                                           | Type                                      | Default |
+| -------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------- |
+| beforeChange   | Execute before change                                                                                                 | `(val: boolean) => Promise<void>`         | -       |
+| checked        | Specify whether it is currently opened                                                                                | `boolean`                                 | `false` |
+| checkedText    | Selected text                                                                                                         | `ReactNode`                               | -       |
+| defaultChecked | Whether to open initially                                                                                             | `boolean`                                 | `false` |
+| disabled       | Disabled status                                                                                                       | `boolean`                                 | `false` |
+| loading        | Loading status                                                                                                        | `boolean`                                 | `false` |
+| onChange       | The callback function when changing, when the Promise is returned, the loading status will be displayed automatically | `(val: boolean) => void \| Promise<void>` | -       |
+| uncheckedText  | Non-selected text                                                                                                     | `ReactNode`                               | -       |
 
 ### CSS Variables
 
@@ -36,3 +36,21 @@ switch selector.
 | --checked-color | Filled color | `var(--adm-color-primary)` |
 | --height        | Height       | `31px`                     |
 | --width         | Width        | `51px`                     |
+
+## FAQ
+
+### How to handle exceptions in async `onChange`?
+
+The `onChange` event supports returning a Promise, when the Promise starts, the Switch will automatically enter the loading state, and when the Promise completes or fails, the Switch will automatically exit the loading state. In general, this satisfies the needs of most projects.
+
+But when Promise fails, Switch will not eat the error, but will re-throw the error object. This is the expected behavior. If you want to intercept some errors yourself and avoid them being thrown, you can use `try/ catch` wraps the processing logic in `onChange`, for example:
+
+```tsx
+async function onChange(val: boolean) {
+  try {
+    await doSomething();
+  } catch (e) {
+    // handle or ignore error
+  }
+}
+```
diff --git a/src/components/switch/index.zh.md b/src/components/switch/index.zh.md
index fd1053355..212ccd33b 100644
--- a/src/components/switch/index.zh.md
+++ b/src/components/switch/index.zh.md
@@ -17,16 +17,16 @@
 
 ### 属性
 
-| 参数           | 说明             | 类型                              | 默认值  |
-| -------------- | ---------------- | --------------------------------- | ------- |
-| beforeChange   | 变化前执行       | `(val: boolean) => Promise<void>` | -       |
-| checked        | 指定当前是否打开 | `boolean`                         | `false` |
-| checkedText    | 选中时的内容     | `ReactNode`                       | -       |
-| defaultChecked | 初始是否打开     | `boolean`                         | `false` |
-| disabled       | 禁用状态         | `boolean`                         | `false` |
-| loading        | 加载状态         | `boolean`                         | `false` |
-| onChange       | 变化时回调函数   | `(val: boolean) => void`          | -       |
-| uncheckedText  | 非选中时的内容   | `ReactNode`                       | -       |
+| 参数                   | 说明                                                    | 类型                                      | 默认值  |
+| ---------------------- | ------------------------------------------------------- | ----------------------------------------- | ------- |
+| beforeChange（已弃用） | 变化前执行（已弃用，推荐使用 `onChange` 属性）          | `(val: boolean) => Promise<void>`         | -       |
+| checked                | 指定当前是否打开                                        | `boolean`                                 | `false` |
+| checkedText            | 选中时的内容                                            | `ReactNode`                               | -       |
+| defaultChecked         | 初始是否打开                                            | `boolean`                                 | `false` |
+| disabled               | 禁用状态                                                | `boolean`                                 | `false` |
+| loading                | 加载状态                                                | `boolean`                                 | `false` |
+| onChange               | 变化时的回调函数，当返回 Promise 时，会自动显示加载状态 | `(val: boolean) => void \| Promise<void>` | -       |
+| uncheckedText          | 非选中时的内容                                          | `ReactNode`                               | -       |
 
 ### CSS 变量
 
@@ -36,3 +36,21 @@
 | --checked-color | 填充颜色 | `var(--adm-color-primary)` |
 | --height        | 高度     | `31px`                     |
 | --width         | 宽度     | `51px`                     |
+
+## FAQ
+
+### 如何处理异步 `onChange` 中的异常？
+
+`onChange` 事件支持返回一个 Promise，当 Promise 开始时，Switch 会自动进入加载状态，而当 Promise 完成或失败时，Switch 会自动退出加载状态。一般情况下，这满足大多数项目的需求。
+
+但是当 Promise 失败时，Switch 并不会吃掉报错，而是会把这个错误对象重新抛出来，这是预期行为，如果你想自己拦截掉一些错误，避免他们被抛出，可以使用 `try/catch` 包裹 `onChange` 中的处理逻辑，例如：
+
+```tsx
+async function onChange(val: boolean) {
+  try {
+    await doSomething();
+  } catch (e) {
+    // handle or ignore error
+  }
+}
+```
diff --git a/src/components/switch/switch.tsx b/src/components/switch/switch.tsx
index 4c020a089..f4e8698db 100644
--- a/src/components/switch/switch.tsx
+++ b/src/components/switch/switch.tsx
@@ -5,6 +5,7 @@ import { usePropsValue } from '../../utils/use-props-value'
 import { mergeProps } from '../../utils/with-default-props'
 import { SpinIcon } from './spin-icon'
 import { useConfig } from '../config-provider'
+import { isPromise } from '../../utils/validate'
 
 const classPrefix = `adm-switch`
 
@@ -13,8 +14,9 @@ export type SwitchProps = {
   disabled?: boolean
   checked?: boolean
   defaultChecked?: boolean
+  /** @deprecated use `onChange` instead */
   beforeChange?: (val: boolean) => Promise<void>
-  onChange?: (checked: boolean) => void
+  onChange?: (checked: boolean) => void | Promise<void>
   checkedText?: ReactNode
   uncheckedText?: ReactNode
 } & NativeProps<'--checked-color' | '--width' | '--height' | '--border-width'>
@@ -44,14 +46,22 @@ export const Switch: FC<SwitchProps> = p => {
       setChanging(true)
       try {
         await props.beforeChange(nextChecked)
-        setChecked(nextChecked)
         setChanging(false)
       } catch (e) {
         setChanging(false)
         throw e
       }
-    } else {
-      setChecked(nextChecked)
+    }
+    const result = setChecked(nextChecked)
+    if (isPromise(result)) {
+      setChanging(true)
+      try {
+        await result
+        setChanging(false)
+      } catch (e) {
+        setChanging(false)
+        throw e
+      }
     }
   }
 
diff --git a/src/utils/use-props-value.ts b/src/utils/use-props-value.ts
index 4bf7aee11..211ff9cf7 100644
--- a/src/utils/use-props-value.ts
+++ b/src/utils/use-props-value.ts
@@ -27,7 +27,7 @@ export function usePropsValue<T>(options: Options<T>) {
       if (!forceTrigger && nextValue === stateRef.current) return
       stateRef.current = nextValue
       update()
-      onChange?.(nextValue)
+      return onChange?.(nextValue)
     }
   )
   return [stateRef.current, setState] as const


Test patch:
diff --git a/src/components/switch/tests/switch.test.tsx b/src/components/switch/tests/switch.test.tsx
index 96b9bb6c1..23d29c3be 100644
--- a/src/components/switch/tests/switch.test.tsx
+++ b/src/components/switch/tests/switch.test.tsx
@@ -1,5 +1,13 @@
 import React, { useState } from 'react'
-import { fireEvent, render, testA11y, waitFor, screen } from 'testing'
+import {
+  fireEvent,
+  render,
+  testA11y,
+  waitFor,
+  screen,
+  sleep,
+  act,
+} from 'testing'
 import Switch from '..'
 
 const classPrefix = `adm-switch`
@@ -69,4 +77,30 @@ describe('Switch', () => {
     })
     jest.useRealTimers()
   })
+
+  test('`onChange` returns a Promise', async () => {
+    jest.useFakeTimers()
+    const App = () => {
+      const [checked, setChecked] = useState(false)
+      return (
+        <Switch
+          checked={checked}
+          onChange={async val => {
+            await sleep(1000)
+            setChecked(val)
+          }}
+        />
+      )
+    }
+
+    render(<App />)
+    const switchEl = screen.getByRole('switch')
+    fireEvent.click(switchEl)
+    expect(switchEl).toHaveClass(`${classPrefix}-disabled`)
+    await act(async () => {
+      jest.runAllTimers()
+    })
+    expect(switchEl).toHaveClass(`${classPrefix}-checked`)
+    jest.useRealTimers()
+  })
 })