ReactNative状态管理

7,440 阅读13分钟

一、什么是状态管理

某个行为需要更新页面里某个数据,继而触发UI的更新,那么就需要用到状态管理。先举一个简单的例子,可以使用按钮进行count的加/减/重置操作。

复杂一点的例如购物车中,底部栏的全选按钮和列表中的勾选框需要做联动。当全选状态改变的时候,需要更新列表中所有勾选框的选中状态。

_originA3AABD7A-2F3F-4B5D-9F79-66338DE40FC3_RPReplay_Final1683635835_AdobeExpress.gif

二、我们如何管理状态?

2.1 官方提供的状态管理

React Hook功能正式发布之后,允许在function component中拥有state和副作用。useReducer和useState就是官方提供的两种state管理的hook。

2.1.1 useState

最简单的useState可以实现组件的更新,示例代码:

const A = () => {
    useEffect(() => {
        console.log('A render');
    });

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

    const increase = useCallback(() => {
        setCount(count + 1);
    }, [count, setCount]);

    const decrease = useCallback(() => {
        setCount(count - 1);
    }, [count, setCount]);

    const reset = useCallback(() => {
        setCount(0);
    }, [setCount]);

    return (
        <View style={styles.container}>
            <Image
                style={styles.image}
                source={require('../images/logo.png')}
            />
            // 将count值传递给子组件
            <B count={count} />
            <View style={styles.buttonContainer}>
                <TouchableOpacity style={styles.button} onPress={increase}>
                    <Text style={styles.buttonText}>increase</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={decrease}
                >
                    <Text style={styles.buttonText}>decrease</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={reset}
                >
                    <Text style={styles.buttonText}>reset</Text>
                </TouchableOpacity>
            </View>
        </View>
    );
};

interface IProps {
    count: number;
}

const B = (props: IProps) => {
    useEffect(() => {
        console.log('B render');
    });

    return <Text style={styles.text}>{`count : ${props.count}`}</Text>;
};

但是存在一些可见预见的问题:

1、当我们的层级变多,可能会存在状态层层传递的问题

2、几乎所有可变的状态都需要在父组件中进行维护。会增加父组件的复杂性。

这里借用Redux的一张图,当Component层级多,我们的更新操作很可能会变成下图左侧的情况。

很自然的我们会想到,那么是不是可以像图片右侧一样,提供一个独立的Store专门用来进行状态相关的交互,那么官方也提供了解决的方案,咱们继续往下看。

2.1.2 useContext+useReducer

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但此种用法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

实现状态管理步骤

1、定义Reducer,用于承载state的更新操作

2、创建Context,将需要用到Context的组件用Provider包裹起来

3、使用const { state, dispatch } = useContext(CounterContext)获取state中所有内容

4、使用dispatch({ type: '动作' })进行状态更新

示例代码

const initialState = {
    count: 0,
};
// 1、定义Reducer,用于承载state的更新操作
const reducer = (state, action) => {
    // payload中可以获取到dispatch时携带的参数
    const { type, payload } = action;
    switch (type) {
        case 'increase':
            return { count: state.count + 1 };
        case 'decrease':
            return { count: state.count - 1 };
        case 'increaseCount':
            return {
                ...state,
                count: state.count + payload.count,
            };
        case 'reset':
            return { count: 0 };
        default:
            throw new Error();
    }
};

// 2、创建Context
const CounterContext = React.createContext(null);
// UI部分
const App = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
      	// 2、创建Context,将需要用到Context的组件用Provider包裹起来
        <CounterContext.Provider value={{ state, dispatch }}>
            <A />
        </CounterContext.Provider>
    );
};

const A = () => {
    return (
        <>
            <B />
        </>
    );
};

const B = () => {
    // 3、获取state中内容
    const { state, dispatch } = useContext(CounterContext);

    const increase = useCallback(() => {
      	// 4、更新状态
        dispatch({ type: 'increase' });
    }, [dispatch]);
  
  	// payload中传递参数
  	const increase2 = useCallback(() => {
        dispatch({ type: 'increaseCount', payload: { count: 2 } });
    }, [dispatch]);

    const decrease = useCallback(() => {
        dispatch({ type: 'decrease' });
    }, [dispatch]);

    const reset = useCallback(() => {
        dispatch({ type: 'reset' });
    }, [dispatch]);

    return (
        <View style={styles.container}>
            <Image
                style={styles.image}
                source={require('../images/logo.png')}
            />
            <Text style={styles.text}>{`count : ${state.count}`}</Text>
            <View style={styles.buttonContainer}>
                <TouchableOpacity style={styles.button} onPress={increase}>
                    <Text style={styles.buttonText}>increase</Text>
                </TouchableOpacity>
              	<TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={increase2}
                >
                    <Text style={styles.buttonText}>increase 2</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={decrease}
                >
                    <Text style={styles.buttonText}>decrease</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={reset}
                >
                    <Text style={styles.buttonText}>reset</Text>
                </TouchableOpacity>
            </View>
        </View>
    );
};

export default App;
小结

看起来实现还算简单,但是useContext+useReducer也存在一些问题:

1、会造成页面刷新频繁,当数据更新被Provider包裹的组件都会重新渲染。通常需要拆分context或者结合useMemo来减少组件渲染的次数。对开发者来说,有一定的成本。示例

2、不支持异步,需要自己实现。

3、对中间件的支持不友好,开发过程中更新状态前会有一些额外操作,这些功能都需要自己实现,在复杂场景中会增加工作量。

2.2 社区常用的状态管理框架

要解决上面提出的几个问题,首先可以想到的是看看社区里有没有比较成熟的状态管理框架。这里挑选了三个比较有代表性的,分别是:Redux、Mobx、Zustand

数据直达:npmtrends.com/mobx-vs-red…

2.2.1 各大公司以及团队现状

作者了解到各大公司基本还是以Redux为主,或者是Redux和其他框架的组合;只有极少数使用其他框架作为标准。

2.2.2 Mobx

MobX是一个简单的状态管理解决方案。关键词:面向对象、响应式、简单易用。个人感觉跟函数式的思想不太吻合,并且state可以随意改变,可能会引起一些意料之外的组件更新。下面列举了简单的使用代码,感兴趣可以了解下。

实现状态管理步骤

1、定义并且创建Store,用于承载state和它的一系列更新操作

2、使用Provider包裹组件,使得全局可使用Store对象

3、使用observer创建组件,将组件变成响应式组件

4、调用Store中定义的更新操作,完成更新动作

示例代码
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react-lite';

class Store {
    constructor() {
        makeAutoObservable(this); 
    }
    count = 0;
    increase() {
        this.count++;
    }
    decrease() {
        this.count--;
    }
    reset() {
        this.count = 0;
    }
}

const store = new Store();
const CounterContext = React.createContext<Store>(null);

const Mobx = () => {
    return (
        <CounterContext.Provider value={store}>
            <A />
        </CounterContext.Provider>
    );
};

const A = () => {
    useEffect(() => {
        console.log('A render');
    });
    return <B />;
};

const B = observer(() => { 
    useEffect(() => {
        console.log('B render');
    });

    const myStore = useContext(CounterContext);

    const increase = useCallback(() => {
        myStore.increase();
    }, [myStore]);

    const decrease = useCallback(() => {
        myStore.decrease();
    }, [myStore]);

    const reset = useCallback(() => {
        myStore.reset();
    }, [myStore]);

    return (
        <View style={styles.container}>
            <Image
                style={styles.image}
                source={require('../images/logo.png')}
            />
            <Text style={styles.text}>{`count : ${myStore.count}`}</Text>
            <View style={styles.buttonContainer}>
                <TouchableOpacity style={styles.button} onPress={increase}>
                    <Text style={styles.buttonText}>increase</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={decrease}
                >
                    <Text style={styles.buttonText}>decrease</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={reset}
                >
                    <Text style={styles.buttonText}>reset</Text>
                </TouchableOpacity>
            </View>
        </View>
    );
});

export default Mobx;

2.2.3 Redux Toolkit

redux虽然很强大,但他经常被人吐槽的一些点就是"配置 Redux store 过于复杂"、"我必须添加很多软件包才能开始使用 Redux"、"Redux 有太多样板代码"。Redux Toolkit就是为了解决上述的一系列问题。Redux官方强烈建议使用Toolkit,所以我们就重点关注Toolkit与其他框架的对比。

基本原理

Redux使用单一不可变状态树来管理整个应用程序的状态。这个状态树被称为store,它维护了应用程序的所有状态。Redux通过三个核心概念实现状态管理:actions、reducers和store。

  • Actions:描述了事件的发生,包含一个type属性和payload(数据)属性。当应用程序状态需要更新时,必须调度一个action。
  • Reducers:纯函数,用于处理actions并更新store中的状态。Reducer接收当前的state和action作为参数,并返回一个新的state。
  • Store:表示整个应用程序的状态,并提供了一些方法,可以用于派发actions和访问当前状态。

React Native Redux将store提供给React组件,可以通过connect函数连接组件和store,以便在组件中访问store中的状态,并将状态更改操作映射到actions。这样,组件可以响应用户交互并更新应用程序状态。

状态如何流转

  1. 组件调用操作(action)并将其分派(dispatch)到Redux store
  2. Store更新状态(state)
  3. Store通知所有订阅(store subscribers)状态(state)已更改
  4. 组件检索新状态(state),更新UI视图

这个过程可以反复循环,以实现数据的双向绑定和动态刷新。

实现状态管理步骤

1、定义并且创建Store,用于承载state和它的一系列更新操作

2、使用Provider包裹组件,使得全局可使用Store对象

3、定义selector,可能会有很多个selector。建议尽量小的定义selector,否则会造成一些不必要的刷新

4、调用Store中定义的更新操作,完成更新动作

示例代码
import { createSlice } from '@reduxjs/toolkit';
import { configureStore } from '@reduxjs/toolkit';

interface CounterState {
    value: number;
}

const initialState: CounterState = {
    value: 0,
};

// 定义Slice,里面包含了Reducers
// 使用Redux Toolkit不需要再繁琐的定义Actions
// 而是直接dispatch reducer中定义的方法即可,可以减少我们的模板代码
export const counterSlice = createSlice({
    name: 'counter',
    initialState,
    reducers: {
        increment: (state) => {
            state.value += 1;
        },
        decrement: (state) => {
            state.value -= 1;
        },
        reset: (state) => {
            state.value = 0;
        },
    },
});

// 1、定义并且创建Store,用于承载state和它的一系列更新操作
export const store = configureStore({
    reducer: {
        counter: counterSlice.reducer,
    },
});

export const { increment, decrement, reset } = counterSlice.actions;

export type RootState = ReturnType<typeof store.getState>;
// 3、定义selector,可能会有很多个selector
export const selectCount = (state: RootState) => state.counter.value;

export default counterSlice.reducer;

const App = () => {
    // 2、使用Provider包裹组件,使得全局可使用Store对象
    // 注意Provider以外无法使用redux的各种方法,会报错
    return (
        <Provider store={store}>
            <A />
        </Provider>
    );
};

const A = () => {
    useEffect(() => {
        console.log('A render');
    });
    return <B />;
};

const B = () => {
    useEffect(() => {
        console.log('B render');
    });

    // 获取store中的值
    const count = useSelector(selectCount);
    const dispatch = useDispatch();
    // 4、调用Store中定义的更新操作,完成更新动作
    const increase = useCallback(() => {
        dispatch(increment());
    }, [dispatch]);

    const decrease = useCallback(() => {
        dispatch(decrement());
    }, [dispatch]);

    const resetData = useCallback(() => {
        dispatch(reset());
    }, [dispatch]);

    return (
        <View style={styles.container}>
            <Image
                style={styles.image}
                source={require('../images/logo.png')}
            />
            <Text style={styles.text}>{`count : ${count}`}</Text>
            <View style={styles.buttonContainer}>
                <TouchableOpacity style={styles.button} onPress={increase}>
                    <Text style={styles.buttonText}>increase</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={decrease}
                >
                    <Text style={styles.buttonText}>decrease</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={resetData}
                >
                    <Text style={styles.buttonText}>reset</Text>
                </TouchableOpacity>
            </View>
        </View>
    );
};
异步和中间件

可以使用redux-thunk中间件轻松实现异步。原本redux中执行每个action都需要即时返回一个新的状态,而thunk相当于是在中间加了一层,阻断了原本action的dispatch,并且支持将dispatch作为参数默认放到异步的方法中,可以让使用者在thunk中完成异步操作后,在合适的时机再继续之前的dispatch操作。

示例代码

import thunk from 'redux-thunk';

const store = configureStore({
    reducer: {
        reducer: countReducer,
        ...,
    },
    // 1、配置store,同时添加thunk中间件
    middleware: [thunk],
});

export const counterSlice = createSlice({
    name: 'counter',
    initialState,
    reducers: {
        fetchDataSuccess: (state, { payload }) => {
            const { payloadData, isFirst } = payload;
            // 更新state中data
            state.data = payloadData;
        },
        finishLoading: (state) => {
            ...
        },
    },
});

// 2、定义异步函数
export const fetchData = (params): AppThunk =>
    async (dispatch) => {
        fetch('https://reactnative.dev/movies.json')
          .then((response) => response.json())
          .then((json) => {
                  // 3、合适的时机dispatch更新状态的action
                  dispatch(
                      fetchDataSuccess({ payloadData: json, isFirst }),
                  );
              })
          .catch((error) => {
                  dispatch(finishLoading());
                  console.log(
                      `请求数据error: ${isFirst} ${JSON.stringify(error)}`,
                  );
              });
};

export default counterSlice.reducer;

// 4、使用异步函数
dispatch(fetchData(params));

2.2.4 Zustand

Zustand是2019年才发布的,但是很受社区欢迎,它是 2021 年 Star 增长最快的 React 状态管理库。

"Zustand" 就是德语里的"State"。

基本原理

Zustand将状态管理中核心的几个要素统一包装成一个对象api后导出,然后将api与useSyncExternalStoreWithSelector进行对接,最终返回一个Selector Hook。状态管理中核心的几个要素:

  • 状态保存在哪里(state)

  • 如何更新状态(setState)

  • 如何获取状态(getState)

  • 如何订阅状态(subscribe)

Zustand的核心依赖React新特性useSyncExternalStoreWithSelector。这个新的hook可以在React中方便的获取外部存储的内容,并且自带Selector优化。

什么叫 “带 Selector 优化” 呢?这是React 提供基于 Selector 的优化范式,声明了一种 state => Selection 的选择器函数,然后在 React 内部机制内针对选择器返回的取值进行优化。说白了就是可以细粒度的监听和更新状态,不会造成无关组件的多次更新。

实现状态管理步骤

1、定义并且创建Store,用于承载state和它的一系列更新操作

2、获取store中对应的值;调用Store中定义的更新操作,完成更新动作

3、调用Store中定义的更新操作,完成更新动作

示例代码

import { create } from 'zustand';

type Count = {
    count: number;
    increase: () => void;
    decrease: () => void;
    reset: () => void;
};

// 1、定义并且创建Store,用于承载state和它的一系列更新操作
const useCountStore = create<Count>((set) => ({
    count: 0,
    increase: () => set((state) => ({ count: state.count + 1 })),
    decrease: () => set((state) => ({ count: state.count - 1 })),
    reset: () => set({ count: 0 }),
}));

const App = () => {
    return <A />;
};

const A = () => {
    // 这种写法可能会导致组件频繁更新,store中只有要有state更新就会触发渲染
    // const count = useCountStore().count;
  	
    // 浅差异方式作为更新检查规则,nuts or honey更新则触发渲染
    // const { nuts, honey } = useBearStore((state) => ({ nuts: state.nuts, honey: state.honey }),shallow);
  
    // 通过自定义的更新检查规则进行更新检查
    // const count = useCountStore((state) => state.count, customCompare);
  
    // 2、获取store中对应的值,以old===new作为更新检查规则
    const count = useCountStore((state) => state.count);
    return (
        <View style={[styles.container, { flex: 1 }]}>
            <Text
                style={[styles.text, { marginBottom: 40 }]}
            >{`count : ${count}`}</Text>
            <B />
        </View>
    );
};

const B = () => {
    // 3、调用Store中定义的更新操作,完成更新动作
    const { increase, decrease, reset } = useCountStore.getState();

    return (
        <View style={styles.container}>
            <Image
                style={styles.image}
                source={require('../images/logo.png')}
            />
            <View style={styles.buttonContainer}>
                <TouchableOpacity style={styles.button} onPress={increase}>
                    <Text style={styles.buttonText}>increase</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={decrease}
                >
                    <Text style={styles.buttonText}>decrease</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={reset}
                >
                    <Text style={styles.buttonText}>reset</Text>
                </TouchableOpacity>
            </View>
        </View>
    );
};

export default App;
异步

Zustand Store中定义的更新函数天然支持异步

import { create } from 'zustand';

type Count = {
    count: number;
    fetchData: (params) => Promise<void>;
};

const useCountStore = create<Count>((set) => ({
    count: 0,
    // 1、定义异步操作
    fetchData:  async (params) => {
      	fetch('https://reactnative.dev/movies.json')
          .then((response) => response.json())
          .then((json) => {
              // 2、根据异步请求结果更新state
              set({ count: json.count });
         });
    },
}));

const App = () => {
    return <A />;
};

const A = () => {
    // 获取store中的数据
    const count = useCountStore((state) => state.count); 
    return (
        <View style={[styles.container, { flex: 1 }]}>
            <Text
                style={[styles.text, { marginBottom: 40 }]}
            >{`count : ${count}`}</Text>
            <B />
        </View>
    );
};

const B = () => {
    // 获取store中的方法
    const { increase, decrease, reset, fetchData } = useCountStore.getState(); 
  
    useEffect(() => {
      	// 3、调用Store中定义的异步操作
    	fetchData(parmas);
    }, []);

    return (
        <View style={styles.container}>
            ...
        </View>
    );
};

export default App;
中间件

中间件的本质是各种函数根据顺序互相嵌套调用, Zustand 天生就是使用这种函数式的设计,所以可以使用 ramda 提供的 pipe 函数将中间件很方便地给串联起来。

// 定义一个只要状态更新就进行打印的中间件
const log = (config) => (set, get, api) =>
  config(
    (...args) => {
      console.log('  applying', args)
      set(...args)
      console.log('  new state', get())
    },
    get,
    api
  )

const immer = config => (set, get, api) => config((partial, replace) => {
  const nextState = typeof partial === 'function'
      ? produce(partial)
      : partial
  return set(nextState, replace)
}, get, api);

/* 通过pipe集合任意数量的中间件 */
const createStore = pipe(log, immer, create)
const useBeeStore = createStore((set) => ({
      bees: false,
      setBees: (input) => set((state) => void (state.bees = input)),
    })
);

Zustand提供的一些中间件:persist(数据持久化),devTools(兼容Redux开发工具,演示),subscribeWithSelector(订阅处理器

非React方式访问状态

Zustand可以轻松的当成一个简单的函数使用,不需要考虑使用的位置。而Redux需要考虑使用的位置是否在Provider包裹的组件中,还必须是React组件中才能使用Redux store,否则程序会出现错误。

const useStore = create(() => ({ paw: true, snout: true, fur: true }))

// 在React组件之外获取state
const paw = useStore.getState().paw //true

// 侦听所有更改,在每次更改时同步触发
const unsub = useStore.subscribe(console.log)
// 更新状态,将触发侦听器
useStore.setState({ paw: false })
// 取消订阅侦听
unsub()

// 销毁商店(删除所有侦听器)
useStore.destroy()

// 当然可以像hook一样使用
function Component() {
const paw = useStore(state => state.paw)
其他特性

Zustand中Store拆分

监听状态更新但不触发组件渲染

三、当我们选择一个框架的时候,需要考虑的是什么

从上面的对比看起来Zustand也是一个优秀的框架,学习和使用成本更低。但是如果综合社区完善程度还有框架稳定性来说,Redux Toolkit可能会更具备优势,这也是目前大多数人的选择。各位可以根据自身的情况选择更适合自己的框架来进行复杂的状态管理。

考虑点Redux ToolkitZustand
框架能力(异步、中间件支持、单测、调试方便程度)优秀优秀
学习/使用成本成本高成本低
框架稳定性更优优秀
可维护性(是否能够很好的支持团队协作,代码可读性)优秀优秀
更新性能优秀优秀
包大小非webpack:48.11KB webpack:14.24KB2.33KB
社区完善程度(教程、最佳实践、工具)优秀一般
作者背景React大牛,Meta员工自由职业者,来自东方的神秘力量

四、状态管理最佳实践

4.1 使用状态管理工具的场景

组件内状态,还是推荐useState进行更新。当整个页面状态变得复杂,才需要用到状态管理工具。如果状态更新在某一个组件节点内是可以完成的,则不需要用到状态管理。如果出现有如下图类似的,跨越多级组件之间的状态更新,则可以考虑使用状态管理工具。

4.2 使用Redux Toolkit

1、添加依赖

yarn add @reduxjs/toolkit

2、创建store

一般会创建一个store文件夹,里面用来写入所有状态管理相关的内容

index.ts里定义了整个store,在整个程序的入口处会用到,后面会有代码示意

import { Action, ThunkAction, configureStore } from '@reduxjs/toolkit';
import { counterSlice } from './slice';
import { otherSlice } from './slice2';
import thunk from 'redux-thunk';
import { useDispatch, TypedUseSelectorHook, useSelector } from 'react-redux';

// 1、定义store
export const store = configureStore({
    // 1.1 组合各个reducer
    reducer: {
        counter: counterSlice.reducer,
        other: otherSlice.reducer,
    },
    // 1.2 添加中间件
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(thunk),
});

export type AppThunk = ThunkAction<void, RootState, null, Action<string>>;
// 定义RootState,为了能够更好的支持TS
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// 定义带类型的dispatch和selector hook,为了能够在整个App中使用正确的TS类型
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

slice.ts中定义了一个reducer,Redux Toolkit可以按照需要拆分多个功能不同的reducer,使得每个reducer职责更明确

import { createSlice } from '@reduxjs/toolkit';
import { AppThunk, RootState } from '.';

// 1、定义state
interface CounterState {
    value: number;
}

// 2、state的初始值定义
const initialState: CounterState = {
    value: 0,
};

// 3、创建slice
export const counterSlice = createSlice({
    // 3.1 slice名称
    name: 'counter',
    // 3.2 state初始值
    initialState,
    // 3.3 使用action函数填充reducers
    reducers: {
        increment: (state) => {
            state.value += 1;
        },
        decrement: (state) => {
            state.value -= 1;
        },
        reset: (state) => {
            state.value = 0;
        },
        beginLoading: () => {
            console.log('开始加载初始count');
        },
        finishLoading: (state, { payload }) => {
            state.value = payload;
            console.log('结束加载');
        },
    },
});

// 4、导出actions,为了能够在外部访问到
export const { increment, decrement, reset, beginLoading, finishLoading } =
    counterSlice.actions;

const fetchData = () => {
    /// 模拟网络请求获取数据
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(2);
        }, 1000);
    });
};

// 5、异步函数,网络请求可以参考
export const fetchCountData = (): AppThunk => async (dispatch) => {
    // 5.1 开始加载处理,可以Loading提示
    dispatch(beginLoading());
    // 5.2 模拟网络请求获取数据
    fetchData()
        .then((data) => {
            // 5.3网络请求结束后更新结果
            dispatch(finishLoading(data));
        })
        .catch((err) => {
            console.log(`请求失败 ${err}`);
        });
};

// 6、定义selector函数,访问state中某个变量
// 当value值被更新,使用到该selector函数的组件也会重新渲染
export const selectCount = (state: RootState) => state.counter.value;

// 7、导出reducer,在index.ts中组装成最终的store
export default counterSlice.reducer;

slice2.ts中示意了另一个业务含义的reducer定义

import { createSlice } from '@reduxjs/toolkit';
import { RootState } from '.';

interface OtherState {
    value: number;
}

const initialState: OtherState = {
    value: 0,
};

export const otherSlice = createSlice({
    name: 'other',
    initialState,
    reducers: {
        increment: (state) => {
            state.value += 1;
        },
    },
});

export const selectOtherValue = (state: RootState) => state.other.value;

export default otherSlice.reducer;

3、使用store中定义的内容进行状态访问和状态更新

import { Provider } from 'react-redux';
import { store, useAppDispatch, useAppSelector } from './store';
import {
    decrement,
    fetchCountData,
    increment,
    reset,
    selectCount,
} from './store/slice';

const App = () => {
    return (
      	// 1、使用Provider包裹组件,传入全局store对象,重要!!!
      	// Provider包裹之外使用store中的内容运行时会报错
      	// 所以最好是在入口组件处使用Provider包裹
        <Provider store={store}>
            <A />
        </Provider>
    );
};

const A = () => {
    return <B />;
};

const B = () => {
    // 2、获取count内容,当count值有更新组件会重新渲染
    const count = useAppSelector(selectCount);
    // 3、定义dispatch hook,方便后续进行action调用
    const dispatch = useAppDispatch();

    const increase = useCallback(() => {
  	// 4、调用store中定义的action进行状态更新
        dispatch(increment());
    }, [dispatch]);

    const decrease = useCallback(() => {
        dispatch(decrement());
    }, [dispatch]);

    const resetData = useCallback(() => {
        dispatch(reset());
    }, [dispatch]);
  
  	useEffect(() => {
      	// 5、调用异步函数
        dispatch(fetchCountData());
    }, [dispatch]);

    return (
        <View style={styles.container}>
            <Image
                style={styles.image}
                source={require('../images/logo.png')}
            />
            <Text style={styles.text}>{`count : ${count}`}</Text>
            <View style={styles.buttonContainer}>
                <TouchableOpacity style={styles.button} onPress={increase}>
                    <Text style={styles.buttonText}>increase</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={decrease}
                >
                    <Text style={styles.buttonText}>decrease</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={resetData}
                >
                    <Text style={styles.buttonText}>reset</Text>
                </TouchableOpacity>
            </View>
        </View>
    );
};

4.3  使用Zustand

1、添加依赖

yarn add zustand

2、创建store

index.ts

import { create } from 'zustand';
import countSlice, { Count } from './countSlice';
import otherSlice, { OtherInfo } from './otherSlice';

export const useBoundStore = create<Count & OtherInfo>((...a) => ({
    ...countSlice(...a),
    ...otherSlice(...a),
}));

countSlice.ts

export type Count = {
    count: number;
    increase: () => void;
    decrease: () => void;
    reset: () => void;
    fetchCountData: () => void;
};

const fetchData = () => {
    /// 模拟网络请求获取数据
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(2);
        }, 1000);
    });
};

const countSlice = (set) => ({
    count: 0,
    increase: () => set((state) => ({ count: state.count + 1 })),
    decrease: () => set((state) => ({ count: state.count - 1 })),
    reset: () => set({ count: 0 }),
    fetchCountData: async () => {
        fetchData().then((data: number) => {
            console.log(`data ${data}`);
            set({ count: data });
        });
    },
});

export default countSlice;

otherSlice.ts

export type OtherInfo = {
    other: number;
    otherIncrease: () => void;
    otherDecrease: () => void;
};

const otherSlice = (set) => ({
    other: 10,
    otherIncrease: () => set((state) => ({ other: state.other + 1 })),
    otherDecrease: () => set((state) => ({ other: state.other - 1 })),
});

export default otherSlice;

3、使用store中定义的内容进行状态访问和状态更新

import React, { useEffect } from 'react';
import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
import { shallow } from 'zustand/shallow';
import { useBoundStore } from './store';

const App = () => {
    return <A />;
};

const A = () => {
    const fetchCountData = useBoundStore((state) => state.fetchCountData);
    useEffect(() => {
        console.log('A render');
    });

    useEffect(() => {
        fetchCountData();
    }, [fetchCountData]);
  
    // 2、获取store中对应的值,以old===new作为更新检查规则
    const count = useBoundStore((state) => state.count);
    const other = useBoundStore((state) => state.other);
    return (
        <View style={[styles.container, { flex: 1 }]}>
            <Text
                style={[styles.text, { marginBottom: 40 }]}
            >{`count : ${count} other: ${other}`}</Text>
            <B />
        </View>
    );
};

const B = () => {
    useEffect(() => {
        console.log('B render');
    });

    const { increase, decrease, reset, increaseOther, decreaseOther } =
        useBoundStore(
            (state) => ({
                increase: state.increase,
                decrease: state.decrease,
                reset: state.reset,
                increaseOther: state.otherIncrease,
                decreaseOther: state.otherDecrease,
            }),
            shallow,
        );

    return (
        <View style={styles.container}>
            <Image
                style={styles.image}
                source={require('../images/logo.png')}
            />
            <View style={styles.buttonContainer}>
                <TouchableOpacity style={styles.button} onPress={increase}>
                    <Text style={styles.buttonText}>increase</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={decrease}
                >
                    <Text style={styles.buttonText}>decrease</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={reset}
                >
                    <Text style={styles.buttonText}>reset</Text>
                </TouchableOpacity>
            </View>
            <View style={styles.buttonContainer}>
                <TouchableOpacity style={styles.button} onPress={increaseOther}>
                    <Text style={styles.buttonText}>increase other</Text>
                </TouchableOpacity>
                <TouchableOpacity
                    style={[styles.button, { marginLeft: 20 }]}
                    onPress={decreaseOther}
                >
                    <Text style={styles.buttonText}>decrease other</Text>
                </TouchableOpacity>
            </View>
        </View>
    );
};

export default App;

4.4 什么状态适合放到Store中

参考Redux官网中的原则:全局 state 应该放在 Redux store 中,本地 state 应该留在 React 组件中。

如果你不确定哪些数据应该放入 Redux,这里有一些常用的经验法则可以参考:

  • 应用程序的其他部分是否关心这些数据?

  • 是否需要基于这些原始数据创建派生数据?

  • 是否使用相同的数据来驱动多个组件?

  • 是否有将此 state 恢复到特定时间点(即时间旅行调试)的需求?

  • 是否需要缓存数据(比如它已经存在,则直接使用 state 中的值而不重新请求)?

  • 是否希望在热重载 UI 组件时(可能会丢失内部 state) 仍能保持数据一致性?

4.5 已有项目状态管理方式迁移

如果之前的项目使用的是useContext+useReducer的方式进行状态管理,但是随着项目的迭代发现需要管理的状态越来越多,跨组件通信越来越多,从更新性能和维护性上就需要考虑使用更好的方式来进行状态管理了。

4.5.1 迁移到Redux Toolkit

目前迁移到Toolkit本人还没有发现比较“无痛”的方式,需要按照新项目的步骤重新创建Store等操作,将之前的一些状态迁移到Toolkit里。

4.5.2 迁移到Zustand

Zustand官方提供了类redux的写法,本人试了下可以和useContext+useReducer方式适配。一共有两种方法

const types = { increase: 'INCREASE', decrease: 'DECREASE' }

const reducer = (state, { type, by = 1 }) => {
  switch (type) {
    case types.increase:
      return { grumpiness: state.grumpiness + by }
    case types.decrease:
      return { grumpiness: state.grumpiness - by }
  }
}

const useGrumpyStore = create((set) => ({
  grumpiness: 0,
  dispatch: (args) => set((state) => reducer(state, args)),
}))

const dispatch = useGrumpyStore((state) => state.dispatch)
dispatch({ type: types.increase, by: 2 })

如果还是觉得麻烦,可以直接使用官方提供的redux中间件

import { redux } from 'zustand/middleware'

const useGrumpyStore = create(redux(reducer, initialState))

这样写的好处可以进行渲染优化,减少原本useContext+useReducer方式频繁更新的问题。

但是对于异步和中间件什么的支持看起来还是跟之前差不多。有一种写法看起来可行,但没有在实际的项目中使用过,这种写法也会让整个状态管理的理解成本变得更高,本人的态度是并不推荐,只是提供一种思路。

对于老的状态我们可以维持之前的方式来进行管理,也就是简单的用上面的方法处理一下。但是新的状态可以使用更“Zustand”的方式来进行状态的管理。感兴趣的可以看一下代码。

const useGrumpyStore = create((set) => ({
  grumpiness: 0,
  mood: 'happy',
  dispatch: (args) => set((state) => reducer(state, args)),
  beCheerful: () =>
        set((state) => ({
            mood: 'cheerful',
        })),
}));

// 使用新mood
const { mood, increaseMood } = useStore(
        (state) => ({
            mood: state.mood,
            increaseMood: state.increaseMood,
        }),
        shallow,
);

五、参考资料

Redux Toolkit官网

React18 useMutableSource → useSyncExternalStore

useSyncExternalStore官方文档

Zustand 源码解析

Zustand文档(中文)

Zustand 源码解析(Npm library)

React 进阶: useSyncExternalStore API 外部状态管理

六、总结

以上就是本文全部内容,介绍了ReactNative里常见的状态管理方式,包括RN官方提供的方案与其他第三方框架的对比。同时也给出了Redux ToolKit和Zustand的最佳实践,希望各位可以更轻松的应对ReactNative状态管理场景。

hi, 我是快手电商的ycq

快手电商无线技术团队正在招贤纳士🎉🎉🎉! 我们是公司的核心业务线, 这里云集了各路高手, 也充满了机会与挑战. 伴随着业务的高速发展, 团队也在快速扩张. 欢迎各位高手加入我们, 一起创造世界级的电商产品~

热招岗位: Android/iOS 高级开发, Android/iOS 专家, Java 架构师, 产品经理(电商背景), 测试开发... 大量 HC 等你来呦~

内部推荐请发简历至 >>>我的邮箱: hr.ec@kuaishou.com <<<, 备注我的花名成功率更高哦~ 😘