React 18+TypeScript

Tích hợp React

Hướng dẫn đầy đủ cách sử dụng component Bản đồ Việt Nam trong ứng dụng React với đầy đủ type safety và hỗ trợ hooks.

01Cài đặt

npm install @xdev-asia/vietnam-map-34-provinces highcharts highcharts-react-official

Thư viện yêu cầu highchartshighcharts-react-official là peer dependencies. Đảm bảo bạn đã cài đặt React 18 trở lên.

02Sử dụng Cơ bản

Import component VietnamMap và đặt vào ứng dụng của bạn. Bản đồ tự động xử lý topology 34 tỉnh thành và trạng thái loading.

  • Responsive chiều cao/chiều rộng
  • Tự động fill container
SimpleExample.tsx
import { VietnamMap } from '@xdev-asia/vietnam-map-34-provinces/react';
export default function App() {
return (
<div style={{ height: '600px' }}>
<VietnamMap />
</div>
);
}

03Tương tác & Props

import { VietnamMap } from '@xdev-asia/vietnam-map-34-provinces/react';
function InteractiveMap() {
return (
<VietnamMap
height={600}
data={[
{ 'hc-key': 'vn-new-ha-noi', value: 5000 },
{ 'hc-key': 'vn-new-ho-chi-minh', value: 8000 }
]}
onProvinceClick={(province) => {
console.log('Đã chọn:', province.name);
}}
colorAxis={{
minColor: '#e0f2fe',
maxColor: '#0284c7'
}}
/>
);
}

04Custom Data & Tooltip

Hiển thị dữ liệu riêng cho từng tỉnh

Truyền data với bất kỳ fields nào bạn muốn. Tất cả custom fields sẽ được pass vào tooltipFormatter và onProvinceClick.

function CustomDataMap() {
const provinceData = [
{
name: 'Hà Nội',
value: 8500000,
population: 8500000,
area: 3344,
gdp: 150000,
hospitals: 120,
universities: 85
},
{
name: 'Hồ Chí Minh',
value: 9000000,
population: 9000000,
area: 9650,
gdp: 280000,
hospitals: 200,
universities: 95
}
// ... các tỉnh khác
];
return (
<VietnamMap
data={provinceData}
tooltipFormatter={(point) => `
<div style="padding: 12px; min-width: 220px;">
<div style="font-weight: bold; font-size: 16px; margin-bottom: 8px;">
📍 ${point.name}
</div>
<table style="width: 100%; font-size: 12px;">
<tr>
<td>Dân số:</td>
<td style="text-align: right;"><b>${point.population?.toLocaleString()}</b></td>
</tr>
<tr>
<td>Diện tích:</td>
<td style="text-align: right;"><b>${point.area} km²</b></td>
</tr>
<tr>
<td>GDP:</td>
<td style="text-align: right;"><b>${point.gdp?.toLocaleString()} tỷ</b></td>
</tr>
<tr>
<td>Bệnh viện:</td>
<td style="text-align: right;"><b>${point.hospitals}</b></td>
</tr>
</table>
</div>
`}
onProvinceClick={(province) => {
console.log('Province data:', province);
// province chứa tất cả custom fields
}}
/>
);
}

05Load Data từ API

Fetch và transform data

import { useState, useEffect } from 'react';
function APIDataMap() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
try {
const res = await fetch('/api/provinces/statistics');
const apiData = await res.json();
// Transform data
const transformed = apiData.map(item => ({
name: item.province_name,
value: item.total_cases,
activeCases: item.active,
recovered: item.recovered,
vaccinationRate: item.vaccination_rate
}));
setData(transformed);
} catch (error) {
console.error('Error:', error);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
if (loading) return <div>Đang tải...</div>;
return (
<VietnamMap
data={data}
tooltipFormatter={(point) => `
<div>
<b>${point.name}</b><br/>
Tổng ca: ${point.value}<br/>
Đang điều trị: ${point.activeCases}<br/>
Đã khỏi: ${point.recovered}
</div>
`}
/>
);
}

06State Management

Kết hợp với Redux/Zustand

// store/useMapStore.ts
import { create } from 'zustand';
export const useMapStore = create((set) => ({
selectedProvince: null,
data: [],
setSelectedProvince: (province) => set({ selectedProvince: province }),
setData: (data) => set({ data })
}));
// Component
function MapWithStore() {
const { data, selectedProvince, setSelectedProvince } = useMapStore();
return (
<div className="grid grid-cols-2 gap-4">
<VietnamMap
data={data}
onProvinceClick={setSelectedProvince}
/>
{selectedProvince && (
<div className="p-4 bg-white rounded shadow">
<h2 className="text-xl font-bold">{selectedProvince.name}</h2>
<p>Dân số: {selectedProvince.population?.toLocaleString()}</p>
<p>GDP: {selectedProvince.gdp?.toLocaleString()} tỷ</p>
</div>
)}
</div>
);
}

07Core Utilities

Truy xuất Dữ liệu trực tiếp

Bạn có thể truy xuất dữ liệu thô của tỉnh và xã thông qua các tiện ích core. Hữu ích để xây dựng giao diện tìm kiếm hoặc danh sách tùy chỉnh.

import {
getProvinceStats,
getProvinceCommunes,
searchCommunes,
getNewProvinceName,
NEW_34_PROVINCES
} from '@xdev-asia/vietnam-map-34-provinces/core';
// 1. Lấy thống kê tổng quan
const stats = getProvinceStats();
console.log(`Tổng số xã: ${stats.totalCommunes}`);
console.log(`Tỉnh lớn nhất: ${stats.largestProvince.name}`);
// 2. Lấy danh sách xã/phường của tỉnh
const communes = getProvinceCommunes('Hà Nội');
console.log(`Hà Nội có ${communes.length} xã/phường`);
// 3. Tìm kiếm xã phường
const results = searchCommunes('Ba Đình');
// [{ province: 'Hà Nội', commune: { code: 10101003, name: 'Phường Ba Đình' } }]
// 4. Convert tên tỉnh cũ sang mới
const newName = getNewProvinceName('Hà Giang'); // → "Tuyên Quang"
// 5. Danh sách tất cả tỉnh thành
const hcm = NEW_34_PROVINCES.find(p => p.code === '29');

08TypeScript Support

Type-safe với custom data

import type { ProvinceData } from '@xdev-asia/vietnam-map-34-provinces/react';
// Define custom interface
interface HealthcareData extends ProvinceData {
name: string;
value: number;
hospitals: number;
doctors: number;
beds: number;
}
function TypedMap() {
const [data, setData] = useState<HealthcareData[]>([]);
const handleClick = (province: HealthcareData) => {
// TypeScript biết province có field hospitals, doctors, beds
console.log(`${province.name}${province.hospitals} bệnh viện`);
};
return (
<VietnamMap
data={data}
onProvinceClick={handleClick}
tooltipFormatter={(point: HealthcareData) => `
<div>
<b>${point.name}</b><br/>
Bệnh viện: ${point.hospitals}<br/>
Bác sĩ: ${point.doctors}<br/>
Giường bệnh: ${point.beds}
</div>
`}
/>
);
}

API Reference

PropTypeDefaultMô tả
dataany[]-Dữ liệu cho từng tỉnh với name, value và custom fields
heightnumber | string600Chiều cao của container bản đồ
showLabelsbooleantrueHiển thị tên tỉnh trên bản đồ
showZoomControlsbooleantrueHiển thị nút zoom +/-
enableDrilldownbooleantrueCho phép click để xem cấp xã/phường
tooltipFormatter(point) => string-Custom tooltip, nhận point data return HTML
onProvinceClick(province) => void-Callback khi click vào tỉnh
hoverColorstring#fbbf24Màu sắc khi hover
borderColorstring#ffffffMàu viền giữa các tỉnh
colorAxisColorAxisOptions-Cấu hình gradient màu (minColor, maxColor)
classNamestring-CSS class cho container wrapper
optionsHighcharts.Options-Override toàn bộ config Highcharts
💡 Tip: Tất cả custom fields trong data sẽ được pass vào tooltipFormatteronProvinceClick

09Best Practices

Performance Optimization

import { useMemo, useCallback } from 'react';
function OptimizedMap({ rawData }) {
// Memoize transformed data
const mapData = useMemo(() => {
return rawData.map(item => ({
name: item.province_name,
value: item.total,
...item
}));
}, [rawData]);
// Memoize callbacks
const handleClick = useCallback((province) => {
console.log('Clicked:', province.name);
}, []);
const tooltipFormatter = useMemo(() => {
return (point) => `<div><b>${point.name}</b>: ${point.value}</div>`;
}, []);
return (
<VietnamMap
data={mapData}
onProvinceClick={handleClick}
tooltipFormatter={tooltipFormatter}
/>
);
}

Error Handling

function SafeMap({ data }) {
const [error, setError] = useState(null);
// Validate data
const validatedData = useMemo(() => {
try {
if (!Array.isArray(data)) {
throw new Error('Data must be an array');
}
return data.filter(item => {
if (!item.name || typeof item.value !== 'number') {
console.warn('Invalid item:', item);
return false;
}
return true;
});
} catch (err) {
setError(err.message);
return [];
}
}, [data]);
if (error) {
return <div className="text-red-500">Error: {error}</div>;
}
return <VietnamMap data={validatedData} />;
}

Responsive Design

function ResponsiveMap() {
const [height, setHeight] = useState(600);
useEffect(() => {
const updateHeight = () => {
if (window.innerWidth < 768) {
setHeight(400);
} else if (window.innerWidth < 1024) {
setHeight(500);
} else {
setHeight(600);
}
};
updateHeight();
window.addEventListener('resize', updateHeight);
return () => window.removeEventListener('resize', updateHeight);
}, []);
return (
<div className="w-full">
<VietnamMap
height={height}
showLabels={window.innerWidth >= 768}
/>
</div>
);
}

10Next.js Integration

Dynamic Import (SSR disabled)

Highcharts cần chạy client-side. Sử dụng dynamic import với ssr: false

// app/map/page.tsx
'use client';
import dynamic from 'next/dynamic';
const VietnamMap = dynamic(
() => import('@xdev-asia/vietnam-map-34-provinces/react').then(m => m.VietnamMap),
{
ssr: false,
loading: () => (
<div className="h-[600px] flex items-center justify-center">
Đang tải bản đồ...
</div>
)
}
);
export default function MapPage() {
return (
<main>
<h1>Bản đồ Việt Nam</h1>
<VietnamMap height={600} />
</main>
);
}

Server-side Data Fetching

// app/map/page.tsx
import dynamic from 'next/dynamic';
const VietnamMap = dynamic(
() => import('@xdev-asia/vietnam-map-34-provinces/react').then(m => m.VietnamMap),
{ ssr: false }
);
export default async function MapPage() {
// Fetch data server-side
const data = await fetch('https://api.example.com/provinces', {
next: { revalidate: 3600 } // Cache 1 hour
}).then(r => r.json());
return <VietnamMap data={data} height={600} />;
}