Introduction
Taro is a cross-platform development framework that allows you to write code once and compile it to run on multiple platforms including WeChat Mini Programs, Alipay Mini Programs, Baidu Smart Programs, ByteDance Mini Programs, QQ Mini Programs, and H5, React Native, and other platforms.
Why Choose Taro
Advantages
- Write Once, Run Everywhere: Single codebase for multiple platforms
- React-like Syntax: Familiar development experience for React developers
- Component-based Development: Reusable components across platforms
- Rich Ecosystem: Comprehensive toolchain and plugin system
- TypeScript Support: Built-in TypeScript support
- Performance Optimization: Optimized compilation and runtime
Supported Platforms
- Mini Programs: WeChat, Alipay, Baidu, ByteDance, QQ, Kuaishou
- Mobile Apps: React Native (iOS/Android)
- Web: H5, Progressive Web Apps
- Desktop: Electron (experimental)
Getting Started
Installation
# Install Taro CLI globally
npm install -g @tarojs/cli
# Create new project
taro init myApp
# Choose template
# ❯ TypeScript
# JavaScript
# Vue
# Vue3
Project Structure
myApp/
├── src/
│ ├── pages/ # Page components
│ │ └── index/
│ │ ├── index.tsx
│ │ ├── index.config.ts
│ │ └── index.scss
│ ├── components/ # Reusable components
│ ├── utils/ # Utility functions
│ ├── services/ # API services
│ ├── app.tsx # App entry
│ ├── app.config.ts # App configuration
│ └── app.scss # Global styles
├── config/ # Build configuration
├── package.json
└── project.config.json
Basic Configuration
// app.config.ts
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/profile/profile'
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'My Taro App',
navigationBarTextStyle: 'black'
},
tabBar: {
color: '#666',
selectedColor: '#b4282d',
backgroundColor: '#fafafa',
borderStyle: 'black',
list: [
{
pagePath: 'pages/index/index',
text: 'Home',
iconPath: 'assets/home.png',
selectedIconPath: 'assets/home-active.png'
},
{
pagePath: 'pages/profile/profile',
text: 'Profile',
iconPath: 'assets/profile.png',
selectedIconPath: 'assets/profile-active.png'
}
]
}
});
Component Development
Functional Components
import { View, Text, Button } from '@tarojs/components';
import { useState } from 'react';
import Taro from '@tarojs/taro';
const Counter: React.FC = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const showToast = () => {
Taro.showToast({
title: `Count: ${count}`,
icon: 'none'
});
};
return (
<View className="counter">
<Text className="count-text">Count: {count}</Text>
<Button onClick={increment}>Increment</Button>
<Button onClick={showToast}>Show Toast</Button>
</View>
);
};
export default Counter;
Class Components
import { Component } from 'react';
import { View, Text, Input } from '@tarojs/components';
import Taro from '@tarojs/taro';
interface State {
inputValue: string;
todos: string[];
}
class TodoList extends Component<{}, State> {
state: State = {
inputValue: '',
todos: []
};
componentDidMount() {
console.log('Component mounted');
}
handleInputChange = (e) => {
this.setState({
inputValue: e.detail.value
});
};
addTodo = () => {
const { inputValue, todos } = this.state;
if (inputValue.trim()) {
this.setState({
todos: [...todos, inputValue],
inputValue: ''
});
}
};
render() {
const { inputValue, todos } = this.state;
return (
<View className="todo-list">
<View className="input-section">
<Input
value={inputValue}
onInput={this.handleInputChange}
placeholder="Enter todo..."
/>
<Button onClick={this.addTodo}>Add</Button>
</View>
<View className="todos">
{todos.map((todo, index) => (
<View key={index} className="todo-item">
<Text>{todo}</Text>
</View>
))}
</View>
</View>
);
}
}
export default TodoList;
Platform APIs
Navigation
import Taro from '@tarojs/taro';
// Navigate to new page
const navigateToDetail = () => {
Taro.navigateTo({
url: '/pages/detail/detail?id=123'
});
};
// Redirect to page
const redirectToLogin = () => {
Taro.redirectTo({
url: '/pages/login/login'
});
};
// Navigate back
const goBack = () => {
Taro.navigateBack({
delta: 1
});
};
// Switch tab
const switchToProfile = () => {
Taro.switchTab({
url: '/pages/profile/profile'
});
};
Storage
import Taro from '@tarojs/taro';
// Set storage
const saveUserData = async (userData) => {
try {
await Taro.setStorage({
key: 'userData',
data: userData
});
console.log('Data saved successfully');
} catch (error) {
console.error('Failed to save data:', error);
}
};
// Get storage
const getUserData = async () => {
try {
const result = await Taro.getStorage({
key: 'userData'
});
return result.data;
} catch (error) {
console.error('Failed to get data:', error);
return null;
}
};
// Remove storage
const clearUserData = async () => {
try {
await Taro.removeStorage({
key: 'userData'
});
console.log('Data cleared');
} catch (error) {
console.error('Failed to clear data:', error);
}
};
Network Requests
import Taro from '@tarojs/taro';
// GET request
const fetchUserList = async () => {
try {
const response = await Taro.request({
url: 'https://api.example.com/users',
method: 'GET',
header: {
'Content-Type': 'application/json'
}
});
if (response.statusCode === 200) {
return response.data;
} else {
throw new Error('Request failed');
}
} catch (error) {
console.error('Network error:', error);
Taro.showToast({
title: 'Network error',
icon: 'none'
});
}
};
// POST request
const createUser = async (userData) => {
try {
const response = await Taro.request({
url: 'https://api.example.com/users',
method: 'POST',
data: userData,
header: {
'Content-Type': 'application/json'
}
});
return response.data;
} catch (error) {
console.error('Failed to create user:', error);
throw error;
}
};
// File upload
const uploadImage = async (filePath) => {
try {
const response = await Taro.uploadFile({
url: 'https://api.example.com/upload',
filePath: filePath,
name: 'file',
formData: {
userId: '123'
}
});
return JSON.parse(response.data);
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
};
State Management
Using Redux
npm install @reduxjs/toolkit react-redux
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import counterSlice from './counterSlice';
const store = configureStore({
reducer: {
counter: counterSlice,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
// store/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
// app.tsx
import { Component, PropsWithChildren } from 'react';
import { Provider } from 'react-redux';
import store from './store';
class App extends Component<PropsWithChildren> {
render() {
return (
<Provider store={store}>
{this.props.children}
</Provider>
);
}
}
export default App;
// Using Redux in components
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../store';
import { increment, decrement } from '../store/counterSlice';
const Counter: React.FC = () => {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
return (
<View>
<Text>Count: {count}</Text>
<Button onClick={() => dispatch(increment())}>+</Button>
<Button onClick={() => dispatch(decrement())}>-</Button>
</View>
);
};
Platform-Specific Development
Conditional Compilation
import Taro from '@tarojs/taro';
const MyComponent: React.FC = () => {
const handleClick = () => {
// #ifdef WEAPP
// WeChat Mini Program specific code
wx.showModal({
title: 'WeChat',
content: 'This is WeChat Mini Program'
});
// #endif
// #ifdef ALIPAY
// Alipay Mini Program specific code
my.alert({
title: 'Alipay',
content: 'This is Alipay Mini Program'
});
// #endif
// #ifdef H5
// H5 specific code
alert('This is H5');
// #endif
};
return (
<View>
<Button onClick={handleClick}>Platform Test</Button>
{/* Conditional rendering */}
{process.env.TARO_ENV === 'weapp' && (
<Text>WeChat Mini Program only</Text>
)}
{process.env.TARO_ENV === 'h5' && (
<Text>H5 only</Text>
)}
</View>
);
};
Platform-Specific Styles
// Common styles
.container {
padding: 20px;
// WeChat Mini Program
/* #ifdef WEAPP */
background-color: #f0f0f0;
/* #endif */
// H5
/* #ifdef H5 */
background-color: #ffffff;
max-width: 750px;
margin: 0 auto;
/* #endif */
// Alipay Mini Program
/* #ifdef ALIPAY */
background-color: #1677ff;
/* #endif */
}
Custom Components
Creating Reusable Components
// components/Card/index.tsx
import { View, Text } from '@tarojs/components';
import { ReactNode } from 'react';
import './index.scss';
interface CardProps {
title?: string;
children: ReactNode;
onClick?: () => void;
}
const Card: React.FC<CardProps> = ({ title, children, onClick }) => {
return (
<View className="card" onClick={onClick}>
{title && <View className="card-title">{title}</View>}
<View className="card-content">{children}</View>
</View>
);
};
export default Card;
// components/Card/index.scss
.card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
overflow: hidden;
&-title {
padding: 16px;
font-size: 18px;
font-weight: 600;
border-bottom: 1px solid #f0f0f0;
}
&-content {
padding: 16px;
}
}
Component with Hooks
import { useState, useEffect } from 'react';
import { View, Text } from '@tarojs/components';
import Taro from '@tarojs/taro';
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await Taro.request({
url: `https://api.example.com/users/${userId}`,
method: 'GET'
});
if (response.statusCode === 200) {
setUser(response.data);
}
} catch (error) {
console.error('Failed to fetch user:', error);
Taro.showToast({
title: 'Failed to load user',
icon: 'none'
});
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId]);
if (loading) {
return <Text>Loading...</Text>;
}
if (!user) {
return <Text>User not found</Text>;
}
return (
<View className="user-profile">
<Text className="user-name">{user.name}</Text>
<Text className="user-email">{user.email}</Text>
</View>
);
};
export default UserProfile;
Performance Optimization
Code Splitting
import { lazy, Suspense } from 'react';
import { View, Text } from '@tarojs/components';
// Lazy load heavy components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const MyPage: React.FC = () => {
return (
<View>
<Text>Page Content</Text>
<Suspense fallback={<Text>Loading...</Text>}>
<HeavyComponent />
</Suspense>
</View>
);
};
Memoization
import { memo, useMemo, useCallback } from 'react';
import { View, Text, Button } from '@tarojs/components';
interface ItemProps {
item: {
id: number;
name: string;
price: number;
};
onSelect: (id: number) => void;
}
const ExpensiveItem = memo<ItemProps>(({ item, onSelect }) => {
const formattedPrice = useMemo(() => {
// Expensive calculation
return `$${item.price.toFixed(2)}`;
}, [item.price]);
const handleSelect = useCallback(() => {
onSelect(item.id);
}, [item.id, onSelect]);
return (
<View className="item">
<Text>{item.name}</Text>
<Text>{formattedPrice}</Text>
<Button onClick={handleSelect}>Select</Button>
</View>
);
});
export default ExpensiveItem;
Testing
Unit Testing with Jest
npm install --save-dev @types/jest jest ts-jest
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1'
},
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts']
};
// __tests__/Counter.test.tsx
import { render, fireEvent } from '@testing-library/react';
import Counter from '../Counter';
describe('Counter Component', () => {
test('renders initial count', () => {
const { getByText } = render(<Counter />);
expect(getByText('Count: 0')).toBeInTheDocument();
});
test('increments count when button clicked', () => {
const { getByText } = render(<Counter />);
const button = getByText('Increment');
fireEvent.click(button);
expect(getByText('Count: 1')).toBeInTheDocument();
});
});
Deployment
Build for Different Platforms
# Build for WeChat Mini Program
npm run build:weapp
# Build for Alipay Mini Program
npm run build:alipay
# Build for H5
npm run build:h5
# Build for React Native
npm run build:rn
WeChat Mini Program Deployment
# Development build
npm run dev:weapp
# Production build
npm run build:weapp
# Open WeChat Developer Tools
# Import the dist folder as project
H5 Deployment
// config/prod.js
module.exports = {
env: {
NODE_ENV: '"production"'
},
defineConstants: {
API_BASE_URL: '"https://api.production.com"'
},
h5: {
publicPath: '/myapp/',
staticDirectory: 'static',
router: {
mode: 'browser' // or 'hash'
}
}
};
Best Practices
1. Component Design
// Good - Single responsibility
const UserAvatar: React.FC<{
src: string;
size?: 'small' | 'medium' | 'large';
onClick?: () => void;
}> = ({ src, size = 'medium', onClick }) => {
const className = `avatar avatar-${size}`;
return (
<Image
className={className}
src={src}
onClick={onClick}
/>
);
};
// Good - Composition over inheritance
const UserCard: React.FC<{ user: User }> = ({ user }) => {
return (
<Card title="User Profile">
<UserAvatar src={user.avatar} size="large" />
<Text>{user.name}</Text>
<Text>{user.email}</Text>
</Card>
);
};
2. Error Handling
import { useState, useEffect } from 'react';
import Taro from '@tarojs/taro';
const useApiData = <T>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await Taro.request({ url });
if (response.statusCode === 200) {
setData(response.data);
} else {
throw new Error(`HTTP ${response.statusCode}`);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
3. Type Safety
// Define types for better development experience
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
interface PageProps {
userId: string;
}
const UserDetailPage: React.FC = () => {
const { userId } = Taro.getCurrentInstance().router?.params as PageProps;
const { data: user, loading, error } = useApiData<User>(
`/api/users/${userId}`
);
// Type-safe component logic
};
Conclusion
Taro provides a powerful solution for cross-platform mini program development. Key benefits include:
- Code Reusability: Write once, deploy everywhere
- Familiar Syntax: React-like development experience
- Rich Ecosystem: Comprehensive tooling and community support
- Performance: Optimized compilation and runtime
- Type Safety: Built-in TypeScript support
While Taro handles most platform differences automatically, understanding platform-specific features and limitations is crucial for building robust applications. Start with simple components and gradually explore advanced features as your application grows.
Recently, I had a mini program development project. Since I’ve been using React as my main technology for the past few years, after weighing the options, I decided to use Taro because it supports React for development. Here, I’ve accumulated some experience and summarized it for future reference.
Get Started
npm i -g @tarojs/cli
taro init
# If you plan to use taro-ui in the future, you need to select Sass because taro-ui uses Sass.
UI Component Libraries
- taro-ui I also researched other options. Considering the convenience of maintenance and use, I ultimately chose taro-ui.
Some Libraries
- For form state management, you can use
react-hook-form - For state management, you can use
redux - For network requests, you can use
axios - For QR code generation, you can use
taro-code

