paint-brush
Kendi Typesafe React Router'ınızı 500 Satırda Nasıl Yazabilirsiniz?ile@olegwock
395 okumalar
395 okumalar

Kendi Typesafe React Router'ınızı 500 Satırda Nasıl Yazabilirsiniz?

ile OlegWock39m2024/03/11
Read on Terminal Reader

Çok uzun; Okumak

O halde, kara kutuyu açmak ve iç işleyişini anlamak için sıfırdan kendi yazım korumalı yönlendiricimizi oluşturacağımız bu yazıda bana katılın. Bu makale, React'ı zaten bildiğinizi ve TypeScript konusunda bilgili olduğunuzu varsaymaktadır.
featured image - Kendi Typesafe React Router'ınızı 500 Satırda Nasıl Yazabilirsiniz?
OlegWock HackerNoon profile picture
0-item

Yaklaşık 6 yıl önce React'ı öğrenirken, react-router edindiğim ilk üçüncü taraf kütüphanelerden biriydi. Demek istediğim, mantıklı: yönlendirme, modern Tek Sayfa Uygulamasının temel yönlerinden biridir. Bunu hafife aldım.


Benim için react-router sihirli bir kutuydu, dahili olarak nasıl çalıştığı hakkında hiçbir fikrim yoktu. Bir süre sonra doğal olarak yönlendirmenin genel olarak nasıl çalıştığını anladığımda bu biraz üzücü oldu. Artık sihir yok, ha :(


Ama "sihirli kara kutular" kavramını geride bıraktığım için mutluyum. Bunun gerçekten zararlı bir fikir olduğunu düşünüyorum. Teknolojinin her parçasının tıpkı sizin ve benim gibi mühendisler tarafından üretildiğini anlamak bana çok ilham veriyor ve umarım sizin için de aynısını yapar.


O halde, kara kutuyu açmak ve iç işleyişini anlamak için sıfırdan kendi yazım korumalı yönlendiricimizi oluşturacağımız bu yazıda bana katılın. Bu makale, React'ı zaten bildiğinizi ve TypeScript konusunda bilgili olduğunuzu varsaymaktadır.

Özellikler ve Sınırlamalar

Bu makalede yönlendiricimizin hangi özelliklerinin ele alınacağını özetleyeyim.


En önemli özelliğimiz yazı güvenliği olacak. Bu, rotalarınızı önceden tanımlamanız gerekeceği anlamına gelir ve TypeScript, URL yerine saçma sapan iletiler göndermediğinizi veya mevcut rotada eksik olan parametreleri almaya çalışmadığınızı kontrol edecektir. Bu bir tür jimnastik gerektirecektir, ama endişelenmeyin, size yol göstereceğim.


Bunun yanı sıra, yönlendiricimiz ortalama bir yönlendiriciden beklediğiniz her şeyi destekleyecektir: URL'lere gezinme, rota eşleştirme, rota parametreleri ayrıştırma, geri/ileri gezinme ve gezinme engelleme.


Şimdi sınırlamalar hakkında. Yönlendiricimiz yalnızca tarayıcıda çalışacak (üzgünüm React Native!) ve SRR'yi desteklemeyecek. SSR desteği nispeten kolay olmalı, ancak bu yazı zaten çok büyük, bu yüzden bu konuya değinmeyeceğim.

Terminoloji

Artık ne yapmak istediğimize dair bir vizyonumuz olduğuna göre yapı ve terminoloji hakkında konuşmamız gerekiyor. Oldukça fazla benzer kavram olacaktır, bu nedenle bunları önceden tanımlamak çok önemlidir.


Kütüphanemizde iki tür rota olacaktır: ham rotalar ve ayrıştırılmış rotalar. Ham rota yalnızca /user/:id/info veya /login gibi görünen bir dizedir; URL'ler için bir şablondur. Ham rota, :id gibi iki nokta üst üste ile başlayan bölümler olan parametreler içerebilir.


Bu formatın geliştiriciler için kullanımı kolaydır ancak bir program için o kadar da kolay değildir; bu rotaları daha makine dostu bir formata dönüştüreceğiz ve buna ayrıştırılmış rota adını vereceğiz.


Ancak kullanıcılar rotaları açmaz, URL'leri açar. Ve URL, kaynağın tanımlayıcısıdır (bizim durumumuzda uygulamanın içindeki sayfa); http://example.app/user/42/info?anonymous=1#bio gibi görünebilir. Yönlendiricimiz çoğunlukla URL'nin path adını vereceğimiz ikinci kısmına ( /user/42/info?anonymous=1#bio ) önem verir.


Yol, yol adından ( /user/42/info ), arama parametrelerinden ( ?anonymous=1 ) ve karmadan ( #bio ) oluşur. Bu bileşenleri ayrı alanlar olarak saklayan nesneye konum adı verilecektir.

API'ye Üst Düzey Genel Bakış

React kütüphanelerini oluştururken üç adımda ilerlemeyi seviyorum. Her şeyden önce zorunlu API. İşlevleri herhangi bir yerden çağrılabilir ve genellikle React'a hiçbir şekilde bağlı değildirler. Yönlendirici durumunda bu, navigate , getLocation veya subscribe gibi işlevler olacaktır.


Daha sonra bu işlevlere dayanarak useLocation veya useCurrentRoute gibi kancalar oluşturulur. Daha sonra bu işlevler ve kancalar, yapı bileşenleri için temel olarak kullanılır. Deneyimlerime göre bu son derece iyi çalışıyor ve kolayca genişletilebilir ve çok yönlü bir kitaplık oluşturmaya olanak tanıyor.


Yönlendiricinin API'si defineRoutes işleviyle başlar. Kullanıcının, rotaları ayrıştıran ve aynı şekle sahip bir harita döndüren tüm ham rotaların haritasını fonksiyona iletmesi gerekir. URL oluşturma veya rota eşleştirme gibi geliştiricilere yönelik tüm API'ler, ham değil ayrıştırılmış rotaları kabul eder.


 const routes = defineRoutes({ login: '/login', user: { me: '/user/me', byId: '/user/:id/info', } });


Bir sonraki adım ayrıştırılmış rotaları createRouter fonksiyonuna iletmektir. Bu yönlendiricimizin eti. Bu işlev tüm işlevleri, kancaları ve bileşenleri oluşturacaktır. Bu olağandışı görünebilir, ancak böyle bir yapı, kabul edilen argüman türlerini ve destek türlerini, routes tanımlanan belirli bir rota kümesine göre uyarlamamıza olanak tanıyarak, tür güvenliğini sağlar (ve DX'i geliştirir).


 const { Link, Route, useCurrentRoute, navigate, /* etc... */ } = createRouter(routes);


createRouter uygulamanızın herhangi bir yerinde kullanılabilecek işlevleri (zorunlu API), bileşenlerinizin konum değişikliklerine tepki vermesini sağlayan kancaları ve üç bileşeni döndürür: Link , Route ve NotFound . Bu, kullanım durumlarının çoğunu kapsamak için yeterli olacaktır ve bu API'leri temel alarak kendi bileşenlerinizi oluşturabilirsiniz.

Eğlence ve Kâr için Tür Düzeyinde Programlama

Konuşmamızın yazı güvenliği kısmını ele alarak başlıyoruz. Daha önce de belirttiğim gibi typesafe router ile TypeScript sizi şu gibi bir durumla ilgili önceden uyaracaktır:

 <Link href="/logim" />


Veya bunun gibi:

 const { userld } = useRoute(routes.user.byId);


Ve eğer bunlarda neyin yanlış olduğunu hemen göremiyorsanız, kesinlikle güvenli bir yönlendiriciye ihtiyacınız var :)

TypeScript'teki yazım sistemi çok güçlüdür. Demek istediğim, tür düzeyinde programlama kullanarak bir satranç motoru , bir macera oyunu ve hatta bir SQL veritabanı oluşturabilirsiniz.


Değerleri değiştirdiğiniz (örneğin, iki dizeyi birleştirerek) 'değer düzeyi programlama'ya zaten aşinasınız:

 function concat(a, b) { return a + b; } concat('Hello, ', 'World!'); // 'Hello, World!'


Ancak bunu türlerle de yapabilirsiniz!

 type Concat<A extends string, B extends string> = `${A}${B}`; type X = Concat<'Hello, ', 'World!'>; // ^? type X = "Hello, World!"


Evet, sıradan işlevleriniz kadar güçlü değil ve farklı görünüyor. Ancak oldukça havalı ve yararlı şeyler yapmanıza olanak tanır.


Ham rotalardan parametreleri çıkarmak ve geliştiricinin Link veya URL'yi oluşturan işleve yanlış değerler aktarmaya çalışmadığını kontrol edebilecek yeni türler oluşturmak için tür düzeyinde programlama kullanacağız.


TS ile tür düzeyinde programlama, hızla okunamayan bir karmaşaya dönüşebilir. Neyse ki bizim için tüm bu karmaşıklığı gizleyen ve bunun gibi temiz kod yazmamıza olanak tanıyan çok sayıda proje zaten var:


 export type RouteParam< Route extends RawRoute, > = Pipe< Route, [ Strings.Split<"/">, Tuples.Filter<Strings.StartsWith<":">>, Tuples.Map<Strings.TrimLeft<":">>, Tuples.ToUnion ] >;


Oldukça hoş, değil mi? Bu arada, parametreleri ham rotadan ayrıştırmak için ihtiyacınız olan tüm kod budur. Bu projede, hotscript kitaplığını kullanacağız; bu, karmaşıklığı ve tür düzeyindeki kod miktarını azaltmamıza yardımcı olacaktır.


Ancak bu gerekli değil: Eğer maceracı hissediyorsanız, tüm bu türleri kendiniz uygulamayı deneyebilirsiniz. Benzer özellikleri üçüncü taraf tür kitaplıkları kullanmadan uygulayan Chicane yönlendiricisinden biraz ilham alabilirsiniz.


Eğer devam edecekseniz, favori başlatıcınızı ( Vite kullanıyorum) kullanarak yeni bir React projesi oluşturmanızı ve orada kodlamaya başlamanızı öneririm. Bu şekilde yönlendiricinizi hemen test edebileceksiniz.


Next.js gibi çerçevelerin bu projeye müdahale edebilecek kendi yönlendirmelerini sağladığını ve bunun yerine 'vanilla' React'ı kullandığını lütfen unutmayın. Herhangi bir zorlukla karşılaşırsanız kodun tamamını burada bulabilirsiniz.


Üçüncü taraf paketleri yükleyerek başlayın: tür düzeyinde yardımcı programlar için hotscript ve parametreleri URL/raw rotasından ayrıştırmak için regexparam .


 npm install hotscript regexparam


Çeşitlerimizin ilk yapı tuğlası ham rotadır. Ham rota / ile başlamalıdır; bunu TS'de nasıl kodlarsınız? Bunun gibi:

 export type RawRoute = `/${string}`;


Kolay değil mi? Ancak defineRoutes tek bir ham rotayı kabul etmez, muhtemelen iç içe geçmiş haritalamayı kabul eder, o yüzden hadi bunu kodlayalım. Şöyle bir şey yazmak isteyebilirsiniz:

 export type RawRoutesMap = { [key: string]: RawRoute | RawRoutesMap };


Bu çalışacak. Ancak bu tür sonsuz derinlikte olabilir ve TS bazı durumlarda bunu hesaplamakta zorlanacaktır. TS'nin hayatını kolaylaştırmak için izin verilen yuvalama düzeyini sınırlayacağız. Tüm uygulamalar için 20 yerleştirme seviyesi yeterli olmalıdır ve TS bunu kolayca halledebilir.


Peki özyinelemeli türlerin derinliğini nasıl sınırlandırırsınız? Bu numarayı bu SO cevabından öğrendim; gereksinimlerimize göre değiştirilmiş bir sürüm:

 export type RecursiveMap<T, MaxDepth extends number> = { [key: string]: RecursiveMap_<T, MaxDepth, []>; }; type RecursiveMap_<T, MaxDepth extends number, Stack extends unknown[]> = MaxDepth extends Stack["length"] ? T : T | { [key: string]: RecursiveMap_<T, MaxDepth, [1, ...Stack]> };


Bu bizim ilk karmaşık tipimiz, o yüzden açıklamama izin verin. Burada iki tipimiz var: RecursiveMap bir giriş noktası olarak çalışır ve ona ek bir tuple parametresi ileterek RecursiveMap_ çağırır. Bu demet, eşlemenin derinliğini izlemek için kullanılır; her çağrıda bu diziye bir öğe ekliyoruz.


Ve bu demetin uzunluğu MaxDepth eşit olana kadar onu çağırmaya devam edeceğiz. TS'de, extends aynı zamanda değişmez değerler olarak da adlandırılan belirli değerlerle kullanıldığında (örneğin, özellikle number değil 42 ), 'eşit' anlamına gelir.


Ve hem MaxDepth hem de Stack["length"] belirli sayılar olduğundan, bu kod MaxDepth === Stack["length"] olarak okunabilir. Bu yapının çokça kullanıldığını göreceksiniz.


Neden sadece sayı eklemek yerine Tuple kullanıyorsunuz? TypeScript'te iki sayıyı eklemek o kadar kolay değil! Bunun için tam bir kütüphane var ve Hotscript de sayılar ekleyebilir, ancak çok fazla kod gerektirir (görmeseniz bile), bu da aşırı kullanıldığında TS sunucunuzu ve kod düzenleyicinizi yavaşlatabilir.


Dolayısıyla benim temel kuralım, karmaşık türlerden makul ölçüde mümkün olduğunca kaçınmaktır.


Bu yardımcı program türüyle eşlememizi şu kadar basit bir şekilde tanımlayabiliriz:

 export type RawRoutesMap = RecursiveMap<RawRoute, 20>;


Ham rota türleri için hepsi bu kadar. Sırada bir sonraki çözümlenmiş rotadır. Ayrıştırılmış rota, birkaç ek alan ve bir işlev içeren yalnızca bir JavaScript nesnesidir; işte nasıl göründüğü:

 export type ParsedRoute<R extends RawRoute> = { keys: RouteParam<R>[]; build(...params: PathConstructorParams<R>): Path<R>; raw: R; ambiguousness: number, pattern: RegExp; };


Bunu keys alanından açmaya başlayalım. Bu sadece bu rota için gerekli olan bir dizi parametredir. İşte nasıl yapıldığı:

 import { Pipe, Strings, Tuples } from "hotscript"; export type RouteParam< Route extends RawRoute, > = Pipe< Route, [ Strings.Split<"/">, Tuples.Filter<Strings.StartsWith<":">>, Tuples.Map<Strings.TrimLeft<":">>, Tuples.ToUnion ] >;


Hotscript'te bir işlevi çağırmanın iki yolu vardır: Call veya Pipe . Tek bir işlevi çağırmanız gerektiğinde Call kullanışlıdır, ancak bizim durumumuzda bunlardan 4 tane var! Pipe girişi kabul eder ve onu sağlanan bir demetin ilk işlevine yönlendirir.


Döndürülen değer ikinci fonksiyona girdi olarak iletilir ve bu şekilde devam eder. Bizim durumumuzda, örneğin ham rota /user/:userId/posts/:postId olsaydı, şu şekilde dönüştürülürdü:

 export type Beep = Pipe< "/user/:userId/posts/:postId", [ Strings.Split<"/">, // ["user", ":userId", "posts", ":postId"] Tuples.Filter<Strings.StartsWith<":">>, // [":userId", ":postId"] Tuples.Map<Strings.TrimLeft<":">>, // ["userId", "postId"] Tuples.ToUnion // "userId" | "postId" ] >;


Görmek? Bu tür düzeyinde programlamanın büyüsüdür! Şimdi bu build fonksiyonunu ele alalım. Rota parametrelerini ( userId ve postId gibi) ve isteğe bağlı arama parametrelerini/karmalarını kabul eder ve bunları bir yolda birleştirir. PathConstructorParams uygulamasına bir göz atın:

 // Allows us to also accept number and // any other type which can be converted into string export type StringLike = { toString: () => string }; export type SearchAndHashPathConstructorParams = { hash?: string, search?: string | { [key: string]: string, } }; export type RouteParamsMap< Route extends RawRoute, Val extends string | StringLike = string, > = { [key in RouteParam<Route>]: Val }; export type PathConstructorParams<R extends RawRoute> = | [RouteParamsMap<R, StringLike>] | [RouteParamsMap<R, StringLike>, SearchAndHashPathConstructorParams];


Fonksiyon parametreleri bir dizi olarak tanımlanır (bu daha sonra ...

build işlevi), burada ilk öğe RouteParamsMap ve ikincisi isteğe bağlı SearchAndHashPathConstructorParams . build değerini döndürmeye ne dersiniz? Yolunu zaten belirledik ama onu TypeScript ile nasıl tanımlarsınız?


Bu, RouteParam oldukça benzer, ancak biraz daha fazla jimnastik türü gerektirir!

 import { Fn } from "hotscript"; interface ReplaceParam extends Fn { return: this["arg0"] extends `:${string}` ? string : this["arg0"]; } // Leading slashes will be removed by Split<"/">, so we need to // add them back after our manipulations type Pathname< Route extends RawRoute, > = `/${Pipe< Route, [ Strings.Split<"/">, Tuples.Map<ReplaceParam>, Tuples.Join<"/"> ] >}${Route extends `${string}/` ? '/' : ''}`; export type Path< Route extends RawRoute, > = Pathname<Route> | `${Pathname<Route>}?${string}` | `${Pathname<Route>}#${string}`;


Burada yaptığımız şey rotamızı segmentlere ayırmak, her segmentin haritasını çıkarmak ve her birinde özel fonksiyonumuz ReplaceParam çağırmak. Geçerli segmentin bir parametre olup olmadığını kontrol eder ve onu string değiştirir veya segmenti olduğu gibi döndürür. ReplaceParam 'işlevi' biraz garip görünebilir, ancak Hotscript ile özel işlevleri bu şekilde tanımlarsınız.


Yolun ya sadece yoldan, ardından soru işaretinin geldiği yoldan (bu, arama parametreleri ve karma içeren URL'leri kapsar) ya da bir karma sembolünden (bu, arama parametreleri olmayan ancak karma içeren URL'leri kapsar) oluştuğunu açıkça belirtiyoruz.


Ayrıca eşleşen rotayı, yani URL'den alınan parametrelerle ayrıştırılmış rotayı tanımlamak için bir türe ihtiyacımız olacak:

 // Interface (and not type) because we need to use `this` export interface RouteWithParams<R extends RawRoute> { route: ParsedRoute<R>, params: RouteParamsMap<R>, // TS can't properly infer type of route object with simple // check like currentRoute.route === routes.user.byId, so we // need our custom type guard matches: <T extends RawRoute>(route: ParsedRoute<T>) => this is RouteWithParams<T>, }


Son tür ParsedRoutesMap ; RawRoutesMap benzer, ancak ayrıştırılmış rotalar için.

 // This accepts RawRoutesMap and transforms it into // mapping of parsed routes of same shape export type ParsedRoutesMap<RM extends RawRoutesMap> = { [Key in keyof RM]: RM[Key] extends RawRoute ? ParsedRoute<RM[Key]> : RM[Key] extends RawRoutesMap ? ParsedRoutesMap<RM[Key]> : never; };


Ve bu notta türlerle bitiriyoruz. Birkaç tane daha olacak, ancak bunlar daha basit ve uygulamaya ilerledikçe bunları ele alacağız. Tür düzeyinde programlama daha fazlasını denemek istediğiniz bir şeyse, daha fazla bilgi edinmek ve tür zorluklarını çözmeye çalışmak için Tür düzeyinde TypeScript'e göz atabilirsiniz (onların da iyi bir kaynak listesi vardır).

Rota Ayrıştırıcı

Son olarak, normal değer düzeyinde kodlamaya geri döndük. defineRoutes uygulayarak işleri yoluna koyalım.

 export const typedKeys = <const T extends {}> (obj: T) => { return Object.keys(obj) as Array<keyof T>; }; export const defineRoutes = <const T extends RawRoutesMap>(routesMap: T): ParsedRoutesMap<T> => { const entries = typedKeys(routesMap).map((key) => { const entry = routesMap[key]; if (typeof entry === 'string') { return [key, parseRoute(entry)] as const; } else { // Nested map return [key, defineRoutes(entry)] as const; } }); return Object.fromEntries(entries); };


Burada karmaşık bir şey yok; parseRoute işlevine daha derinlemesine bakalım.

 import { parse, inject, type RouteParams as RegexRouteParams } from "regexparam"; export class InvalidRoute extends Error { }; export class InvalidRouteParams extends Error { }; const parseRoute = <const R extends RawRoute>(route: R): ParsedRoute<R> => { if (!route.startsWith('/')) { throw new InvalidRoute('route should start with slash (/)') } const { keys, pattern } = parse(route); const hasRequiredParams = keys.length > 0; const parsedRoute: ParsedRoute<R> = { build(...args) { const params = ( hasRequiredParams ? args[0] : undefined ) as RouteParamsMap<R, StringLike> | undefined; const searchAndHash = ( hasRequiredParams ? args[1] : args[0] ) as SearchAndHashPathConstructorParams | undefined; if (hasRequiredParams) { if (!params) { throw new InvalidRouteParams( `Parameters for route ${route} weren't provided` ); } const missingKeys = keys.filter(k => !(k in params)); if (missingKeys.length) { throw new InvalidRouteParams( `Missing parameters for route ${route}: ${missingKeys.join(', ')}` ); } } else if (args.length > 1) { throw new InvalidRouteParams( `Route ${route} doesn't accept any parameters, received ${args[0]}` ); } let path = hasRequiredParams ? inject(route, params as RegexRouteParams<R>) : route; if (searchAndHash && searchAndHash.search) { if (typeof searchAndHash.search === 'string') { path += searchAndHash.search.startsWith('?') ? searchAndHash.search : '?' + searchAndHash.search; } else { path += '?' + new URLSearchParams(searchAndHash.search).toString(); } } if (searchAndHash && searchAndHash.hash) { path += searchAndHash.hash.startsWith('#') ? searchAndHash.hash : '#' + searchAndHash.hash; } return path as Path<R>; }, raw: route, keys: keys as RouteParam<R>[] || [], ambiguousness: keys.length, pattern: pattern, }; return parsedRoute; };


parseRoute , gözle görülür derecede daha uzun olmasına rağmen çok basittir. Rotayı ayrıştırmak ve parametreleri çıkarmak için regexparam kütüphanesini kullanırız. Rota için gereken bir dizi parametreyi elde etmemizi sağlar ve daha sonra URL'yi rotayla eşleştirmek için kullanacağımız düzenli bir ifade üretir.


Bu bilgiyi, bu nesneyi oluşturmak için kullanılan orijinal ham rota ve belirsizlik düzeyi (bu, rotadaki yalnızca bir dizi parametredir) ile birlikte saklarız.

Geçmiş Paketleyici

Her yönlendiricinin durumunu bir yerde saklaması gerekir. Tarayıcıdaki uygulamalarda bu aslında 4 seçeneğe indirgenir: bellek içi (kök bileşenin içindeki bir durum değişkeninde veya bileşenler ağacının dışındaki bir değişkende), Geçmiş API'si veya URL'nin karma kısmı.


Kullanıcıya rotalarınızın olduğunu hiç göstermek istemiyorsanız, örneğin React'te bir oyun kodluyorsanız, bellek içi yönlendirme sizin seçiminiz olabilir. React uygulamanız daha büyük bir uygulamada yalnızca bir sayfa olduğunda rotayı karma olarak saklamak kullanışlı olabilir ve URL'yi istediğiniz gibi değiştiremezsiniz.


Ancak çoğu durumda History API'yi kullanmak en iyi seçenek olacaktır. SSR ile uyumludur (diğer seçenekler değildir), kullanıcının alışık olduğu davranış kalıplarını takip eder ve en temiz görünür. Bu projemizde de onu kullanacağız. Ancak dikkate değer bir kusuru var: ek paketleyiciler olmadan çoğunlukla kullanılamaz.


History AP ile popstate etkinliğine abone olabilirsiniz; tarayıcı, URL değiştiğinde size bilgi verecektir. Ancak yalnızca değişiklik kullanıcı tarafından örneğin geri düğmesine basılarak başlatıldığında. Bir URL değişikliği koddan başlatıldıysa bunu kendiniz takip etmeniz gerekir.


İncelediğim yönlendiricilerin çoğu kendi sarmalayıcılarını kullanıyor: react-router ve chicane kullanım geçmişi NPM paketi, TanStack yönlendiricinin kendi uygulaması var ve wouter'ın tam teşekküllü bir sarmalayıcısı yok ama yine de maymun yama geçmişine sahip olması gerekiyor.


O halde kendi sarmalayıcımızı uygulayalım.

 export type HistoryLocation = Pick<Location, | 'origin' | 'href' | 'hash' | 'search' | 'pathname' >; export type NavigationBlocker = (isSoftNavigation: boolean) => boolean; export const createHistory = () => { const winHistory = window.history; const winLocation = window.location; const getLocation = (): HistoryLocation => { return { origin: winLocation.origin, href: winLocation.href, pathname: winLocation.pathname, search: winLocation.search, hash: winLocation.hash, }; }; /* Some magic code */ return /* something... */; };


Kullanacağımız iki tür var: HistoryLocation ve NavigationBlocker . Birincisi, yerleşik Location türünün biraz sınırlı bir sürümüdür (bu, window.location türüdür) ve ikincisi, gezinme engellemeye geldiğimizde ele alınacaktır. Bu bölümdeki diğer tüm kodlar createHistory fonksiyonunun içine girecektir.


Geçmiş değişikliklerine abonelik uygulamaya başlayalım. Bu projede abone olmak için React tarzı işlevler kullanacağız: bir geri aramayı geçerek subscribe çağrısı yaparsınız ve bu, aboneliğinizi iptal etmek istediğinizde çağırmanız gereken başka bir işlevi döndürür.

 const subscribers: Set<VoidFunction> = new Set(); const onChange = () => { subscribers.forEach(fn => { try { fn(); } catch (err) { console.error('Error while handling location update', err); } }) }; const subscribe = (listener: VoidFunction) => { subscribers.add(listener); return () => { subscribers.delete(listener); }; };


Bir sonraki adım, programlı olarak yapılan değişiklikler de dahil olmak üzere konum değişikliklerine tepki vermektir. Nasıl yapardın? Elbette maymun yamalamayla . Bu biraz kirli görünebilir (ve gerçekten de öyle), ancak ne yazık ki daha iyi seçeneklerimiz yok.

 const origPushState = winHistory.pushState.bind(winHistory); const origReplaceState = winHistory.replaceState.bind(winHistory); winHistory.pushState = (data, unused, url) => { // tryNavigate will be covered later tryNavigate(() => { origPushState(data, unused, url); onChange(); }); }; winHistory.replaceState = (data, unused, url) => { tryNavigate(() => { origReplaceState(data, unused, url); onChange(); }); }; // This event is emmited when user initiates navigation // or when calling history.go, history.back and history.forward window.addEventListener('popstate', onChange);


Ve geçmiş uygulamamızdaki son büyük eksik parça, navigasyon engellemedir: navigasyon isteğini durdurmanıza ve onu koşullu olarak iptal etmenize olanak tanıyan bir özellik. Gezinme engellemenin standart bir örneği, kullanıcının bir formdaki ilerlemesini kaybetmesinin önlenmesidir.

 let blockers: NavigationBlocker[] = []; const beforeUnloadHandler = (event: Event) => { const blocked = blockers.some(blocker => blocker(false)); if (blocked) { event.preventDefault(); // @ts-ignore For older browsers event.returnValue = ''; return ''; } }; const tryNavigate = (cb: VoidFunction) => { const blocked = blockers.some(blocker => blocker(true)); if (blocked) return; cb(); }; const addBlocker = (blocker: NavigationBlocker) => { blockers.push(blocker); if (blockers.length === 1) { addEventListener('beforeunload', beforeUnloadHandler, { capture: true }); } return () => { blockers = blockers.filter(b => b !== blocker); if (blockers.length === 0) { removeEventListener('beforeunload', beforeUnloadHandler, { capture: true }); } } };


Bizim uygulamamızda engelleyici, bu gezinmeyi engellememiz gerekip gerekmediğini belirten bir boole değeri döndüren bir işlevdir. Gezinme engellemeyle ilgili olarak iki tür gezinme vardır ve bunları farklı şekilde ele almamız gerekir.


Bir tarafta, kullanıcı uygulamamızdaki bir sayfadan uygulamamızdaki başka bir sayfaya geçtiğinde yumuşak gezinme vardır. Tamamen kontrol ediyoruz ve bu nedenle onu engelleyebilir, herhangi bir özel kullanıcı arayüzünü görüntüleyebilir (kullanıcının amacını doğrulamak için) veya gezinmeyi engelledikten sonra eylemler gerçekleştirebiliriz.


Öte yandan, kullanıcı başka bir siteye gittiğinde veya sekmeyi tamamen kapattığında zor bir gezinme söz konusudur. Tarayıcı, bir güvenlik sorunu oluşturacağından JavaScript'in bu gezinmenin gerçekleştirilip gerçekleştirilmeyeceğine karar vermesine izin veremez. Ancak tarayıcı, kullanıcıya ekstra bir onay iletişim kutusu göstermek isteyip istemediğimizi JavaScript'in belirtmesine izin verir.


Yazılımla gezinmeyi engellerken, ek kullanıcı arayüzü (örneğin, özel onay iletişim kutusu) görüntülemek isteyebilirsiniz, ancak zor gezinme durumunda, kullanıcı bunu yalnızca sayfada kalmaya karar verdiğinde göreceğinden bu gerçekten mantıklı değildir. , bu noktada işe yaramaz ve kafa karıştırıcıdır.


Geçmişimiz gezinme engelleyici işlevini çağırdığında, yumuşak gezinme gerçekleştirip gerçekleştirmediğimizi belirten bir boole değeri sağlayacaktır.


Ve tüm bunlarla birlikte, sadece tarih nesnemizi döndürmemiz gerekiyor:

 return { subscribe, getLocation, push: winHistory.pushState, replace: winHistory.replaceState, go: (distance: number) => tryNavigate(() => winHistory.go.call(winHistory, distance)), back: () => tryNavigate(() => winHistory.back.call(winHistory)), forward: () => tryNavigate(() => winHistory.forward.call(winHistory)), addBlocker, };

1. Adım: Zorunlu API

Nihayet buradayız. Zorunlu API, diğer tüm kancalar ve bileşenler için temel oluşturacak ve geliştiricinin ihtiyaçlarını karşılayacak özel kancalar oluşturmasına olanak tanıyacak. Öncelikle rota haritamızı düz bir diziye dönüştürmemiz gerekiyor. Bu şekilde tüm rotalar üzerinden döngü yapmak çok daha kolay olacak ve bu, rota eşleştirme kısmı üzerinde çalışmaya başladığımızda işimize yarayacak.


Hem tip yardımcı programına ( ParsedRoutesMap ParsedRoute birleşimine dönüştürecek) hem de fonksiyona ( routesMap ayrıştırılmış rotalar dizisine dönüştürecek) ihtiyacımız var. Tiple başlayalım:

 export type Values<T extends {}> = T[keyof T]; type FlattenRouteMap<T> = T extends ParsedRoute<any> | RawRoute ? T : T extends ParsedRoutesMap<RawRoutesMap> | RawRoutesMap ? AllRoutesFromMap<T> : never; export type AllRoutesFromMap< RM extends ParsedRoutesMap<RawRoutesMap> | RawRoutesMap > = FlattenRouteMap<Values<RM>>;


Bunu iki türe ayırmak gereksiz görünebilir ancak bunun çok önemli bir nedeni var: Eğer onu kendisini çağıran tek bir tür olarak uygularsanız, TS türün aşırı derin ve muhtemelen sonsuz olduğundan şikayet edecektir. Bu yüzden, onu birbirini çağıran iki türe bölerek bu sorunu çözmeye çalışıyoruz.


Değer düzeyindeki bir işlev için, iletilen değerin ayrıştırılmış bir yol olup olmadığını kontrol etmek için bir tür koruyucuya da ihtiyacımız olacak.

 export const isParsedRoute = <T extends `/${string}` = `/${string}`>( route: any ): route is ParsedRoute<T> => { return !!route && typeof route === 'object' && typeof route.raw === 'string' && typeof route.build === 'function'; } export const getAllRoutes = <T extends RawRoutesMap>( routesMap: ParsedRoutesMap<T> ): ParsedRoute<AllRoutesFromMap<T>>[] => { type PossibleRawRoute = AllRoutesFromMap<T>; return typedKeys(routesMap).flatMap((k) => { const val = routesMap[k]; if (isParsedRoute<PossibleRawRoute>(val)) { return [val] as const; } // At this point we know that val isn't ParsedRoute, so it has to be map of routes // but TS can't infer that, so we help him a little by casting val to correct type return getAllRoutes(val as ParsedRoutesMap<T>); }); };


Şimdi yönlendiricimizi uygulamaya başlayalım. Tarihte olduğu gibi, bu ve sonraki iki bölümde, aksi belirtilmediği sürece tüm kodlar createRouter işlevine girecektir.

 import { useSyncExternalStore, ComponentType, useMemo, MouseEventHandler, ComponentProps, useEffect } from 'react'; export const createRouter = <T extends RawRoutesMap>(routesMap: ParsedRoutesMap<T>) => { // Type for any possible route from passed routesMap type RouteType = AllRoutesFromMap<T>; // Similar to above, but for matched routes, ie includes URL parameters type BindedRouteWithParams = RouteWithParams<RouteType>; // Some of our functions will accept route filter, // which can be single parsed route, array or object type RouteFilter<T extends RouteType> = | ParsedRoute<T> | ParsedRoute<T>[] | Record<string, ParsedRoute<T>>; const history = createHistory(); const routes = getAllRoutes(routesMap); };


Öncelikle yönlendiricimize mevcut konumu bilinen rotalardan biriyle eşleştirmeyi öğretelim. Birkaç yardımcı program, createRouter işlevinin içine değil, genel kapsama veya ayrı dosyalara girebilir:

 export const filterOutFalsy = <T>(obj: T[]): Exclude<T, undefined>[] => { return obj.filter(Boolean) as Exclude<T, undefined>[]; }; export class RouteMatchingConflict extends Error { }; // This will be used later export class RouteMismatch extends Error { };


Ve bu kod createRouter fonksiyonuna giriyor.

 const extractRouteParams = <T extends RawRoute>( pathname: string, parsedRoute: ParsedRoute<T> ) => { const match = parsedRoute.pattern.exec(pathname); if (!match) return undefined; // Extract all route parameters from match array // and construct object from them return Object.fromEntries(parsedRoute.keys.map((key, index) => { return [key, match[index + 1]]; })) as RouteParamsMap<T>; }; const findMatchingRoute = ( location: HistoryLocation ): BindedRouteWithParams | undefined => { const matchingRoutes = filterOutFalsy(routes.map(route => { const params = extractRouteParams<RawRoute>( location.pathname, route ); if (!params) return undefined; return { route, params, matches<T extends RawRoute>(r: ParsedRoute<T>) { return route === r; }, }; })); if (matchingRoutes.length === 0) return undefined; if (matchingRoutes.length === 1) return matchingRoutes[0]; // At this point we have multiple matching routes :/ // Gotta decide which one we prefer let lowestAmbiguousnessLevel = Infinity; let lowestAmbiguousnessMatches: BindedRouteWithParams[] = []; matchingRoutes.forEach((match) => { if (match.route.ambiguousness === lowestAmbiguousnessLevel) { lowestAmbiguousnessMatches.push(match); } else if (match.route.ambiguousness < lowestAmbiguousnessLevel) { lowestAmbiguousnessLevel = match.route.ambiguousness; lowestAmbiguousnessMatches = [match]; } }); if (lowestAmbiguousnessMatches.length !== 1) { throw new RouteMatchingConflict( `Multiple routes with same ambiguousness level matched pathname ${location.pathname}: ${lowestAmbiguousnessMatches.map(m => m.route.raw).join(', ')}` ); } return lowestAmbiguousnessMatches[0]; }; let currentRoute = findMatchingRoute(history.getLocation()); // This function will be later returned from createRouter function const getCurrentRoute = () => currentRoute;


Burada bilinen tüm rotaların üzerinden geçiyoruz ve her birini mevcut konumla eşleştirmeye çalışıyoruz. Rotanın regex'i URL ile eşleşirse rota parametrelerini URL'den alırız, aksi takdirde null değerini alırız. Eşleşen her rota için bir RouteWithParams nesnesi oluşturup onu bir diziye kaydediyoruz. Artık 0 veya 1 eşleşen rotamız varsa her şey basit.


Ancak birden fazla rota mevcut konumla eşleşiyorsa hangisinin daha yüksek önceliğe sahip olduğuna karar vermemiz gerekir. Bunu çözmek için ambiguousness alanını kullanıyoruz. Hatırlayacağınız gibi bu rotanın bir dizi parametresi vardır ve ambiguousness en düşük olan rotaya öncelik verilir.


Örneğin, /app/dashboard ve /app/:section iki rotamız olsaydı, http://example.com/app/dashboard konumu her iki rotayla da eşleşirdi. Ancak bu URL'nin /app/:section değil, / /app/dashboard rotasına karşılık gelmesi gerektiği oldukça açıktır.


Ancak bu algoritma kurşun geçirmez değildir. Örneğin, /app/:user/settings/:section ve /app/dashboard/:section/:region rotalarının her ikisi de http://example.com/app/dashboard/settings/asia URL'siyle eşleşir. Ve aynı belirsizlik seviyesine sahip oldukları için yönlendiricimiz hangisine öncelik verilmesi gerektiğine karar veremeyecektir.


Şimdi konum değişikliklerine tepki vermek ve currentRoute değişkenini güncellemek için bu kodu birbirine yapıştırmamız gerekiyor;

 const areRoutesEqual = <A extends RawRoute, B extends RawRoute>( a: RouteWithParams<A> | undefined, b: RouteWithParams<B> | undefined ): boolean => { if (!a && !b) return true; // Both are undefined if ((!a && b) || (a && !b)) return false; // Only one is undefined if (!a!.matches(b!.route)) return false; // Different routes // Same routes, but maybe parameters are different? const allParamsMatch = a.route.keys.every(key => a.params[key] === b!.params[key]); return allParamsMatch; }; history.subscribe(() => { const newRoute = findMatchingRoute(history.getLocation()); if (!areRoutesEqual(newRoute, currentRoute)) { currentRoute = newRoute; notifyRouteChange(); // Will be covered later } });


Artık yönlendiricimiz konum değişikliklerine tepki veriyor ve kullanıcı her zaman geçerli rotayı alabiliyor, yaşasın! Ancak rota değişikliklerine abone olma özelliği olmadan pek kullanışlı değil, o yüzden şunu da ekleyelim. Yaklaşım geçmiş sarmalayıcıda kullandığımız yaklaşıma çok benzer.

 const subscribers: Set<VoidFunction> = new Set(); const subscribe = (cb: VoidFunction) => { subscribers.add(cb); return () => void subscribers.delete(cb); }; const notifyRouteChange = () => { subscribers.forEach(cb => { try { cb(); } catch (err) { console.error('Error in route change subscriber', err); } }); };


Gezinmeyi gerçekleştirmek için, history.push ve history.replace etrafında basit bir sarmalayıcı olan navigate ve navigateUnsafe işlevlerini açığa çıkaracağız:

 // This function accepts any string path (no type-safety) const navigateUnsafe = ( path: string, { action = 'push' }: { action?: 'push' | 'replace' } = {} ) => { history[action]({}, '', path) }; // And this function accepts only paths that correspond to one of routes const navigate = ( path: Path<RouteType>, options: { action?: 'push' | 'replace' } = {} ) => { navigateUnsafe(path, options); };


İşte bu gerçek bir yönlendirici! Çok basit ama yine de çalışıyor. Hala uygulamamız gereken bazı kancalar ve bileşenler var, ancak buradan sonra her şey çok daha kolaylaşıyor.

Adım 2: Kancalar

Kancalar için geçerli konumu ve geçerli rotayı döndüren basit kancalarla başlayabiliriz. Kendi başlarına oldukça kolaydırlar, ancak UseSyncExternalStore bunları tek satırlık bir hale dönüştürür. Zorunlu API'mizi daha önce tasarlama şeklimiz, bu kancalara ilişkin kodu büyük ölçüde azaltmamıza olanak tanıdı.

 const useLocation = () => { return useSyncExternalStore(history.subscribe, history.getLocation); }; const useCurrentRoute = () => { return useSyncExternalStore(subscribe, getCurrentRoute); };


Yalnızca belirli bir rotada/rota kümesinde oluşturulması gereken bileşenleri kodlarken, geçerli rotayı almak için useCurrentRoute kullanabilir, kriterlere uyup uymadığını kontrol edebilir ve ardından parametrelerini kullanabilirsiniz (veya bir hata oluşturabilirsiniz).


Ancak bu o kadar yaygın bir görevdir ki, kullanıcılarımızın bunun için kendi kancalarını yazmalarını sağlamak suç olacaktır - yönlendiricimiz bunu kutudan çıkar çıkmaz sağlamalıdır.

 function useRoute<T extends RouteType>(filter: RouteFilter<T>, strict?: true): RouteWithParams<T>; function useRoute<T extends RouteType>(filter: RouteFilter<T>, strict: false): RouteWithParams<T> | undefined; function useRoute<T extends RouteType>(filter: RouteFilter<T>, strict?: boolean): RouteWithParams<T> | undefined { const currentRoute = useCurrentRoute(); const normalizedFilter = Array.isArray(filter) ? filter : isParsedRoute(filter) ? [filter] : Object.values(filter); const isMatching = !!currentRoute && normalizedFilter.some(route => currentRoute.matches(route)); if (isMatching) return currentRoute as RouteWithParams<T>; else { if (strict === false) return undefined; throw new RouteMismatch( `Current route doesn't match provided filter(s)` ); } }


Bu kancanın iki versiyonu vardır: katı ve rahat. Kullanıcı ikinci bir parametre olarak true iletirse (veya herhangi bir şeyi iletmezse, çünkü true varsayılan değerdir), mevcut rota sağlanan filtrelerden biriyle eşleşmezse bu kanca bir hata verecektir.


Bu şekilde kancanın eşleşen bir rotaya döneceğinden veya hiç dönmeyeceğinden emin olabilirsiniz. İkinci parametre yanlışsa, bir istisna atmak yerine, geçerli rotanın filtrelerle eşleşmemesi durumunda kanca, yalnızca tanımsız değerini döndürecektir.


Bu davranışı TypeScript'e açıklamak için, işlev aşırı yükleme adı verilen bir özelliği kullanırız. Bu, farklı türlerde birden fazla işlev tanımı tanımlamamıza olanak tanır ve TypeScript, kullanıcı böyle bir işlevi çağırdığında kullanılacak olanı otomatik olarak seçecektir.


Yol parametrelerine ek olarak, arama parametrelerinde bazı veriler de iletilebilir; bu nedenle bunları dizeden eşlemeye ayrıştırmak için bir kanca ekleyelim. Bunun için yerleşik tarayıcı API'si URLSearchParams'ı kullanacağız.

 const useSearchParams = () => { const location = useLocation(); return useMemo(() => { return Object.fromEntries( (new URLSearchParams(location.search)).entries() ); }, [location.search]); };


Ve bu bölümdeki son kanca da oldukça basit olan useNavigationBlocker : yalnızca geri aramayı kabul eder ve değişirse engelleyiciyi yeniden eklemek için history.addBlocker yapılan çağrıları bir efekte sarar.

 const useNavigationBlocker = (cb: NavigationBlocker) => { useEffect(() => { return history.addBlocker(cb); }, [cb]); };


Şimdi bileşenlere geçelim!

Adım 3: Bileşenler

Yönlendirme kütüphaneleri denilince akla gelen ilk bileşen nedir? Eminim Route ya da en azından benzer bir şeydir. Daha önce gördüğünüz gibi, tüm ağır işleri yapan, iyi tasarlanmış zorunlu bir API nedeniyle kancalarımız çok basitti.


Aynı şey bileşenler için de geçerlidir; kullanıcı tarafından birkaç satır kodla kolayca uygulanabilir. Ama biz ciddi bir yönlendirme kütüphanesiyiz, hadi kutuya pilleri dahil edelim :)

 type RouteProps = { component: ComponentType, match: RouteFilter<RouteType> }; const Route = ({ component: Component, match }: RouteProps) => { const matchedRoute = useRoute(match, false); if (!matchedRoute) return null; return (<Component />); };


Eh, bu kolaydı! NotFound bileşenini nasıl uyguladığımızı tahmin etmek ister misiniz? :)

 type NotFoundProps = { component: ComponentType }; const NotFound = ({ component: Component }: NotFoundProps) => { const currentRoute = useCurrentRoute(); if (currentRoute) return null; return (<Component />); };


Yönlendiricimiz için gereken son bileşen ise biraz daha karmaşık olan Link . Her zaman zorlu gezinmeyi başlatacağından ve herhangi bir tür güvenliği sağlamadığından yalnızca <a href="/app/dashboard" /> kullanamazsınız. O halde bu sorunları ele alalım:

 type LinkProps = Omit<ComponentProps<"a">, 'href'> & ( // Our link accepts either type-strict href // or relaxed unsafeHref { href: Path<RouteType>, unsafeHref?: undefined } | { href?: undefined, unsafeHref: string } ) & { action?: 'push' | 'replace' }; const Link = ({ action = 'push', onClick, href, unsafeHref, ...props }: LinkProps) => { const hrefToUse = (href ?? unsafeHref)!; const targetsCurrentTab = props.target !== '_blank'; const localOnClick: MouseEventHandler<HTMLAnchorElement> = (event) => { if (onClick) { onClick(event); if (event.isDefaultPrevented()) { // User-defined click handler cacnelled navigation, we should exit too return; } } const inNewTab = !targetsCurrentTab || event.ctrlKey || event.shiftKey || event.metaKey || event.button === 1; if (!isExternal && !inNewTab) { event.preventDefault(); navigateUnsafe(hrefToUse, { action }); } }; const isExternal = useMemo(() => { if (!hrefToUse) return false; return new URL(hrefToUse, window.location.href).origin !== location.origin; }, [hrefToUse]); return <a {...props} href={hrefToUse} onClick={localOnClick} /> };


navigate işlevine benzer şekilde Link bileşeni, kendisine ilettiğiniz URL'yi kontrol eder ancak aynı zamanda isteğe bağlı bir dize URL'si (kaçış taraması olarak veya harici bağlantılar için) sağlamanıza da olanak tanır. <a> davranışını geçersiz kılmak için kendi onClick dinleyicimizi ekliyoruz ve bunun içine orijinal onClick ( Link bileşenimize iletilen) çağırmamız gerekecek.


Bundan sonra, navigasyonun geliştirici tarafından iptal edilip edilmediğini kontrol ederiz (eğer öyleyse, olayı görmezden gelmeliyiz). Her şey yolundaysa, bağlantının harici olup olmadığını ve mevcut sekmede açık olması gerekip gerekmediğini kontrol ederiz. Ve ancak o zaman yerleşik sabit navigasyonu iptal edebilir ve bunun yerine kendi navigateUnsafe fonksiyonumuzu çağırabiliriz.


Ve şimdi, createRouter işlevinden tüm işlevlerimizi, kancalarımızı ve bileşenlerimizi (doğrudan geçmişten yeniden aktarılan birkaç işlevle birlikte) döndürmemiz gerekiyor.

 return { // Directly re-exported from history go: history.go, back: history.back, forward: history.forward, addBlocker: history.addBlocker, getLocation: history.getLocation, subscribeToLocation: history.subscribe, // Imperative API subscribe, getCurrentRoute, navigate, navigateUnsafe, // Hooks useLocation, useCurrentRoute, useRoute, useSearchParams, useNavigationBlocker, // Components Link, Route, NotFound, };


Ve bununla yönlendiricimiz tamamlandı. Şimdi, az önce yaptığımız minicik yönlendiricimizi sergilemek için minicik uygulamamızı oluşturabiliriz! Bu arada, bu yönlendiricinin tam kodunu (örnek uygulamanın kodu dahil) burada bulabilirsiniz.

Yapboz Parçalarını Birleştirme

Peki tüm bu kodlar nasıl birbirine bağlanıyor? Oldukça düzgün bir şekilde, eğer kendim de söylersem! Bir not alma uygulaması yaptığınızı düşünün. Öncelikle rotaları tanımlayarak ve şöyle bir yönlendirici oluşturarak başlarsınız:

 export const routes = defineRoutes({ // Yep, this will be very minimal app root: '/', newNote: '/new', note: '/note/:noteId', }); const router = createRouter(routes); // Export functions and component you will be using export const { navigate, Link, Route, NotFound, useRoute, useNavigationBlocker } = router;


Daha sonra tanımladığınız rotaları sayfa bileşenlerine bağlarsınız:

 function App() { return ( <div className="app"> <div className="links"> {/* This is how you build URL for Link */} <Link href={routes.root.build({})}>View all notes</Link> <Link href={routes.newNote.build({})}>Create new</Link> </div> <Route match={routes.root} component={NotesListPage} /> <Route match={routes.newNote} component={NewNotePage} /> <Route match={routes.note} component={NoteDetailsPage} /> <NotFound component={NotFoundPage} /> </div> ) }


NoteDetailsPage içindeyken, URL'den bir not kimliği almanız gerekir, böylece bir useRoute kancası kullanırsınız:

 export const NoteDetailsPage = () => { const { getNote } = useNotes(); const { params } = useRoute(routes.note); const note = getNote(params.noteId); return note ? (<> <h1>{note.title}</h1> <div>{note.text}</div> </>) : (<h1>Not found</h1>); };


Yeni bir not oluştururken, kullanıcının notu kaydetmeden gezinmesi durumunda muhtemelen niyetini onaylamak istersiniz:

 export const NewNotePage = () => { const saveNote = () => { isSubmittingNoteRef.current = true; const note = createNote(title, text); // And this is programmatic redirect navigate(routes.note.build({ noteId: note.id })); isSubmittingNoteRef.current = false; }; const [title, setTitle] = useState(''); const [text, setText] = useState(''); const { createNote } = useNotes(); const isSubmittingNoteRef = useRef(false); useNavigationBlocker((isSoftNavigation) => { const dirty = title !== '' || text !== ''; if (!dirty || isSubmittingNoteRef.current) return false; if (isSoftNavigation) { const confirmation = confirm('Do you want to leave?'); return !confirmation; } else { return true; } }); return <> <h1>New note</h1> <input placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} /> <textarea placeholder="Text" value={text} onChange={e => setText(e.target.value)} /> <button onClick={saveNote}>Save</button> </>; };

Olası İyileştirmeler

Yönlendiricimiz gerçekten yönlendirme yapsa da TanStack yönlendirici, tepki yönlendirici veya Next.js yönlendirici gibi üretime hazır çözümlerle eşleşemez. Demek istediğim, sadece ~ 500 satırlık kod, bu fazla değil. Peki tam olarak eksik olan ne?


Her şeyden önce, Sunucu Tarafı Oluşturma. Bugün tüm uygulamaların SSR'ye ihtiyacı olmayabilir, ancak tüm yönlendirme kitaplıklarının bunu desteklemesi bekleniyor. Bir dizeye sunucu tarafı oluşturma eklemek (SSR akışı değil!), geçerli konumu bellekte saklayacak (sunucuda Geçmiş API'si olmadığından) farklı bir history oluşturmayı ve bunu createRouter işlevine takmayı içerecektir.


Akışlı SSR'yi uygulamanın ne kadar zor olacağının farkında değilim, ancak bunun Suspense desteğiyle güçlü bir şekilde bağlantılı olacağını varsayıyorum.


İkincisi, bu yönlendirici eşzamanlı işlemeyle iyi bir şekilde entegre olmuyor. Çoğunlukla useSyncExternalStore kullanımımız nedeniyle, engellenmeyen geçişlerle uyumlu değildir. Yırtılmayı önlemek için bu şekilde çalışır: Kullanıcı arayüzünün bir kısmının belirli bir depolama değeriyle işlendiği, ancak kullanıcı arayüzünün geri kalanının farklı bir değerle işlendiği bir durum.


Bu nedenle, yönlendirici Suspense ile iyi bir şekilde entegre olmuyor, çünkü askıya alınan her konum güncellemesinde bir geri dönüş gösterilecek. Bu arada, bu makalede React'te eşzamanlılığı ele aldım ve bu makalede Suspense, veri getirme ve kanca use bahsediyorum.


Ancak bu olumsuzluklara rağmen, umarım bu makaleyi ilginç bulmuşsunuzdur ve bu arada kendi yönlendiricinizi kurmuşsunuzdur :)