taro
taro copied to clipboard
Input controlled 模式无效
相关平台
微信小程序
复现仓库
https://github.com/nuintun/taro-bugs
小程序基础库: 2.52.2 使用框架: React
index.module.scss
.inputNumber {
width: 176px;
display: flex;
min-height: 54px;
position: relative;
border-radius: 8px;
align-items: center;
box-sizing: border-box;
background-color: #fff;
border: 1px solid #e1e1e1;
.input {
outline: none;
padding: 0 2px;
font-size: 28px;
text-align: center;
}
.decrease {
border-right: 1px solid #e1e1e1;
}
.increase {
border-left: 1px solid #e1e1e1;
}
.decrease,
.increase {
.text{
width: 48px;
height: 48px;
line-height: 48px;
text-align: center;
display: inline-block;
vertical-align: middle;
}
&.disabled {
.text{
opacity: 0.12;
}
}
}
}
utils.ts
/**
* @function isFunction
* @description 是否为函数
* @param value 需要验证的值
*/
export function isFunction(value: any): value is Function {
return typeof value === 'function';
}
useIsMounted.tsx
/**
* @module useIsMounted
*/
import { useCallback, useEffect, useRef } from 'react';
/**
* @function useIsMounted
* @description [hook] 检查组件是否已经挂载
*/
export default function useIsMounted(): () => boolean {
const isMountedRef = useRef(false);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
return isMounted;
}
usePersistRef.ts
/**
* @module usePersistRef
*/
import React, { useRef } from 'react';
/**
* @function usePersistRef
* @description 生成自更新 useRef 对象
*/
export default function usePersistRef<T = undefined>(): React.MutableRefObject<T | undefined>;
/**
* @function usePersistRef
* @description 生成自更新 useRef 对象
* @param value 引用值
*/
export default function usePersistRef<T>(value: T): React.MutableRefObject<T>;
export default function usePersistRef<T = undefined>(value?: T): React.MutableRefObject<T | undefined> {
const valueRef = useRef(value);
valueRef.current = value;
return valueRef;
}
useUpdateEffect.ts
/**
* @module useUpdateEffect
*/
import React, { useEffect, useRef } from 'react';
/**
* @function useUpdateEffect
* @description [hook] 组件 useEffect 后回调
* @param effect 回调函数
* @param deps 回调依赖
*/
export default function useUpdateEffect(effect: React.EffectCallback, deps?: React.DependencyList): void {
const isMountedRef = useRef(false);
useEffect(() => {
if (!isMountedRef.current) {
isMountedRef.current = true;
} else {
return effect();
}
}, deps);
}
useControllableValue.ts
/**
* @module useControllableValue
*/
import React, { useCallback, useState } from 'react';
import { isFunction } from './utils';
import useIsMounted from './useIsMounted';
import usePersistRef from './usePersistRef';
import useUpdateEffect from './useUpdateEffect';
export interface Props {
[prop: string]: any;
}
export interface Options<V> {
trigger?: string;
defaultValue?: V;
valuePropName?: string;
defaultValuePropName?: string;
}
/**
* @function getValuePropName
* @param options 配置选项
*/
function getValuePropName<V>(options: Options<V>): string {
const { valuePropName = 'value' } = options;
return valuePropName;
}
/**
* @function getDefaultValuePropName
* @param options 配置选项
*/
function getDefaultValuePropName<V>(options: Options<V>): string {
const { defaultValuePropName = 'defaultValue' } = options;
return defaultValuePropName;
}
/**
* @function isControlled
* @param props 组件 Props
* @param options 配置选项
*/
function isControlled<V>(props: Props, options: Options<V>): boolean {
const valuePropName = getValuePropName(options);
return valuePropName in props;
}
/**
* @function isUncontrolled
* @param props 组件 Props
* @param options 配置选项
*/
function isUncontrolled<V>(props: Props, options: Options<V>): boolean {
const defaultValuePropName = getDefaultValuePropName(options);
return defaultValuePropName in props;
}
/**
* @function getValue
* @param props 组件 Props
* @param options 配置选项
*/
function getValue<V>(props: Props, options: Options<V>): V {
const valuePropName = getValuePropName(options);
return props[valuePropName];
}
/**
* @function getDefaultValue
* @param props 组件 Props
* @param options 配置选项
*/
function getDefaultValue<V>(props: Props, options: Options<V>): V {
const defaultValuePropName = getDefaultValuePropName(options);
return props[defaultValuePropName];
}
/**
* @function useControllableValue
* @description [hook] 生成同时支持受控和非受控状态的值
* @param props 组件 Props
* @param options 配置选项
*/
export default function useControllableValue<V = undefined>(
props: Props,
options: Options<V> = {}
): [value: V | undefined, setValue: (value: React.SetStateAction<V | undefined>, ...args: any[]) => void] {
const isMounted = useIsMounted();
const propsRef = usePersistRef(props);
const optionsRef = usePersistRef(options);
const [value, setValueState] = useState<V | undefined>(() => {
if (isControlled(props, options)) {
return getValue(props, options);
}
if (isUncontrolled(props, options)) {
return getDefaultValue(props, options);
}
return options.defaultValue;
});
const prevValueRef = usePersistRef(value);
const setValue = useCallback((value: React.SetStateAction<V | undefined>, ...args: any[]) => {
if (isMounted()) {
const props = propsRef.current;
const setStateAction = (prevState: V | undefined): V | undefined => {
const { trigger = 'onChange' } = props;
const nextState = isFunction(value) ? value(prevState) : value;
if (nextState !== prevState && isFunction(props[trigger])) {
props[trigger](nextState, ...args);
}
return nextState;
};
if (isControlled(props, optionsRef.current)) {
setStateAction(prevValueRef.current);
} else {
setValueState(setStateAction);
}
}
}, []);
useUpdateEffect(() => {
if (isControlled(props, options)) {
const prevValue = prevValueRef.current;
const nextValue = getValue(props, options);
if (nextValue !== prevValue) {
setValueState(nextValue);
}
}
});
return [value, setValue];
}
index.tsx
import styles from './index.module.scss';
import React, { memo, useCallback, useMemo } from 'react';
import classNames from 'classnames';
import useControllableValue from './useControllableValue';
import { BaseEventOrig, Image, Input, InputProps, Text, View } from '@tarojs/components';
export interface InputNumberProps extends Omit<InputProps, 'value'> {
min?: number;
max?: number;
step?: number;
value?: number;
controls?: boolean;
defaultValue?: number;
prefix?: React.ReactNode;
suffix?: React.ReactNode;
onChange?: (value: number) => void;
}
function resolveValue(value: number, props: InputNumberProps): number {
const { min, max } = props;
if (min != null && value < min) {
return min;
}
if (max != null && value > max) {
return max;
}
return value;
}
export default memo(function InputNumber(props: InputNumberProps): React.ReactElement {
const { min, max, step = 1, className, disabled, controls = true, ...restProps } = props;
const [value = 0, setValue] = useControllableValue<number>(props);
const onIncrease = useCallback(() => {
if (!disabled) {
setValue((value = 0) => resolveValue(value + step, props));
}
}, [min, max, step, disabled]);
const onDecrease = useCallback(() => {
if (!disabled) {
setValue((value = 0) => resolveValue(value - step, props));
}
}, [min, max, step, disabled]);
const onInput = useCallback(
(e: BaseEventOrig<InputProps.inputValueEventDetail>) => {
const input = +e.detail.value;
if (!Number.isNaN(input)) {
const nextValue = resolveValue(input, props);
setValue(nextValue);
return nextValue;
}
return value;
},
[value, min, max]
);
const [increaseClassName, decreaseClassName] = useMemo(() => {
const { disabled: disabledClassName } = styles;
return [
classNames(styles.increase, { [disabledClassName]: max == null ? disabled : value >= max }),
classNames(styles.decrease, { [disabledClassName]: min == null ? disabled : value <= min })
];
}, [value, min, max, disabled]);
return (
<View className={classNames(styles.inputNumber, className)}>
{controls && (
<View className={decreaseClassName} onClick={onDecrease}>
<Text className={styles.text}>-</Text>
</View>
)}
<Input
{...restProps}
controlled
type="number"
onInput={onInput}
confirmType="done"
cursorSpacing={50}
disabled={disabled}
value={value.toString()}
className={classNames('ui-input', styles.input)}
/>
{controls && (
<View className={increaseClassName} onClick={onIncrease}>
<Text className={styles.text}>+</Text>
</View>
)}
</View>
);
});
复现步骤
输入,无法限定 Input 值 受控和非受控模式很怪异,非受控,除了初始化后面修改 value 不应该更新视图,受控才应该修改。 现在 controlled 模式就像个摆设! 不如学习 React Input 用 defaultValue 和 value 区分受控和非受控😁
期望结果
受控模式生效
实际结果
受控模式无效
环境信息
👽 Taro v3.5.1
Taro CLI 3.5.1 environment info:
System:
OS: Windows 10
Binaries:
Node: 18.7.0 - C:\Program Files\nodejs\node.EXE
Yarn: 1.22.19 - C:\Users\nuintun\AppData\Roaming\npm\yarn.CMD
npm: 8.15.0 - C:\Program Files\nodejs\npm.CMD

@nuintun 麻烦提供一下完整 demo
@Chen-jj 缺失文件已补全
https://github.com/nuintun/taro-bugs
@Chen-jj ??