Introduction
A headless React hook for Turkish addresses: province, district and neighbourhood picked in a chain, the postal code derived from the pick, server validation and the PTT dataset.
Türkiye only
This package covers addresses in Türkiye: its 81 provinces, 973 districts and 72,466 neighbourhoods. Addresses from other countries are not included.
A Turkish address is picked in three steps, each depending on the one before: the province, then a district inside it, then a neighbourhood inside that. The postal code belongs to the neighbourhood. useViraAddress() runs that chain for you, loads the data a province at a time and works out the postal code. The markup and the styling stay yours.
| In these docs | In Turkish | What it is |
|---|---|---|
| Province | il | 81 of them. Each has a plate code from 1 to 81, e.g. 34 for İstanbul. |
| District | ilçe | 973 in total, nested under a province. |
| Neighbourhood | mahalle | 72,466 in total. In rural districts these are mostly villages (köy). |
| Postal code | posta kodu | Five digits, issued by PTT, Türkiye's postal service. |
Getting started
$ pnpm add @virastack/addressAddress line
Builds up as you pick
value
{
"provinceCode": null,
"province": null,
"district": null,
"neighbourhood": null,
"postalCode": null,
"postalCodes": []
}Picking a province lists its districts. Picking a district lists its neighbourhoods, which are fetched for that province the first time it is needed. Picking a neighbourhood fills in value.postalCode. Changing any level clears everything below it. Typing a postal code works the other way round and fills in the selects.
The demo above runs the same hook on shadcn/ui selects; see UI libraries for other components.
Why ViraStack Address?
- Chained selection: Province, district, neighbourhood. Changing a level clears the ones below it, in the hook and in your form.
- Postal code both ways: Picking a neighbourhood fills in its code, and typing a code fills in the selects: all three when the code has one neighbourhood, province and district otherwise. See from a postal code.
- One-line address:
formatAddressturns the pick into "Alandüzü Mah. Adapazarı/Sakarya", optionally with the postal code. - Up-to-date data: An automated check compares the dataset against PTT every month. When something changes, a patch release ships; the API stays the same.
- Loaded on demand: Province and district names ship with the package; a province's neighbourhoods download only when it is picked. A typical visit costs about 10 kB.
- Headless: Nothing is rendered for you. A native select, shadcn/ui, Ant Design or your own component all work.
- Turkish-aware: Sorting, casing and search handle the dotted
İand the dotlessıcorrectly. - Validated on the server: A postal code sent by the client is never trusted;
resolveAddresschecks the address against the dataset, derives the code itself and refuses a submitted code that does not match.
Comparison
Both packages compared here are data packages: you write the selects, the chaining and the validation. The difference is the hook, the postal code, loading on demand and the server.
| Feature | ViraStack Address | turkey-neighbourhoods | turkey-location-data |
|---|---|---|---|
| React hook (chained selects) | Yes | No | No |
| Province, district and neighbourhood data | Yes | Yes | Yes |
| Neighbourhoods in the package | 72,466 | 73,305 | 49,662 |
| Postal code from the neighbourhood | Yes | No | No |
| Runs in the browser | Yes | Yes | No |
| Loads neighbourhoods per province on demand | Yes | No | No |
| Address validation on the server | Yes | Partial or manual | Partial or manual |
| Turkish-aware case, sorting and search | Yes | No | Partial or manual |
| Distances between cities | No | Yes | No |
| Runtime dependencies | - | - | fs |
| Size (same form, first load, min+gzip) | 8.6 KB | 434.5 KB | Does not build for the browser |
| Latest release | 0.0.0Not published | 4.0.3Mar 2024 | 1.1.1May 2025 |
yes · partial or manual · no. Checked on 29 October 2026 against the npm packages (turkey-neighbourhoods 4.0.3, turkey-location-data 1.1.1) and a local build of @virastack/address.
How size was measured: The same form (chained province, district and neighbourhood selects) was written with each package and built with esbuild in production mode, minified, tree-shaken and code split. The size is the gzip difference in the JS loaded on first visit over the same form built without a data package, so React, present on both sides, is not counted. With Address, a province’s neighbourhoods load when it is picked: 4.8 KB median, 29.6 KB at most. turkey-location-data reads its data with Node’s fs module, so it does not build for the browser. Partial server validation means the address can be checked by hand against the exported lists.
Entry points
Only the root entry carries "use client". /server and /data never import React, so they run in Server Components, Server Actions, route handlers and plain Node scripts.
| Import | Exports | Runs on |
|---|---|---|
@virastack/address | useViraAddress, formatAddress, formatName, toSearchKey, compareNames, normalizePostalCode, defaultSource | client |
@virastack/address/server | resolveAddress, resolvePostalCode, postalCodeFor, formatAddress, districtsOf, findProvince, provinces | server |
@virastack/address/data | provinces, districts, province, neighbourhoods, findPostalCode, setLoader, source | client + server |
Requirements
- React 18.2+ for the hook (React 19 is supported).
/serverand/datawork without React. - Node.js 20+
- No runtime dependencies; the dataset ships inside the package.
- React Hook Form is optional. The hook imports nothing from it and takes your form through the
formoption.
Scope
- useViraAddress(), React Hook Form and UI libraries
- Server validation and the dataset
- Turkish helpers and types
Out of scope: street, building and door numbers (they are not in the dataset; collect them as free text), addresses outside Türkiye, maps and geocoding, and a ready-made select component.
