Developing Mini Programs with Taro

This article introduces developing mini programs with Taro, including usage scenarios and implementation details to improve efficiency in cross-platform mini program development.

Sep 9, 2023 · 11 min read · 2189 Words · -Views -Comments · Programming

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

  1. Write Once, Run Everywhere: Single codebase for multiple platforms
  2. React-like Syntax: Familiar development experience for React developers
  3. Component-based Development: Reusable components across platforms
  4. Rich Ecosystem: Comprehensive toolchain and plugin system
  5. TypeScript Support: Built-in TypeScript support
  6. 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

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:

  1. Code Reusability: Write once, deploy everywhere
  2. Familiar Syntax: React-like development experience
  3. Rich Ecosystem: Comprehensive tooling and community support
  4. Performance: Optimized compilation and runtime
  5. 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
Authors
Developer, digital product enthusiast, tinkerer, sharer, open source lover