ReactNative总结系列一 --- Hook避坑指南

0 阅读17分钟

这篇文章不是简单的总结基础使用,而是结合 React 官方文档、阿里的文档以及自己在实际工作中的经验,总结的一份实战总结,认真看完应该会对Hook有更深的理解。

文中的很多错误例子,是我在实际工作项目里发现有刚接触hook的同学写出来的,由于涉及公司代码,做了了精简和变量名替换。有不对的地方欢迎指正。

useState ✅

用于保存状态,触发界面刷新。

基础使用

const [state, setState] = useState({ name: "", age: 0 })

// 更新 state,有两种方式
// ✅ 设置新对象
setState({ name: "zhangsan", age: 0 })

// ✅ 需要使用最新的 state 旧数据
setState(pre => ({ ...pre, age: pre.age + 1 }))

错误使用

❌ 直接修改 state,或 setState 后立即读取

const [state, setState] = useState({ name: "", age: 0 })

// ❌ 直接修改 state 里的值,不会触发刷新
state.name = "zhangsan"

// ✅ 使用 setState 来更新
setState({ name: "zhangsan", age: 0 })

// ❌ setState 后马上读取,这里还是输出 "",而不是 zhangsan
console.log(state.name)

// ✅ setState 改变 state 是异步的,下一次 render 时 state 才会更新

❌ useState 保存不会变的数据

const App = () => {
  // ❌ config 不会每次渲染都变,没必要放 state 里
  const [config] = useState({
    apiUrl: 'https://api.example.com',
    timeout: 5000,
    retry: 3,
  });
  return <Text>{config.apiUrl}</Text>;
};

// ✅ 放到组件外面,或者直接去掉 useState
const CONFIG = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retry: 3,
};

const App = () => {
  return <Text>{CONFIG.apiUrl}</Text>;
};

❌ 冗余的 state

const [user, setUser] = useState({ name: 'zhangsan', age: 18, city: 'xxx' })

// ❌ 从 user 里拆出 3 个冗余的 state
const [userName, setUserName] = useState('');
const [userAge, setUserAge] = useState(0);
const [userCity, setUserCity] = useState('');

useEffect(() => {
  setUserName(user.name);
  setUserAge(user.age);
  setUserCity(user.city);
}, [user]);

// ✅ 直接取值,删掉多余的 useState 和 useEffect
const userName = user.name;
const age = user.age;
const city = user.city;
// ❌ 多个 state 总是同时更新
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const move = (newX: number, newY: number) => {
  setX(newX);
  setY(newY);
};

// ✅ 合并成一个对象
const [position, setPosition] = useState({ x: 0, y: 0 });
const move = (x: number, y: number) => {
  setPosition({ x, y });
};
// ❌ 互相矛盾的 state,容易改漏
const [isFocused, setIsFocused] = useState(false);
const [isBlurred, setIsBlurred] = useState(true);

const onFocus = () => {
  setIsFocused(true);
  setIsBlurred(false);
};
const onBlur = () => {
  setIsBlurred(true);
  setIsFocused(false);
};

// ✅ 用单一枚举代替,天然互斥
const [focusState, setFocusState] = useState<'focused' | 'blurred'>('blurred');

const onFocus = () => setFocusState('focused');
const onBlur = () => setFocusState('blurred');

// 使用时
const isFocused = focusState === 'focused';
const isBlurred = focusState === 'blurred';

❌ 不合理的 useEffect 更新 state

const [homeCardData, setHomeCardData] = useState<HomeCardDataType>();

// ❌ 用 useEffect 初始化 state,会造成多次 render
useEffect(() => {
  const cacheCardData = DataCacheUtil.getCache();
  if (cacheCardData) {
    setHomeCardData(cacheCardData)
  }
}, [])

// ✅ 用 useState 的初始化函数
const [homeCardData, setHomeCardData] = useState<HomeCardDataType>(() => {
  return DataCacheUtil.getCache();
});
// ❌ 可计算属性也用 state + effect
const [totalPrice, setTotalPrice] = useState(0);
useEffect(() => {
  setTotalPrice(count * unitPrice);
}, [count, unitPrice]);

// ✅ 直接计算,必要时用 useMemo
const totalPrice = count * unitPrice;
// ❌ 在 useEffect 里更新 state
const [tabIndex, setTabIndex] = useState(0);
useEffect(() => {
  // 每次关闭时重置 tab
  if (!props.visible) {
    setTabIndex(0);
  }
}, [props.visible]);

// ✅ 把更新 state 和用户操作绑定在一起,别偷懒直接用 useEffect
const onClick = () => {
  props.visibleChange(false);
  setTabIndex(0);
}
// ❌ 用 useEffect 保持 state 和 props 同步
const [selectedItem, setSelectedItem] = useState(props.selected);
useEffect(() => {
  setSelectedItem(props.selected);
}, [props.selected]);

// ✅ 删掉 selectedItem,直接用 props
// 同时 props 提供一个回调(比如 onChange)通知父组件变化
// ⚠️ 只有当你只想要一个初始值时,同步 props 才有意义
// 按照习惯,这种 props 以 initial 或 default 开头
interface Props {
  initialColor: Color;
  defaultCount: number;
}
// ❌ 同一数据在多个 state 变量之间保持同步
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState();
const [selectedId, setSelectedId] = useState();

const onSelect = (id) => {
  const item = items.find(item => item.id === id);
  setSelectedItem(item);
  setSelectedId(item.id);
}

// ✅ 只保留 id,selectedItem 动态计算(数据量大时用 useMemo)
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);

const onSelect = (id) => {
  setSelectedId(id);
}

const selectedItem = items.find(item => item.id === selectedId);

❌ 深度嵌套的 state

// ❌ 深度嵌套更新麻烦,容易出错
const [user, setUser] = useState({
  profile: {
    name: 'Tom',
    contact: {
      phone: '123',
      address: {
        city: 'Beijing',
        detail: 'xxx街道'
      }
    }
  }
});

const updateCity = (newCity) => {
  setUser({
    ...user,
    profile: {
      ...user.profile,
      contact: {
        ...user.profile.contact,
        address: {
          ...user.profile.contact.address,
          city: newCity  // 改一个城市,复制了四层对象
        }
      }
    }
  });
};

// ✅ 尽量扁平化,但也不要搞出太多 state,需要权衡(个人建议最多两层)
const [name, setName] = useState('Tom');
const [contact, setContact] = useState({
  phone: '123',
  address: {
    city: 'Beijing',
    detail: 'xxx街道'
  }
});

深入 useState

  1. useState 返回的是数组。一般直接解构使用,但当其他东西触发 render 时,useState 返回的数组对象本身是会改变的,但数组里的内容不变。

  2. 初始值用函数生成时,不要写成 useState(fn()),这会让每次 render 都执行一次 fn。应该写成 useState(() => fn())useState(fn),只在第一次 render 时调用。

  3. setState 在大部分情况下是合并执行的(包括useEffect 里),但在 Promise、setTimeout等异步函数中不会合并。在 render 期间也不会合并

export const HookTestScreen = () => {
  const [count, setCount] = useState(0);
  const [state, setState] = useState({ name: '', age: 0 });

  useEffect(() => {
    if (count === 1) {
      // 合并执行,只 render 一次
      console.log('HookTestScreen effect setState');
      setState({ name: 'name', age: 1 });
      setCount(count + 1);
    }
  }, [count]);

  const onClick = () => {
    // 合并执行,只 render 一次
    setState({ name: 'name', age: 1 });
    setCount(count + 1);
  };

  const onClick2 = () => {
    // 下面两种方式都不会合并,render 两次
    // Promise.resolve().then(() => {
    //   setState({ name: 'name', age: 1 });
    //   setCount(count + 1);
    // });
    setTimeout(() => {
      setState({ name: 'name', age: 1 });
      setCount(count + 1);
    }, 0);
  };

  if (count === 5) {
    // 不会合并,render 两次,打印两次 "HookTestScreen render"
    setState({ name: 'name', age: count });
    setCount(count + 1);
  }

  console.log('HookTestScreen render !!!');

  return (
    <View>
      <Text>count: {count}</Text>
      <Button title={'同步'} onPress={onClick} />
      <Button title={'异步'} onPress={onClick2} />
    </View>
  );
}
  1. 虚拟 DOM 位置不变的相同组件,state 会被保留下来
const [showA, setShowA] = useState(true);

// ⚠️ 无论 showA 是 true 还是 false,MyInput 内部的所有状态都会保留,不会重置
// 如果加上 key 就会重置了
let content;
if (showA) {
  content = <MyInput />;
} else {
  content = <MyInput />;
}

return (
  <View style={styles.container}>
    {content}
    <Button title="切换状态" onPress={() => setShowA(!showA)} />
  </View>
);

// ⚠️ 这样就不会保留 state
...
if (showA) {
  content = <View><MyInput /></View>;
} else {
  content = (
    <View>
      <Text>我占了第一个位置</Text>
      <MyInput />
    </View>
  );
}
...

// 那如果是这样呢,会保留吗?
const content = () => <MyInput />
// ⚠️ 答案是会保留!!!
return (
  <View style={styles.container}>
    {content()}
    <Button title="切换状态" onPress={() => setShowA(!showA)} />
  </View>
)
// ⚠️ 如果组件定义在组件内部,就算名字一样,也不会保留 state
// 每次渲染对 React 来说都是新组件
let content;
const MyInput = () => {
  return <TextInput />
}

if (showA) {
  content = <MyInput />;
} else {
  content = <MyInput />;
}

return (
  <View style={styles.container}>
    {content}
    <Button title="切换状态" onPress={() => setShowA(!showA)} />
  </View>
);

按照刚才说的"位置变了就会重置",那么下面这样会重置吗?

<View>
  {count % 2 === 0 && <Text>123</Text>}
  <MyInput />
</View>
// ⚠️ 答案是:当 count 变化时,MyInput 并不会重置
// ⚠️ 因为上面的 {...} 相当于一个槽位,不管里面怎么变都是一样的,MyInput 不会重置
// ⚠️ 就算是下面这样,也不会
{count % 2 === 0 ? (
  <>
    <Text>123</Text>
    <Text>123</Text>
  </>
) : (
  <Text>123</Text>
)}
<MyInput />

但测试中发现一个很诡异的事情,仅仅是写法不一样,内容一样,现象却不同:

let testViewList = [1, 2];
if (count % 2 === 0) {
  testViewList = [1, 2, 3, 4]
}

// 这样写不会重置
const testView2 = count % 2 === 0 ? (
  <div>
    {testViewList.map(item => <EffectTestView />)}
    <ForwardView text={state.name} ref={forwardViewRef} />
  </div>
) : (
  <div>
    {testViewList.map(item => <EffectTestView />)}
    <ForwardView text={state.name} ref={forwardViewRef} />
  </div>
)

// 这样就会重置,内容一样的,格式不一样
const testView2 = count %2 === 0 ? <div>
    {testViewList.map(item=><EffectTestView />)}
    <ForwardView text={state.name} />
</div> : <div> {testViewList.map(item=><EffectTestView />)}
    <ForwardView text={state.name}/></div>

useRef ✅

  • 返回一个 ref 对象,只有一个 current 属性,ref 在组件整个生命周期内不变。
  • 用来引用组件,执行组件的方法。
  • ⚠️ 更新 ref 的 current 不会触发组件 render。

基础使用

// 作为组件引用
const scrollRef = useRef(null);
return (<ScrollView ref={scrollRef}>...</ScrollView>)
...
// 使用
scrollRef.current.scrollToEnd()

// 保存数据,记录上一次的内容
const Input = () => {
  const [text, setText] = useState('');
  const prevRef = useRef('');

  const onBlur = () => {
    prevRef.current = text;  // 直接赋值,不触发渲染
    console.log('上次内容:', prevRef.current);
  };

  return <TextInput onChangeText={setText} onBlur={onBlur} />;
};

错误示例

❌ 用 ref 来触发渲染

const countRef = useRef(0);
const add = () => {
  // ❌ 直接修改 ref,组件不会重新渲染,界面永远显示 0
  countRef.current += 1;
};

return (
  <View>
    <Text>{countRef.current}</Text>
    <Button title="+" onPress={add} />
  </View>
);

// ✅ 需要刷新界面的数据,用 useState
const [count, setCount] = useState(0);
const add = () => {
  setCount(c => c + 1);
};

❌ useEffect 依赖 ref 对象

const countRef = useRef(0);

// ❌ 依赖 countRef,这个对象不会变,所以 useEffect 只会触发一次
useEffect(() => {
  console.log(`count = ${countRef.current}`)
}, [countRef])

// ✅ 依赖 ref.current
// ⚠️ 即便是改成 ref.current,也得是有其他 state 改变触发了 render,才会执行
useEffect(() => {
  console.log(`count = ${countRef.current}`)
}, [countRef.current])

// ✅ useEffect 里使用 ref 其实可以不写依赖,因为每次调用都是最新值
const timerRef = useRef(0);
useEffect(() => {
  const id = setInterval(() => {
    timerRef.current += 1;
    // ⚠️ 这里每次输出的值都会 +1,和 state 不一样
    console.log(`timerRef: ${timerRef.current}`)
  }, 1000);
  return () => clearInterval(id);
}, []);  // 空依赖,ref 数据实时读写

深入 useRef

  • 如果 useRef 的初始值由函数生成,每次 render 时这个函数都会被调用。
  • ⚠️ 自定义组件导出实例方法:
// 1. 定义 ref 暴露的方法类型
type ChildRef = {
  sayHello: () => void;
};

// 2. 子组件接收 props,暴露 sayHello 方法
const ChildView = forwardRef<ChildRef, { name: string }>((props, ref) => {
  useImperativeHandle(ref, () => ({
    sayHello: () => {
      console.log(`Hello, ${props.name}!`);
    },
  }), [props.name]); // ⚠️ 依赖 props.name,变了会重新生成函数;如果没有,sayHello 永远是第一次的值

  return <Text>ChildView: {props.name}</Text>;
});

// 3. 父组件声明类型化 ref 并调用
const Parent = () => {
  const childRef = useRef<ChildRef>(null);

  return (
    <View>
      <Button title="打招呼" onPress={() => childRef.current?.sayHello()} />
      <ChildView ref={childRef} name="Tom" />
    </View>
  );
};

useEffect ✅

  • 监听值变化的钩子,用来做不属于 UI 渲染的操作,比如请求数据。
  • 监听组件生命周期。

基础使用

const [count, setCount] = useState(0);

// 第二个参数传空数组,相当于生命周期
useEffect(() => {
  // 组件挂载时执行一次
  console.log('useEffect didMount')
  return () => {
    // 组件卸载时执行一次
    console.log('useEffect unMount')
  }
}, []);

// ⚠️ 不传第二个参数,组件每次 render 都会回调
useEffect(() => {
  console.log('useEffect render!!!')
})

// 监听 count 变化
useEffect(() => {
  // 组件挂载或 count 变化时执行
  console.log('start count change ' + count)
  return () => {
    // 组件卸载或 count 变化时执行
    // ⚠️ 这里的 count 是之前的值,比如 count 从 0 变成 1,这里打印的还是 0
    console.log('end count change ' + count)
  }
}, [count])

错误示例

❌ useEffect 依赖不使用的变量,或依赖对象 & 函数

  • 造成无效请求(工作中查过一个 bug,就是因为 useEffect 依赖了不需要的变量导致请求报错,不要忽视)。
  • 造成死循环:触发 useEffect → 获取数据、更新 state → 渲染 → 触发 useEffect。
// ❌ 依赖了未使用的变量 refreshFlag,变化时造成不必要的刷新
useEffect(() => {
  fetchUser(userId);
}, [userId, refreshFlag]);

// ❌ 依赖函数,每次渲染都是新引用,死循环
const handleRefresh = () => fetchList();
useEffect(() => {
  handleRefresh();
}, [handleRefresh]);

// ❌ 依赖对象,每次渲染都是新引用,死循环
const params = { page: 1, size: 20 };
useEffect(() => {
  fetchList(params);
}, [params]);

// ✅ 依赖对象里的具体值
useEffect(() => {
  fetchList({ page: params.page, size: params.size });
}, [params.page, params.size]);

❌ 用 useEffect 更新渲染期间能计算的内容

// ❌ 造成多次渲染
useEffect(() => {
  total > 0 ? setNextButtonDisabled(false) : setNextButtonDisabled(true);
}, [total]);

// ✅ 渲染期间直接计算
const buttonDisabled = total <= 0;

❌ 用 useEffect 做昂贵计算

// ❌ 多余渲染 + 逻辑分散
const [filteredList, setFilteredList] = useState([]);
useEffect(() => {
  const result = heavyFilter(rawList);  // 昂贵计算
  setFilteredList(result);
}, [rawList]);

// ✅ 用 useMemo 缓存
const filteredList = useMemo(() => heavyFilter(rawList), [rawList]);

❌ 把事件逻辑放进 useEffect

// ❌ 点击按钮改变 count,再在 useEffect 里触发获取数据
const [count, setCount] = useState(0);
useEffect(() => {
  fetchData(count);
}, [count]);

const onClick = () => {
  setCount(count + 1);
}

// ✅ 在点击事件里,刷新 UI 同时获取数据,维护性和可读性更好
// 因为获取数据可能只和点击有关,并不是 count 变了就要获取
const onClick = () => {
  const nextCount = count + 1;
  setCount(nextCount);
  fetchData(nextCount);
}

❌ 一个 Effect 干多件事

// ❌ 依赖变化时互相影响,造成不必要的请求和刷新
useEffect(() => {
  subscribeToRoom(roomId);      // 订阅房间
  updateUserStatus(userId);     // 更新状态
}, [roomId, userId]);

// ✅ 拆成独立 useEffect,每个只负责一种
useEffect(() => {
  subscribeToRoom(roomId);
  return () => unsubscribeFromRoom(roomId);
}, [roomId]);

useEffect(() => {
  updateUserStatus(userId);
}, [userId]);

❌ 多个 useEffect 连环更新 state

const [count, setCount] = useState(1);
const [total, setTotal] = useState(0);
const [bonus, setBonus] = useState(0);

// ❌ 连环套:count 变 → total 变 → bonus 变,渲染多次
useEffect(() => {
  setTotal(count * 10);
}, [count]);

useEffect(() => {
  setBonus(total * 0.2);  // 等 total 更新完才执行
}, [total]);

// ✅ 在事件处理函数中一次统一执行,setState 会合并,只刷新一次
const add = () => {
  const nextCount = count + 1;
  const nextTotal = nextCount * 10;
  const nextBonus = nextTotal * 0.2;

  setCount(nextCount);
  setTotal(nextTotal);
  setBonus(nextBonus);
};

❌ useEffect 里用定时器读取 state

const [count, setCount] = useState(0);
useEffect(() => {
  const timer = setInterval(() => {
    // ⚠️ 这里永远打印 0,不会变
    console.log(count);
    // ⚠️ 这里也是,永远只会设置成 1
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(timer);
}, []);
  • ✅ 如果一定要在定时器里读取值,用 useRef 保存。
  • ✅ 如果一定要在定时器里更新值,用函数式更新 setCount(c => c + 1)

❌ 获取数据不处理竞态

// ❌ 挂载时获取数据,快速切换 id 时,由于网络原因,可能新请求的结果还没返回,旧请求先返回了
// 导致最后是旧请求的数据
useEffect(() => {
  fetchUser(id).then(res => {
    setUser(res);  // 如果 id 已变,这是脏数据!
  });
}, [id]);

// ✅ 增加判断丢弃过期结果
useEffect(() => {
  let cancelled = false;
  fetchUser(id).then(res => {
    if (!cancelled) setUser(res);
  });
  return () => { cancelled = true; };
}, [id]);

❌ prop 变化时用 useEffect 重置 state

const [page, setPage] = useState(1);

// ❌ prop 变化后异步重置
useEffect(() => {
  setPage(1);
}, [props.type]);

// ✅ 渲染期间处理,render 期间的 setState 会立即触发,提升效率
const prevType = useRef(props.type);
const isTypeChange = props.type !== prevType.current;
if (isTypeChange) {
  prevType.current = props.type;
  setPage(1);
}

❌ 用 useEffect 或内部状态重置整个组件

const [form, setForm] = useState({ name: '', age: '' });

// ❌ 手动逐个重置,容易遗漏,不好维护,以后加了新 state 也容易忘
useEffect(() => {
  setForm({ name: '', age: '' });
  // 其他 state 重置...
}, [props.userId]);

// ✅ 使用组件时传入 key,key 变化会创建新的组件实例,所有 state 自动重置
<UserForm key={userId} userId={userId} />

❌ 用 useEffect 回调数据给父组件,再同步到兄弟组件

// ❌ A 组件通知父组件,父组件再同步给 B
const [value, setValue] = useState('');
useEffect(() => {
  // 比较好的做法是不要在 useEffect 里执行 props 回调,而是在事件处理函数里
  props.onChange(value);
}, [value]);

// ❌ B 组件
useEffect(() => {
  setValue(propValue);  // 又从 props 同步回来
}, [propValue]);

// ✅ 将状态提升到父组件,父组件统一管理
const [value, setValue] = useState('');
<A value={value} onChange={setValue} />
<B value={value} onChange={setValue} />

深入 useEffect

  1. ⚠️ useEffect 的宗旨:少用,能不用尽量不用。

  2. 多个 useEffect 按照代码顺序依次执行。

  3. useEffect 触发 setState 时,是整个 render 完了再触发,多个 setState 会合并刷新

  4. 如果一次 render 中触发多个 useEffect 里的 setState,也只造成一次 render

  5. React 19 新 API useEffectEvent 来优化 useEffect。

    • 非响应式的事件函数,总是可以获取最新值,只能在 useEffect 里调用。
    • 可以解决 useEffect 里获取不到最新值的问题,或者只想在组件挂载时获取一次数据。
    const [count, setCount] = useState(0);
    
    const onTick = useEffectEvent(() => {
      console.log(count);  // 永远读到最新 count!
    });
    
    // useEffect 里调用它,依赖数组不需要放 count,也不用依赖 onTick
    useEffect(() => {
      const timer = setInterval(() => {
        onTick();  // 调用事件,读取最新值
      }, 1000);
      return () => clearInterval(timer);
    }, []);  // ✅ 空依赖,定时器只创建一次,但值永远是最新的
    
    // 只想在挂载时获取一次数据
    const [user, setUser] = useState(null);
    const onFetch = useEffectEvent(() => {
      fetchUser(userId).then(setUser);
    });
    
    useEffect(() => {
      onFetch();
    }, []);  // ✅ 空依赖,只在挂载时执行一次
    

useCallback ✅

用来缓存一个函数,主要是配合 memo 将方法传入子组件时,避免子组件 props 变化触发刷新

基本用法

const [count, setCount] = useState(0);

// ✅ 只在第一次渲染时创建
const onChildCallback = useCallback(() => {
  console.log('onChildCallback');
}, []);

// ✅ 当 count 改变时,重新创建
const onChildCountCallback = useCallback(() => {
  console.log(`onChildCallback ${count}`);
}, [count]);

// ❌ 完全不写数组,每次渲染都创建新的,相当于没用
const onChildEveryCallback = useCallback(() => {
  console.log('onChildEveryCallback');
});

const ChildMemo = memo(({ onPress }) => {
  console.log('Child render');
  return <TouchableOpacity onPress={onPress}>Click</TouchableOpacity>;
});

// ✅ 点击 Button,父组件 render,但子组件不会重新 render
<View>
  <Button title="add Count" onPress={() => setCount(c => c + 1)} />
  <ChildMemo onPress={onChildCallback} />
</View>

错误示例

❌ 和 state 无关的纯函数用 useCallback

// ❌ 生成唯一请求 ID,不需要 useCallback
const generateRequestId = useCallback(() => {
  return `requestId_${Date.now()}`;
}, []);

// ✅ 移出组件,放到文件顶层
const generateRequestId = () => `requestId_${Date.now()}`;

❌ 缓存的函数使用了 state,却没有依赖

// ❌ 依赖数组没有 count,之后 count 变了,onChildCountCallback 输出还是初始值
const onChildCountCallback = useCallback(() => {
  console.log(`onChildCallback ${count}`);
}, []);

// ✅ 加上 count 依赖
const onChildCountCallback = useCallback(() => {
  console.log(`onChildCallback ${count}`);
}, [count]);

❌ 子组件没 memo,或有别的 props 没缓存

const Parent = () => {
  const [count, setCount] = useState(0);

  // ✅ handlePress 缓存了
  const handlePress = useCallback(() => {
    console.log('press');
  }, []);

  // ❌ style 每次渲染都是新对象
  const cardStyle = { backgroundColor: '#fff', padding: 16 };

  return (
    <View>
      <Button title="add count" onPress={() => setCount(c => c + 1)} />
      {/* ❌ 子组件没 memo,缓存函数没用,每次还是重新渲染 */}
      <ChildA onPress={handlePress} />
      {/* ❌ 子组件 memo 了,但 style 没缓存,仍然每次重新渲染 */}
      <ChildMemo style={cardStyle} onPress={handlePress} />
    </View>
  );
};

// ✅ 缓存 ChildA,style 提取到组件外部,用 StyleSheet 创建
const ChildAMemo = memo(ChildA);
const styles = StyleSheet.create({
  card: {
    backgroundColor: '#fff',
    padding: 16,
  },
});

// 使用
<View>
  <Button title="add count" onPress={() => setCount(c => c + 1)} />
  <ChildAMemo onPress={handlePress} />
  <ChildMemo style={styles.card} onPress={handlePress} />
</View>

❌ 只在 useEffect 里使用的函数,却用 useCallback 缓存

// ❌ 函数内部都是对 ref 操作,不需要缓存
const clearRequest = useCallback(() => {
  requestId.current = null;
  active.current = false;
}, []);

const onSuccess = useCallback(() => {
  console.log("onSuccess");
  clearRequest();
}, [clearRequest]);

const onError = useCallback((event) => {
  console.log("onError");
  clearRequest();
}, [clearRequest]);

const onCancel = useCallback(() => {
  console.log("onCancel");
  clearRequest();
}, [clearRequest]);

// ❌ 三个函数都只在 useEffect 里使用,却额外缓存,造成连续依赖,降低可读性
useEffect(() => {
  const s = emitter.addListener('success', onSuccess);
  const e = emitter.addListener('error', onError);
  const c = emitter.addListener('cancel', onCancel);
  return () => {
    clearRequest();
    s.remove();
    e.remove();
    c.remove();
  };
}, [onSuccess, onError, onCancel]);
  • 修改:将函数直接写在 useEffect 里
useEffect(() => {
  // ✅ 函数直接定义在 useEffect 里
  const clearRequest = () => {
    requestId.current = null;
    active.current = false;
  };

  const onSuccess = () => {
    console.log("onSuccess");
    clearRequest();
  };

  const onError = () => {
    console.log("onError");
    clearRequest();
  };

  const onCancel = () => {
    console.log("onCancel");
    clearRequest();
  };

  const s = emitter.addListener('success', onSuccess);
  const e = emitter.addListener('error', onError);
  const c = emitter.addListener('cancel', onCancel);

  return () => {
    clearRequest();
    s.remove();
    e.remove();
    c.remove();
  };
}, []);

⚠️ 假设其他地方也用了 clearRequest,需要把它提出来:

const clearRequest = () => {
  requestId.current = null;
  active.current = false;
};

// 这里个人建议是,useEffect 还是正常使用 clearRequest,不需要加依赖
// 因为 clearRequest 里都是对 ref 操作,没有闭包问题,都是最新值
// ⚠️ 但这样 eslint 会报警告,可以 ignore 掉,或者用 React 19 的 useEffectEvent
useEffect(() => {
  // ...
}, [])

深入 useCallback

  • 只有在确保子组件是 memo 的,并且要传递函数给子组件时才使用。
  • 并不会加快第一次渲染的速度,用得多反而造成性能开销。

useMemo ✅

  • 缓存对象,避免重复的昂贵计算。
  • 缓存后传递给子组件,避免子组件无效渲染。
    • 其实也可以缓存函数代替 useCallback,但习惯上缓存函数还是用 useCallback。
    • 达成优化的唯一前提:组件被 memo,并且所有 props 都被缓存。

基本用法

// 缓存复杂计算结果
const result = useMemo(() => {
  // 使用 data 进行复杂计算,耗时长
  ...
}, [data])

// 缓存 props,避免子组件重渲染
const Parent = () => {
  const [count, setCount] = useState(0);
  // 缓存 config,避免点击按钮重新渲染 ChildMemo
  const config = useMemo(() => ({ theme: 'dark', showHeader: true }), []);

  return (
    <View>
      <Button title="add count" onPress={() => {
        console.log(`count = ${count}`);
        setCount(c => c + 1);
      }} />
      <ChildMemo config={config} />
    </View>
  );
};

错误示例

❌ useMemo 缓存简单计算

const [count, setCount] = useState(0);

// ❌ 简单计算,useMemo 开销比计算还大,并且降低可读性
const double = useMemo(() => count * 2, [count]);

// ✅ render 时动态计算
const double = count * 2;

// ❌ 简单计算
const visible = useMemo(() => {
  return modalInfo?.key === KEY_VISIBLE;
}, [modalInfo]);

// ✅ 直接计算
const visible = modalInfo?.key === KEY_VISIBLE;

// ❌ 无意义的 useMemo
const extra = useMemo(() => {
  return modalInfo?.extra;
}, [modalInfo]);

// ✅ 去掉 useMemo
const extra = modalInfo?.extra;

❌ useMemo 的依赖项每次渲染都变

const MonthView = () => {
  const months = ['1', '2', '3'];

  // ❌ months 每次渲染都会重新生成,是新对象,useMemo 每次都重新执行,没起到缓存作用
  const monthData = useMemo(() => {
    return months.map((m, i) => ({ label: m, value: i }));
  }, [months]);

  // ✅ months 移到 useMemo 里面,或者放在组件外
  const monthData = useMemo(() => {
    const months = ['1', '2', '3'];
    return months.map((m, i) => ({ label: m, value: i }));
  }, []);
  ...
}

深入 useMemo

  • useMemo 的宗旨和 useEffect 一样:少用,能不用尽量不用,大概率是负优化。
    • 只有计算超过 1ms 的,才有缓存的意义。
  • 不能提升首次渲染性能,甚至多了会有负面影响(降低首次渲染速度),只影响后续重渲染。

memo

准确说不算 hook,用来缓存复杂组件,减少不必要的渲染,提高性能。

基本用法

const Child = memo(({ count }: { count: number }) => {
  console.log('Child render');
  return <Text>{count}</Text>;
});

const Parent = () => {
  const [count, setCount] = useState(0);
  const [other, setOther] = useState(0);

  // 只有点击第一个 button,才会触发 Child 重新绘制
  return (
    <View>
      <Button title="改 count" onPress={() => setCount(c => c + 1)} />
      <Button title="改 other" onPress={() => setOther(o => o + 1)} />
      <Child count={count} />
    </View>
  );
};

// ⚠️ 自定义比较
type UserCardProps = {
  user: { id: number; name: string; age: number };
  onPress: () => void;
};

const UserCard = memo(({ user, onPress }: UserCardProps) => {
  console.log('UserCard render');
  return (
    <View onTouchEnd={onPress}>
      <Text>{user.name} (age: {user.age})</Text>
    </View>
  );
}, (prev, next) => {
  // 返回 true 表示 props 没变,不刷新;返回 false 刷新
  // ⚠️ 这里示例只判断 user 的 id 和 name,相等时不刷新 UserCard,忽略 age 和 onPress
  return prev.user.id === next.user.id && prev.user.name === next.user.name;
});

错误示例

❌ memo 的 props 有动态值,导致 memo 失效

const Parent = () => {
  const [count, setCount] = useState(0);

  // ❌ 每次渲染都是新函数,memo 无效,每次点击 ChildMemo 都会刷新
  const handlePress = () => {
    console.log('pressed');
  };

  // ✅ 用 useCallback 缓存
  const handlePress = useCallback(() => {
    console.log('pressed');
  }, []);

  const tags = useMemo(() => ['vip', 'new'], []);

  return (
    <View>
      <Button title="addCount" onPress={() => setCount(c => c + 1)} />
      <ChildMemo onPress={handlePress} tags={tags} />
    </View>
  );
};

❌ memo 的自定义比较写反,导致不刷新

const ListItem = memo(({ item, onSelect }: { item: Item; onSelect: (id: string) => void }) => {
  return (
    <TouchableOpacity onPress={() => onSelect(item.id)}>
      <Text>{item.name}</Text>
      <Text>{item.status}</Text>
    </TouchableOpacity>
  );
}, (prev, next) => {
  // ❌ 1. 刷新条件写反:id、status 不同反而不刷新(返回 true 时不刷新)
  // ❌ 2. 未判断 name,name 变了也不刷新
  return prev.item.id !== next.item.id || prev.item.status !== next.item.status;

  // ✅ 全部相等返回 true,不刷新。⚠️ onPress 这里没加上,实际中也要注意
  return prev.item.id === next.item.id
    && prev.item.name === next.item.name
    && prev.item.status === next.item.status;
});

❌ children 传入 memo 组件导致失效

const Parent = () => {
  const [count, setCount] = useState(0);
  const onClick = useCallback(() => {
    setCount(c => c + 1)
  }, []);

  return (
    // ❌ 每次 onClick 后,MemoItem 还是会触发 render
    // ⚠️ MemoChild 对于 MemoItem 来说,相当于一个内置的 props(children=<MemoChild/>)
    // ⚠️ <MemoChild /> 等价于 React.createElement(MemoChild),每次渲染都是新对象
    <MemoItem onClick={onClick}>
      <MemoChild />
    </MemoItem>
  );

  // ✅ 将 MemoChild 生成的元素缓存起来
  // ⚠️ MemoChild 是针对自己不变化,被 useMemo 后是为了让 MemoItem 不变化
  const MemoChild2 = useMemo(() => <MemoChild />, []);
  return (
    <MemoItem onClick={onClick}>
      <MemoChild2 />
    </MemoItem>
  )
};

深入 memo

  • memo 返回的是高阶组件,本质还是组件,每次调用还是会创建出新组件,只是里面不变

  • export default memo(UserPage); 如果组件导出当成 screen,不需要加 memo,因为 react-native-navigation 框架会自动缓存。

  • 最新的 React 编译器支持编译时自动 memo 和 useMemo,不再需要手写。

  • 不用 useMemo 和 memo 避免子组件被其他 state 影响而刷新的方案(提供思路,实际需要时还是直接 memo):

    状态下移:

    // ❌ 父组件有多个 state,任一变化都导致整个列表刷新
    const Parent = () => {
      const [list, setList] = useState([]);
      const [input, setInput] = useState('');  // 输入框变化,列表跟着刷新!
    
      return (
        <View>
          <TextInput value={input} onChangeText={setInput} />
          {list.map(item => <Item key={item.id} data={item} />)}
        </View>
      );
    };
    
    // ✅ 找出和其他组件无关的 state,封装到子组件里
    const SearchInput = () => {
      const [input, setInput] = useState('');  // 状态在子组件内部
      return <TextInput value={input} onChangeText={setInput} />;
    };
    
    const Parent = () => {
      const [list, setList] = useState([]);
      // ⚠️ input 不影响 list 了
      return (
        <View>
          <SearchInput />
          {list.map(item => <Item key={item.id} data={item} />)}
        </View>
      );
    };
    

    状态提升:

    const Parent = () => {
      // ❌ 父 View 有和 A、B 无关的 state,但改变时 A、B 也会跟着渲染
      const [count, setCount] = useState(0);
      const [modalVisible, setModalVisible] = useState(false);
    
      return (
        <View>
          <Button title="打开弹窗" onPress={() => setModalVisible(true)} />
          <Modal visible={modalVisible} onClose={() => setModalVisible(false)} />
          <A />
          <B />
        </View>
      );
    };
    
    // ✅ 提取成独立组件,state 封装进去
    const ModalWrapper = ({ children }: { children: React.ReactNode }) => {
      const [visible, setVisible] = useState(false);
    
      return (
        <View>
          <Button title="打开弹窗" onPress={() => setVisible(true)} />
          <Modal visible={visible} onClose={() => setVisible(false)} />
          {children}
        </View>
      );
    };
    
    // ⚠️ 父组件刷新,内部子组件也会刷新,但如果是当成 children 传进来的,就不影响
    // 这里弹窗更新,不会影响 A 和 B
    const Parent = () => (
      <ModalWrapper>
        <A />
        <B />
      </ModalWrapper>
    );
    

useContext ✅

提供全局状态或者跨组件共享数据。

基本用法

  1. 创建 Context
// 类型定义直接取 useState 的返回值类型,因为后面给 Context 赋值就是 useState 的返回值
const TestContext = React.createContext<{
  contextText: ReturnType<typeof useState<string>>[0];
  setContextText: ReturnType<typeof useState<string>>[1];
}>({ contextText: '', setContextText: () => {} });
  1. 创建 Provider
const [contextText, setContextText] = useState<string>();

// ⚠️ 使用 useMemo 主要是防止其他 state 变化导致 value 变化,从而使用到 context 的地方造成无效重绘
const contextValue = useMemo(
  () => ({ contextText, setContextText }),
  [contextText, setContextText],
);

<TestContext.Provider value={contextValue}>
  <ChildContextMemoView />
</TestContext.Provider>
  1. 子组件使用 context
const ChildContextMemoView = memo(() => {
  const { contextText, setContextText } = useContext(TestContext);
  return (
    <View>
      <Text>{`我是 context 测试子组件,context text: ${contextText}`}</Text>
      <Button
        text={'子组件改变 context 的值'}
        onPress={() => {
          setContextText(`点击时间:${new Date().toLocaleTimeString()}`);
        }}
      />
    </View>
  );
});

错误示例

❌ context 的 value 使用动态对象

// ❌ 每次 render 都会生成新对象,造成所有消费处重绘
<RadioContext.Provider value={{ checkedItem, setCheckedItem }}>
  <View>{children}</View>
</RadioContext.Provider>

// ✅ 使用 useMemo 缓存
const contextValue = useMemo(() => {
  return { checkedItem, setCheckedItem };
}, [checkedItem, setCheckedItem]);

<RadioContext.Provider value={contextValue}>
  <View>{children}</View>
</RadioContext.Provider>

深入 useContext

  • 使用 createContext 创建的同一个 context 给多个页面用,互相不会影响(value 不同)。
  • 当 Provider 的 value 变化时,会触发所有 useContext 的子组件 render,不论是否使用了 context 的值。并且会绕过 memo 的检测
    • 被触发重绘会使其子组件重绘,不论子组件是否使用了 useContext。用 memo 包裹子组件就不会重绘。
    • 子组件使用 useContext 导致触发重绘,不会影响父组件。
  • 有一种场景:组件只需要修改 context 并不需要它的值。那它的值改变造成重绘就是一种浪费,可以优化。
    • 使用两个 context 分别提供值和修改函数。
    • 具体放到另一篇文章里:《ReactNative 总结系列三 — 性能优化》

总结

  1. 打开 eslint 的 hook 依赖项检测,出现告警及时修复,可以极大程度降低 hook 可能出现的闭包问题。

  2. hook 的依赖数组中尽量不要出现对象,最好是对象里的实际属性。

  3. hook 凡是有依赖项的(useEffect、useCallback、useMemo 等),每次组件渲染都会生成一遍函数,然后再去对比依赖数组里的项,不同才执行或者返回新的对象。

  4. 使用 useMemo、useCallback 并不能提升第一次渲染的速度,甚至对于第一次渲染还会有额外的性能开销(内存占用,数量大后会造成可见的延迟)。

  5. 滥用 hook 缓存并不能提升性能,反而会增加开销,降低代码的可读性和可维护性。有针对性的合理使用 hook 才能真正发挥其优化价值。


参考文档