使用 Context 和 useReducer 的 React Native 全局状态管理方案
介绍
React Native 全局状态(如登录后的用户信息)的管理方案通常可以是 Redux、Mobx 或者原生的 Context。
全局状态的管理要解决两个问题:一个是数据的存储,全局状态需要有一个可以存储的地方;另一个是全局的访问,需要方便地在任何位置获取和修改。
基于 React Native 自带的能力,本文使用 useReducer 处理数据的存储,使用 Context 使状态可以全局读取和修改。(本文的功能服务于函数组件,类组件使用时由于无法使用 Hook 就没那么方便了。)
实现
我们实现一个用户登录信息的管理工具,用于用户登录信息的读取和管理。其代码实现如下:
// AuthContex.tsx
import React, {
createContext,
ReactNode,
useReducer,
useEffect,
useContext,
useMemo,
} from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
type UserInfo = {
username: string;
};
type AuthState = {
isLoading: boolean;
token?: string;
userInfo?: UserInfo;
};
type AuthAction = {
type: 'retrieveToken' | 'login' | 'logout';
token?: string;
userInfo?: UserInfo;
};
function reducer(state: AuthState, action: AuthAction) {
switch (action.type) {
case 'retrieveToken':
return {
...state,
isLoading: false,
token: action.token,
userInfo: action.userInfo,
};
case 'login':
return {
...state,
isLoading: false,
token: action.token,
userInfo: action.userInfo,
};
case 'logout':
return {
...state,
isLoading: false,
token: undefined,
userInfo: undefined,
};
}
}
const initialState = {
isLoading: true,
token: undefined,
userInfo: undefined,
};
const AuthContext = createContext<
| {
state: AuthState;
dispatch: React.Dispatch<AuthAction>;
}
| undefined
>(undefined);
const AuthProvider = ({children}: {children: ReactNode}): JSX.Element => {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
async function retrieveToken() {
try {
const values = await AsyncStorage.multiGet(['token', 'userInfo']);
if (values[0][1] && values[1][1]) {
const tokenValue = values[0][1];
const userInfoValue = JSON.parse(values[1][1]);
dispatch({
type: 'retrieveToken',
token: tokenValue,
userInfo: userInfoValue,
});
} else {
dispatch({
type: 'retrieveToken',
token: undefined,
userInfo: undefined,
});
}
} catch {
dispatch({
type: 'retrieveToken',
token: undefined,
userInfo: undefined,
});
}
}
retrieveToken();
}, []);
return (
<AuthContext.Provider value={{state, dispatch}}>
{children}
</AuthContext.Provider>
);
};
const useAuth = (): {
state: AuthState;
actions: {
login: (token: string, userInfo: UserInfo) => Promise<void>;
logout: () => Promise<void>;
};
} => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used in AuthProvider!');
}
const authActions = useMemo(
() => ({
login: async (token: string, userInfo: UserInfo) => {
await AsyncStorage.multiSet([
['token', token],
['userInfo', JSON.stringify(userInfo)],
]);
context.dispatch({type: 'login', token: token, userInfo: userInfo});
},
logout: async () => {
await AsyncStorage.multiRemove(['token', 'userInfo']);
context.dispatch({
type: 'logout',
token: undefined,
userInfo: undefined,
});
},
}),
[context],
);
return {state: context.state, actions: authActions};
};
export {AuthProvider, useAuth};
使用
我们最终暴露了 AuthProvider 这样一个组件和 useAuth 这样一个 Hook。
AuthProvider
AuthProvider 用于提供一个在其内部可以任意访问和修改全局状态的 Context,它的唯一使用是将该组件包裹在业务组件树的最外层,如可以在 App.js 中作为组件的最外层。
function App(): JSX.Element {
return (
<AuthProvider>
<AppContent /> // 这里是具体的业务组件
</AuthProvider>
);
}
这样,我们在内部组件中,就可以在任何位置访问到该 Context。
useAuth
真正业务中,我们需要读取用户 token、用户信息,同时还需要有用户登录和退出登录等操作,而这些只需要 useAuth 这一个 Hook 即可。
这个 Hook 返回的对象中包含了一个 state 对象和一个 actions 对象。前者规定了用户信息的存储内容,其中 UserInfo 需要根据项目中需要存储的字段自主配置。使用时,我们直接读取即可。
// 1、根据 token 是否存在判断当前界面是主页面还是登录页面
const {state} = useAuth();
return (
<NavigationContainer>
{state.token ? <MainStack /> : <LoginStack />}
</NavigationContainer>
);
// 2、读取用户的 username 并展示
const {state} = useAuth();
return (
<Text style={styles.username}>
{state.userInfo?.username ?? '未设置用户名'}
</Text>
);
而 actions 对象中,预置了 login 和 logout 两个操作(需要实现注册操作只需以类似方式添加即可),使用时只需要在合理的位置调用,即可实现登录信息的设置和全局状态的更改。
// 3、登录接口调用成功后执行登录逻辑(本地数据存储,全局状态设置)
const {actions: {login}} = useAuth();
// ...
<TouchableOpacity
style={styles.login}
onPress={() => {
// 先调用登录接口,成功后回调
login('token123', {username: 'qiweipeng'})
.then(() => {
console.log('登录成功!');
})
.catch(() => {
console.log('因为本地没有存储成功导致登录失败!');
});
}}>
<Text style={styles.loginText}>Sign In</Text>
</TouchableOpacity>
// ...
// 4、执行退出登录逻辑(清除本地数据,全局状态更改)
const {actions: {logout}} = useAuth();
// ...
<TouchableOpacity
style={styles.logout}
onPress={() => {
logout()
.then(() => {
console.log('退出登录成功!');
})
.catch(() => {
console.log('因为本地数据清除失败导致退出登录失败!');
});
}}>
<Text style={styles.logoutText}>Sign Out</Text>
</TouchableOpacity>
// ...
思考
1、本文使用 useReducer 进行数据的存储,实际上使用 useState 也不是不行,如果数据比较简单,也是可以使用 useState 的,需要寄托在 Context 中都是可以实现数据的全局读写的。
2、useReducer 使用了类似 Redux 的思想,通过 dispatch 函数操作 action 继而影响 state。如果没有封装直接使用的话,我们可能会考虑在需要更改状态的位置直接调用 dispatch 函数。但是本文为了较好的封装,同时也不希望 reducer 这个函数直接对外暴露,因此在设计 useAuth 的时候,定义了 login、logout 这样的函数,从而将reducer 的细节封装在了内部。比如 retrieveToken(应用启动后从本地读取数据到内存的步骤)这样一个 action 就是没必要暴露出去的,我们在内部就完成了其全部逻辑。