Building an MBTI Personality Quiz Mini-Program with Taro

Project Overview

The application is a quiz platform that allows users to create and share personality assessments quickly. The backend is built with Spring Boot, Redis, and AI technologies, while the frontend uses Taro for cross-platform development.

Application Structure

Three Main Screens

  1. Landing Page: Entry point with the quiz title and start button
  2. Quiz Screen: Displays questions with navigation controls
  3. Results Screen: Shows the calculated MBTI personality type

Data Models

Question Schema

Each question is represented as a JSON object with options stored in an array structure:

{
  "title": "When working on a project, you prefer to",
  "options": [
    {
      "key": "A",
      "value": "Work independently",
      "trait": "I"
    },
    {
      "key": "B",
      "value": "Collaborate with others",
      "trait": "E"
    }
  ]
}

This structure separates presentation from logic, making it easier to maintain and extend. The tradeoff is slightly larger storage requirements compared to simpler key-value formats.

Answer Submission Format

When users submit their responses, only an array of option keys is transmitted:

["A", "B", "A", "B", "A"]

This approach eliminates the need to send the entire question structure with each submission, reducing payload size and improving performance.

Scoring Algorithm

The algorithm evaluates user answers based on MBTI trait categories:

  • I/E: Introversion vs Extroversion
  • S/N: Sensing vs Intuition
  • T/F: Thinking vs Feeling
  • J/P: Judging vs Perceiving

Each answer option maps to a specific trait. The system tallies votes for each dimension, and the dominant trait in each category determines the final personality type. For example, selecting five I-answers and one E-answer indicates a strong introverted tendency.

Result Mapping

The system predefines all 16 MBTI personality types with their characteristic traits:

{
  "personalityType": "ISTJ",
  "description": "Practical and reliability-focused, known for attention to detail",
  "iconUrl": "icon_url_istj",
  "traits": ["I", "S", "T", "J"]
}

Scoring works by matching user trait selections against each personality type's trait profile. For each answer, if a trait exists in a type's profile, that type gains a point. After processing all answers, the type with the highest score is returned.

Sample Quiz Data

Question Bank

[
  {
    "title": "When arranging activities, you tend to",
    "options": [
      { "key": "A", "value": "Prefer detailed schedules", "trait": "J" },
      { "key": "B", "value": "Go with the flow", "trait": "P" }
    ]
  },
  {
    "title": "Your approach to rules is",
    "options": [
      { "key": "A", "value": "Strict adherence", "trait": "T" },
      { "key": "B", "value": "Flexible application", "trait": "F" }
    ]
  },
  {
    "title": "In social settings, you usually",
    "options": [
      { "key": "A", "value": "Lead the conversation", "trait": "E" },
      { "key": "B", "value": "Listen more than talk", "trait": "I" }
    ]
  },
  {
    "title": "When facing new challenges, you prefer to",
    "options": [
      { "key": "A", "value": "Research before acting", "trait": "J" },
      { "key": "B", "value": "Learn through experimentation", "trait": "P" }
    ]
  },
  {
    "title": "In daily life, you focus more on",
    "options": [
      { "key": "A", "value": "Specific details and facts", "trait": "S" },
      { "key": "B", "value": "Patterns and possibilities", "trait": "N" }
    ]
  },
  {
    "title": "When making decisions, you typically consider",
    "options": [
      { "key": "A", "value": "Logical consequences", "trait": "T" },
      { "key": "B", "value": "Personal values and feelings", "trait": "F" }
    ]
  },
  {
    "title": "Your ideal routine is",
    "options": [
      { "key": "A", "value": "Structured and predictable", "trait": "S" },
      { "key": "B", "value": "Open and adaptable", "trait": "N" }
    ]
  },
  {
    "title": "When encountering problems, you first",
    "options": [
      { "key": "A", "value": "Explore all possibilities", "trait": "P" },
      { "key": "B", "value": "Consider potential outcomes", "trait": "J" }
    ]
  },
  {
    "title": "Your perspective on time is",
    "options": [
      { "key": "A", "value": "A finite resource to manage", "trait": "T" },
      { "key": "B", "value": "A flexible concept", "trait": "F" }
    ]
  },
  {
    "title": "Generally, you prefer",
    "options": [
      { "key": "A", "value": "Working alone", "trait": "I" },
      { "key": "B", "value": "Working with others", "trait": "E" }
    ]
  }
]

Personality Type Results

[
  {
    "traits": ["I", "S", "T", "J"],
    "description": "Dependable and systematic, focused on practical outcomes",
    "image": "istj_icon",
    "typeName": "ISTJ (Logistician)"
  },
  {
    "traits": ["I", "S", "F", "J"],
    "description": "Warm-hearted and protective, known for loyalty",
    "image": "isfj_icon",
    "typeName": "ISFJ (Defender)"
  },
  {
    "traits": ["I", "N", "F", "J"],
    "description": "Insightful and compassionate, deeply understanding of others",
    "image": "infj_icon",
    "typeName": "INFJ (Advocate)"
  },
  {
    "traits": ["I", "N", "T", "J"],
    "description": "Strategic and independent, focused on achieving goals",
    "image": "intj_icon",
    "typeName": "INTJ (Architect)"
  },
  {
    "traits": ["I", "S", "T", "P"],
    "description": "Analytical and hands-on, skilled at problem-solving",
    "image": "istp_icon",
    "typeName": "ISTP (Virtuoso)"
  },
  {
    "traits": ["I", "S", "F", "P"],
    "description": "Artistic and sensitive, valuing personal expression",
    "image": "isfp_icon",
    "typeName": "ISFP (Adventurer)"
  },
  {
    "traits": ["I", "N", "F", "P"],
    "description": "Creative and empathetic, driven by values",
    "image": "infp_icon",
    "typeName": "INFP (Mediator)"
  },
  {
    "traits": ["I", "N", "T", "P"],
    "description": "Curious and logical, passionate about learning",
    "image": "intp_icon",
    "typeName": "INTP (Logician)"
  },
  {
    "traits": ["E", "S", "T", "P"],
    "description": "Energetic and bold, quick to take action",
    "image": "estp_icon",
    "typeName": "ESTP (Entrepreneur)"
  },
  {
    "traits": ["E", "S", "F", "P"],
    "description": "Outgoing and friendly, bringing positive energy",
    "image": "esfp_icon",
    "typeName": "ESFP (Entertainer)"
  },
  {
    "traits": ["E", "N", "F", "P"],
    "description": "Charismatic and enthusiastic, inspiring others",
    "image": "enfp_icon",
    "typeName": "ENFP (Campaigner)"
  },
  {
    "traits": ["E", "N", "T", "P"],
    "description": "Innovative and argumentative, always exploring new ideas",
    "image": "entp_icon",
    "typeName": "ENTP (Debater)"
  },
  {
    "traits": ["E", "S", "T", "J"],
    "description": "Efficient and results-oriented, natural leadership",
    "image": "estj_icon",
    "typeName": "ESTJ (Executive)"
  },
  {
    "traits": ["E", "S", "F", "J"],
    "description": "Supportive and social, excellent at teamwork",
    "image": "esfj_icon",
    "typeName": "ESFJ (Consul)"
  },
  {
    "traits": ["E", "N", "F", "J"],
    "description": "Inspiring and charismatic, natural at guiding others",
    "image": "enfj_icon",
    "typeName": "ENFJ (Protagonist)"
  },
  {
    "traits": ["E", "N", "T", "J"],
    "description": "Bold and decisive, natural strategic thinkers",
    "image": "entj_icon",
    "typeName": "ENTJ (Commander)"
  }
]

Taro Development Setup

Technology Stack

  • Taro (cross-platform framework)
  • React
  • TypeScript
  • Taro UI component library

Using a compatible component library like Taro UI is essential to prevent style inconsistencies across different platforms.

Environment Configuration

Before starting development, install the WeChat Developer Tools from the official documentation. Initialize a new project by creating a folder and running taro init quiz-app in the terminal.

Open the project in your IDE and run npm install to fetch dependencies. If you encounter installation errors, use npm install --force.

Project Structure

Create additional pages by copying the default index page template and registering them in app.config.ts:

export default defineAppConfig({
  pages: [
    'pages/home/index',
    'pages/quiz/index',
    'pages/result/index'
  ],
  window: {
    backgroundTextStyle: 'light',
    navigationBarBackgroundColor: '#fff',
    navigationBarTitleText: 'MBTI Quiz',
    navigationBarTextStyle: 'black'
  }
})

The first page listed becomes the entry point of the application.

Implementing the Landing Page

The home page displays the quiz title, description, and start button:

import { View, Image } from '@tarojs/components'
import { AtButton } from 'taro-ui'
import mbtiImage from '../../assets/mbti.png'
import Footer from '../../components/Footer'

export default () => {
  return (
    <View className='homePage'>
      <View className='mainTitle'>MBTI Personality Quiz</View>
      <View className='subtitle'>
        Discover your personality type in just 2 minutes
      </View>
      <AtButton type='primary' circle className='startBtn'
        onClick={() => Taro.navigateTo({ url: '/pages/quiz/index' })}
      >
        Start Quiz
      </AtButton>
      <Image src={mbtiImage} className='heroImage' />
      <Footer />
    </View>
  )
}

.homePage {
  background: #A2C7D7;
  min-height: 100vh;
  
  .mainTitle {
    color: white;
    padding-top: 48px;
    text-align: center;
    font-size: 32px;
  }

  .subtitle {
    color: white;
    margin: 24px 0 48px;
    text-align: center;
    padding: 0 20px;
  }

  .startBtn {
    width: 60vw;
  }
}

Implementing the Quiz Page

The quiz page manages question navigation and answer tracking:

import { View } from '@tarojs/components'
import { AtRadio, AtButton } from 'taro-ui'
import questions from '../../data/questions.json'
import Footer from '../../components/Footer'
import Taro from '@tarojs/taro'
import { useState, useEffect } from 'react'
import './quiz.scss'

export default () => {
  const [currentIndex, setCurrentIndex] = useState(1)
  const [currentQuestion, setCurrentQuestion] = useState(questions[0])
  const [selectedAnswer, setSelectedAnswer] = useState('')
  const [answers, setAnswers] = useState<string[]>([])

  const choices = currentQuestion.options.map(opt => ({
    label: `${opt.key}. ${opt.value}`,
    value: opt.key
  }))

  useEffect(() => {
    setCurrentQuestion(questions[currentIndex - 1])
    setSelectedAnswer(answers[currentIndex - 1] || '')
  }, [currentIndex])

  const handleNext = () => {
    const newAnswers = [...answers]
    newAnswers[currentIndex - 1] = selectedAnswer
    setAnswers(newAnswers)
    setCurrentIndex(currentIndex + 1)
  }

  const handlePrevious = () => {
    setCurrentIndex(currentIndex - 1)
  }

  const handleFinish = () => {
    const newAnswers = [...answers]
    newAnswers[currentIndex - 1] = selectedAnswer
    setAnswers(newAnswers)
    Taro.setStorageSync('quizAnswers', newAnswers)
    Taro.navigateTo({ url: '/pages/result/index' })
  }

  return (
    <View className='quizPage'>
      <View className='questionTitle'>
        {currentIndex}. {currentQuestion.title}
      </View>

      <View className='optionsContainer'>
        <AtRadio 
          options={choices} 
          value={selectedAnswer} 
          onClick={(value) => setSelectedAnswer(value)} 
        />
      </View>

      {currentIndex > 1 && (
        <AtButton circle className='navBtn' onClick={handlePrevious}>
          Previous
        </AtButton>
      )}

      {currentIndex < questions.length ? (
        <AtButton 
          type='primary' 
          circle 
          className='navBtn'
          disabled={!selectedAnswer}
          onClick={handleNext}
        >
          Next
        </AtButton>
      ) : (
        <AtButton 
          type='primary' 
          circle 
          className='navBtn'
          disabled={!selectedAnswer}
          onClick={handleFinish}
        >
          View Results
        </AtButton>
      )}

      <Footer />
    </View>
  )
}

.quizPage {
  padding: 20px;
  background: #f5f5f5;
  min-height: 100vh;

  .questionTitle {
    font-size: 18px;
    margin-bottom: 20px;
    color: #333;
  }

  .optionsContainer {
    background: white;
    padding: 15px;
    border-radius: 8px;
    margin-bottom: 20px;
  }

  .navBtn {
    margin: 10px auto;
    width: 80vw;
  }
}

Implementing the Results Page

The results page retrieves stored answers and displays the calculated personality type:

import { View, Image } from '@tarojs/components'
import { AtButton } from 'taro-ui'
import questionData from '../../data/questions.json'
import resultData from '../../data/results.json'
import Taro from '@tarojs/taro'
import { calculateResult } from '../../utils/scoreCalculator'
import './result.scss'

export default () => {
  const storedAnswers = Taro.getStorageSync('quizAnswers')

  if (!storedAnswers || storedAnswers.length === 0) {
    Taro.showToast({
      title: 'No answers found. Please retake the quiz.',
      icon: 'error',
      duration: 2000
    })
    return null
  }

  const personalityResult = calculateResult(storedAnswers, questionData, resultData)

  return (
    <View className='resultPage'>
      <View className='typeTitle'>{personalityResult.typeName}</View>
      <View className='typeDescription'>{personalityResult.description}</View>
      <Image className='resultIcon' src={personalityResult.image} />
      <AtButton 
        type='primary' 
        circle 
        className='homeBtn'
        onClick={() => Taro.reLaunch({ url: '/pages/home/index' })}
      >
        Return Home
      </AtButton>
    </View>
  )
}

.resultPage {
  padding: 40px 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  text-align: center;

  .typeTitle {
    color: white;
    font-size: 36px;
    margin-bottom: 20px;
  }

  .typeDescription {
    color: rgba(255,255,255,0.9);
    font-size: 16px;
    line-height: 1.6;
    margin-bottom: 30px;
    padding: 0 20px;
  }

  .resultIcon {
    width: 120px;
    height: 120px;
    border-radius: 60px;
    margin: 20px auto;
  }

  .homeBtn {
    width: 60vw;
    margin-top: 30px;
  }
}

Scoring Implementation

The scoring function matches user trait selections against personality profiles:

/**
 * Calculate the best matching personality type
 * @param userAnswers Array of selected option keys
 * @param allQuestions Question bank
 * @param personalityTypes All personality type definitions
 */
export function calculateResult(
  userAnswers: string[],
  allQuestions: Question[],
  personalityTypes: PersonalityType[]
) {
  const traitCounts: Record<string, number> = {}

  userAnswers.forEach((answer, answerIndex) => {
    const question = allQuestions[answerIndex]
    if (!question) return

    const selectedOption = question.options.find(opt => opt.key === answer)
    if (selectedOption) {
      const trait = selectedOption.trait
      traitCounts[trait] = (traitCounts[trait] || 0) + 1
    }
  })

  let highestScore = 0
  let bestMatch = personalityTypes[0]

  personalityTypes.forEach(type => {
    const score = type.traits.reduce((total, trait) => {
      return total + (traitCounts[trait] || 0)
    }, 0)

    if (score > highestScore) {
      highestScore = score
      bestMatch = type
    }
  })

  return bestMatch
}

interface Question {
  title: string
  options: Array<{ key: string; value: string; trait: string }>
}

interface PersonalityType {
  traits: string[]
  description: string
  image: string
  typeName: string
}

Data Flow Between Pages

The application uses Taro's storage API to pass data between pages, avoiding direct state sharing which is restricted in mini-programs:

  • Quiz Page to Storage: On submission, answers are stored using Taro.setStorageSync('quizAnswers', answers)
  • Result Page to Storage: Result are retrieved using Taro.getStorageSync('quizAnswers')

For the home navigation, Taro.reLaunch is used instead of Taro.navigateTo because WeChat mini-programs have limits on the navigation stack depth.

Component Organization

Reusable components like the footer should be organized in a dedicated components folder for better maintainability. Import components by their relative path from the page directory.

All styling should be defined in separate SCSS files co-located with their corresponding page or component files.

Tags: Taro React TypeScript WeChat Mini-Program MBTI Quiz

Posted on Sun, 10 May 2026 22:14:34 +0000 by brandonr