可以。这个是 React Native 面试里非常高频的手写/实操题 :要求实现一个类似 Web 端 Modal.confirm() / message.success() 的全局弹窗 + Toast,业务页面不需要自己维护组件状态。
核心思路是:
Provider + Context + 全局状态/事件中心 + Portal(或顶层渲染)
我推荐面试时先写一个简单、可扩展、不依赖第三方库的版本。
一、最终使用效果
业务代码希望做到:
php
import { toast, dialog } from './global-feedback';
toast.success('保存成功');
toast.error('网络异常');
dialog.alert({
title: '提示',
message: '确定要删除吗?',
});
dialog.confirm({
title: '删除',
message: '确定删除这条数据吗?',
onConfirm: () => {
console.log('删除');
},
});
也就是说:
scss
业务页面
│
├── toast.success()
├── toast.error()
└── dialog.confirm()
│
▼
全局 Feedback Manager
│
▼
React Context / Provider
│
▼
App 最顶层渲染
│
┌─────┴─────┐
│ │
Toast Modal
二、Toast 实现
Toast.tsx
typescript
import React, {
createContext,
useCallback,
useContext,
useState,
} from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
} from 'react-native';
type ToastType = 'success' | 'error' | 'info';
interface ToastOptions {
message: string;
type?: ToastType;
duration?: number;
}
interface ToastContextValue {
showToast: (options: ToastOptions) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
export const ToastProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [toast, setToast] = useState<ToastOptions | null>(null);
const showToast = useCallback((options: ToastOptions) => {
setToast(options);
setTimeout(() => {
setToast(null);
}, options.duration ?? 2000);
}, []);
return (
<ToastContext.Provider value={{showToast}}>
{children}
{toast && (
<View pointerEvents="none" style={styles.container}>
<View style={styles.toast}>
<Text style={styles.text}>
{toast.message}
</Text>
</View>
</View>
)}
</ToastContext.Provider>
);
};
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error(
'useToast must be used inside ToastProvider',
);
}
return context;
};
const styles = StyleSheet.create({
container: {
position: 'absolute',
left: 0,
right: 0,
top: 80,
alignItems: 'center',
},
toast: {
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 8,
backgroundColor: '#333',
},
text: {
color: '#fff',
fontSize: 14,
},
});
然后 App:
javascript
export default function App() {
return (
<ToastProvider>
<RootNavigator />
</ToastProvider>
);
}
业务页面:
ini
const HomeScreen = () => {
const {showToast} = useToast();
const handleSave = () => {
showToast({
message: '保存成功',
type: 'success',
});
};
return (
<Button
title="保存"
onPress={handleSave}
/>
);
};
这个版本已经能用了。
三、但是面试官很可能继续问
面试官:
"这样每个页面都得
useToast(),我想直接toast.success()怎么办?"
这时候就要做成:
arduino
toast.success('保存成功');
而不是:
scss
const {showToast} = useToast();
showToast({
message: '保存成功',
});
这就是全局服务模式。
四、实现 Global Feedback Manager
创建:
csharp
global-feedback/
├── index.ts
├── manager.ts
├── FeedbackProvider.tsx
└── types.ts
manager.ts
typescript
type ToastType = 'success' | 'error' | 'info';
interface ToastOptions {
message: string;
type?: ToastType;
duration?: number;
}
type ToastHandler = (
options: ToastOptions,
) => void;
let toastHandler: ToastHandler | null = null;
export const registerToastHandler = (
handler: ToastHandler,
) => {
toastHandler = handler;
};
export const unregisterToastHandler = () => {
toastHandler = null;
};
export const toast = {
show(options: ToastOptions) {
toastHandler?.(options);
},
success(message: string) {
toastHandler?.({
message,
type: 'success',
});
},
error(message: string) {
toastHandler?.({
message,
type: 'error',
});
},
info(message: string) {
toastHandler?.({
message,
type: 'info',
});
},
};
这样业务层:
javascript
import {toast} from './global-feedback';
toast.success('保存成功');
就可以了。
五、Provider 注册 Handler
javascript
import React, {useEffect, useState} from 'react';
import {
View,
Text,
StyleSheet,
} from 'react-native';
import {
registerToastHandler,
unregisterToastHandler,
} from './manager';
export const FeedbackProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [toast, setToast] = useState<ToastOptions | null>(
null,
);
useEffect(() => {
registerToastHandler(options => {
setToast(options);
setTimeout(() => {
setToast(null);
}, options.duration ?? 2000);
});
return () => {
unregisterToastHandler();
};
}, []);
return (
<View style={{flex: 1}}>
{children}
{toast && (
<View
pointerEvents="none"
style={styles.container}>
<View style={styles.toast}>
<Text style={styles.text}>
{toast.message}
</Text>
</View>
</View>
)}
</View>
);
};
然后:
javascript
export default function App() {
return (
<FeedbackProvider>
<RootNavigator />
</FeedbackProvider>
);
}
业务代码:
javascript
import {toast} from './global-feedback';
function LoginScreen() {
const login = async () => {
try {
await requestLogin();
toast.success('登录成功');
} catch (error) {
toast.error('登录失败');
}
};
return (
<Button
title="登录"
onPress={login}
/>
);
}
六、全局 Dialog
Dialog 和 Toast 的实现思路完全一样。
定义:
typescript
interface DialogOptions {
title?: string;
message: string;
cancelText?: string;
confirmText?: string;
onCancel?: () => void;
onConfirm?: () => void;
}
Manager:
ini
type DialogHandler = (
options: DialogOptions,
) => void;
let dialogHandler: DialogHandler | null = null;
export const registerDialogHandler = (
handler: DialogHandler,
) => {
dialogHandler = handler;
};
export const unregisterDialogHandler = () => {
dialogHandler = null;
};
export const dialog = {
alert(options: DialogOptions) {
dialogHandler?.(options);
},
confirm(options: DialogOptions) {
dialogHandler?.(options);
},
};
Provider:
csharp
const [dialog, setDialog] =
useState<DialogOptions | null>(null);
注册:
ini
useEffect(() => {
registerDialogHandler(options => {
setDialog(options);
});
return () => {
unregisterDialogHandler();
};
}, []);
渲染:
ini
{dialog && (
<View style={styles.overlay}>
<View style={styles.modal}>
<Text style={styles.title}>
{dialog.title}
</Text>
<Text style={styles.message}>
{dialog.message}
</Text>
<View style={styles.actions}>
<Button
title={dialog.cancelText ?? '取消'}
onPress={() => {
setDialog(null);
dialog.onCancel?.();
}}
/>
<Button
title={dialog.confirmText ?? '确定'}
onPress={() => {
setDialog(null);
dialog.onConfirm?.();
}}
/>
</View>
</View>
</View>
)}
于是业务代码:
php
dialog.confirm({
title: '删除',
message: '确定删除这条数据吗?',
onConfirm: async () => {
await deleteUser();
toast.success('删除成功');
},
});
七、面试真正想考的其实不是 Modal
这个题目重点是考你有没有理解:
① 为什么需要 Provider?
因为 RN 的 UI 必须处于 React 渲染树中。
markdown
toast.success()
↓
改变全局状态
↓
Provider
↓
重新 render
↓
Toast
② 为什么不能随便在 API 文件里面直接 render?
因为:
arduino
toast.success('成功');
这个调用发生在 React 组件树之外。
React Native 本身没有类似:
javascript
document.body.appendChild(...)
这种 DOM 操作。
所以需要一个常驻在 Root 的 UI 容器。
③ 为什么用 Handler?
因为:
scss
toast.success()
本身不负责 UI。
它只负责:
发送命令
↓
Manager
↓
Provider
↓
UI
这实际上是一种:
命令与 UI 解耦
的设计。
八、进一步优化:Promise Dialog
如果面试官继续问:
"能不能让我这样写?"
csharp
const confirmed = await dialog.confirm({
title: '删除',
message: '确定删除?',
});
if (confirmed) {
await deleteUser();
}
这个就更漂亮。
Manager:
typescript
interface DialogOptions {
title?: string;
message: string;
}
interface DialogTask {
options: DialogOptions;
resolve: (result: boolean) => void;
}
let dialogHandler:
((task: DialogTask) => void) | null = null;
export const registerDialogHandler = (
handler: (task: DialogTask) => void,
) => {
dialogHandler = handler;
};
export const dialog = {
confirm(
options: DialogOptions,
): Promise<boolean> {
return new Promise(resolve => {
dialogHandler?.({
options,
resolve,
});
});
},
};
Provider:
ini
const [task, setTask] =
useState<DialogTask | null>(null);
useEffect(() => {
registerDialogHandler(task => {
setTask(task);
});
}, []);
点击确定:
ini
const handleConfirm = () => {
task?.resolve(true);
setTask(null);
};
点击取消:
ini
const handleCancel = () => {
task?.resolve(false);
setTask(null);
};
业务代码直接:
csharp
const confirmed = await dialog.confirm({
title: '删除',
message: '确定删除这条数据吗?',
});
if (!confirmed) {
return;
}
await deleteUser();
这个版本就已经比较接近生产级设计了。
九、如果是面试,我建议你记住这一套
全局 Toast / Modal 的核心答案:
scss
┌──────────────┐
│ 业务代码 │
│ │
│ toast.xxx() │
│ dialog.xxx() │
└──────┬───────┘
│
▼
┌─────────────────┐
│ Global Manager │
│ │
│ handler / queue │
└────────┬────────┘
│
▼
┌─────────────────┐
│ FeedbackProvider│
│ │
│ React State │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Root UI │
│ │
│ Toast / Modal │
└─────────────────┘
一句话回答面试官:
我会把 Toast 和 Dialog 常驻在 App Root,通过 Provider 挂载 UI,通过一个全局 Manager 暴露命令式 API。业务层调用
toast.success()或dialog.confirm()时,Manager 通知 Provider 更新状态,从而触发 Root 层 UI 渲染。这样业务组件不需要关心弹窗的状态和挂载位置,实现 UI 与业务逻辑解耦。