0%

使用 Context 和 useReducer 的 React Native 全局状态管理方案

介绍

React Native 全局状态(如登录后的用户信息)的管理方案通常可以是 ReduxMobx 或者原生的 Context

全局状态的管理要解决两个问题:一个是数据的存储,全局状态需要有一个可以存储的地方;另一个是全局的访问,需要方便地在任何位置获取和修改。

基于 React Native 自带的能力,本文使用 useReducer 处理数据的存储,使用 Context 使状态可以全局读取和修改。(本文的功能服务于函数组件,类组件使用时由于无法使用 Hook 就没那么方便了。)

实现

我们实现一个用户登录信息的管理工具,用于用户登录信息的读取和管理。其代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// 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 中作为组件的最外层。

1
2
3
4
5
6
7
function App(): JSX.Element {
return (
<AuthProvider>
<AppContent /> // 这里是具体的业务组件
</AuthProvider>
);
}

这样,我们在内部组件中,就可以在任何位置访问到该 Context

useAuth

真正业务中,我们需要读取用户 token、用户信息,同时还需要有用户登录和退出登录等操作,而这些只需要 useAuth 这一个 Hook 即可。

这个 Hook 返回的对象中包含了一个 state 对象和一个 actions 对象。前者规定了用户信息的存储内容,其中 UserInfo 需要根据项目中需要存储的字段自主配置。使用时,我们直接读取即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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 对象中,预置了 loginlogout 两个操作(需要实现注册操作只需以类似方式添加即可),使用时只需要在合理的位置调用,即可实现登录信息的设置和全局状态的更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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 的时候,定义了 loginlogout 这样的函数,从而将reducer 的细节封装在了内部。比如 retrieveToken(应用启动后从本地读取数据到内存的步骤)这样一个 action 就是没必要暴露出去的,我们在内部就完成了其全部逻辑。