Bu, Redux kodu yazmak için resmi stil kılavuzudur.
Redux uygulamaları yazmak için önerilen kalıplarımızı, en iyi uygulamalarımızı ve önerilen yaklaşımlarımızı listeler.
Hem Redux çekirdek kitaplığı hem de Redux belgelerinin çoğu fikirsizdir. Redux'u kullanmanın birçok yolu vardır ve çoğu zaman işleri yapmanın tek bir "doğru" yolu yoktur.
Ancak zaman ve deneyim, bazı konularda bazı yaklaşımların diğerlerinden daha iyi çalıştığını göstermiştir. Ayrıca birçok geliştirici, karar yorgunluğunu azaltmak için resmi rehberlik sağlamamızı istedi.
Bunu göz önünde bulundurarak, hatalardan,boşa kürek çekmekten ve anti-desenlerden kaçınmanıza yardımcı olmak için bu öneriler listesini bir araya getirdik. Ayrıca ekip tercihlerinin farklı olduğunu ve farklı projelerin farklı gereksinimleri olduğunu anlıyoruz, bu nedenle hiçbir stil kılavuzu her zaman uymaz. Bu tavsiyelere uymanız teşvik edilir, ancak kendi durumunuzu değerlendirmek ve ihtiyaçlarınıza uyup uymadıklarına karar vermek için zaman ayırın.
Kural Kategorileri
Bu kuralları üç kategoriye ayırdık:
Öncelik A: Essential :
Bu kurallar hataları önlemeye yardımcı olur, bu nedenle ne pahasına olursa olsun bunları öğrenin ve bunlara uyun. İstisnalar olabilir, ancak bunlar çok nadirdir ve yalnızca hem JavaScript hem de Redux konusunda uzman bilgisi olanlar tarafından yapılmalıdır.
Bu kuralların çoğu projede okunabilirliği ve/veya geliştirici deneyimini iyileştirdiği bulunmuştur. Bunları ihlal ederseniz kodunuz çalışmaya devam eder, ancak ihlaller nadir ve haklı olmalıdır. Makul olarak mümkün olduğunda bu kurallara uyun.
Birden fazla, eşit derecede iyi seçeneğin mevcut olduğu durumlarda, tutarlılığı sağlamak için keyfi bir seçim yapılabilir. Bu kurallarda, kabul edilebilir her seçeneği açıklıyoruz ve varsayılan bir seçenek öneriyoruz. Bu, tutarlı olduğunuz ve iyi bir nedeniniz olduğu sürece, kendi kod tabanınızda farklı bir seçim yapmakta özgür hissedebileceğiniz anlamına gelir. Lütfen yine de iyi bir nedeniniz olsun!
Öncelik A: Essential
Do Not Mutate State
Mutasyona uğrayan durum, bileşenlerin düzgün bir şekilde yeniden oluşturulamaması da dahil olmak üzere Redux uygulamalarındaki hataların en yaygın nedenidir ve ayrıca Redux DevTools'ta zaman yolculuğu hata ayıklamasını bozacaktır. Hem reducerların içinde hem de diğer tüm uygulama kodlarında state değerlerinin gerçek mutasyonundan her zaman kaçınılmalıdır.
Geliştirme sırasında mutasyonları yakalamak için redux-immutable-state-invariant ve durum güncellemelerinde tesadüfi mutasyonları önlemek için Immer gibi araçları kullanın.
Not: Varolan değerlerin kopyalarını değiştirmek uygundur - bu, değişmez güncelleme mantığı yazmanın normal bir parçasıdır. Ayrıca, değişmez güncellemeler için Immer kitaplığını kullanıyorsanız, gerçek veriler mutasyona uğramadığından "mutasyona uğrama" mantığı yazmak kabul edilebilir - Immer değişiklikleri güvenli bir şekilde izler ve dahili olarak değişmez şekilde güncellenen değerler üretir.
Reducers Must Not Have Side Effects
Reducer fonksiyonlar yalnızca state ve action bağımsız değişkenlerine bağlı olmalıdır ve yalnızca bu bağımsız değişkenlere dayalı olarak yeni bir state değeri hesaplamalı ve döndürmelidir. Herhangi bir asenkron mantık yürütmemeli (AJAX çağrıları, zaman aşımları, vaatler), rastgele stateler üretmemeli (Date.now(), Math.random()) , redüktörün dışındaki değişkenleri değiştirmemeli veya redüktör işlevinin kapsamı dışındaki şeyleri etkileyen diğer kodları çalıştırmamalıdırlar.
Not: Bir reducer, import library veya yardımcı işlevler gibi kendi dışında tanımlanan diğer işlevleri aynı kurallara uydukları sürece çağırması kabul edilebilir.
Detaylı açıklama
Bu kuralın amacı, reducerlar çağrıldığında tahmin edilebilir şekilde davranmasını garanti etmektir. Örneğin, time-travel hata ayıklaması yapıyorsanız, "current" state değerini üretmek için önceki actionlar reducer fonksyionları birçok kez çağrılabilir. Bir indirgeyicinin yan etkileri varsa, bu, bu etkilerin hata ayıklama işlemi sırasında yürütülmesine ve uygulamanın beklenmedik şekillerde davranmasına neden olur.
Bu kuralın bazı gri alanları var. Açıkçası, console.log(state) gibi bir kod bir yan etkidir, ancak pratikte uygulamanın nasıl davrandığı üzerinde hiçbir etkisi yoktur.
Do Not Put Non-Serializable Values in State or Actions
Promises, Symbols, Maps/Sets, functions, veya class instances gibi serileştirilemeyen değerleri Redux deposu durumuna veya gönderilen eylemlere koymaktan kaçının. Bu, Redux DevTools aracılığıyla hata ayıklama gibi özelliklerin beklendiği gibi çalışmasını sağlar. Ayrıca, kullanıcı arayüzünün beklendiği gibi güncellenmesini sağlar.
İstisna: Action, reducer’a ulaşmadan önce bir ara katman yazılımı tarafından araya girilecek ve durdurulacaksa, eylemlere serileştirilemeyen değerler koyabilirsiniz. Redux-thunk ve redux-promise gibi ara yazılımlar bunun örnekleridir.
Standart bir Redux uygulaması, tüm uygulama tarafından kullanılacak yalnızca tek bir Redux store örneğine sahip olmalıdır. Genellikle store.js gibi ayrı bir dosyada tanımlanmalıdır.
İdeal olarak, hiçbir zaman store doğrudan import edilmez. <Provider> aracılığıyla bir React bileşen ağacına geçirilmeli veya thunks gibi ara katman yazılımı aracılığıyla dolaylı olarak başvurulmalıdır. Nadir durumlarda, onu diğer mantık dosyalarına aktarmanız gerekebilir, ancak bu son çare olmalıdır.
Priority B Rules: Strongly Recommended
Use Redux Toolkit for Writing Redux Logic
Redux Toolkit, Redux'u kullanmak için önerilen araç setimizdir. Store mutasyonları yakalayacak ve Redux DevTools Uzantısını etkinleştirecek şekilde ayarlamak, Immer ile değişmez güncelleme mantığını basitleştirmek ve daha fazlası dahil olmak üzere önerilen en iyi uygulamalarımızı oluşturan işlevlere sahiptir.
Use Immer for Writing Immutable Updates
Değiştirilemez güncelleme mantığını elle yazmak genellikle zordur ve hatalara açıktır. Immer, "mutative" mantığı kullanarak daha basit değişmez güncellemeler yazmanıza izin verir ve hatta uygulamanın başka yerlerinde mutasyonları yakalamak için geliştirme durumunuzu dondurur. Tercihen Redux Toolkit'in bir parçası olarak, değişmez güncelleme mantığı yazmak için Immer'ı kullanmanızı öneririz.
Structure Files as Feature Folders with Single-File Logic
Redux, uygulamanızın klasör ve dosyalarının nasıl yapılandırıldığıyla ilgilenmez. Bununla birlikte, belirli bir özelliğin tek bir yerde birlikte konumlandırılması mantığı, genellikle bu kodun bakımını kolaylaştırır.
Bu nedenle, çoğu uygulamanın dosyaları bir "feature folder" yaklaşımını (aynı klasördeki bir özelliğe ilişkin tüm dosyalar) kullanarak yapılandırmasını öneririz. Belirli bir feature folder içinde, o özelliğin Redux mantığı, tercihen Redux Toolkit createSlice API kullanılarak tek bir "slice" dosyası olarak yazılmalıdır. (Bu aynı zamanda "ducks" patterni olarak da bilinir). Daha eski Redux kod tabanları genellikle "actions" ve "reducers" için ayrı klasörler içeren "folder-by-type" yaklaşımını kullanırken, ilgili mantığı bir arada tutmak bu kodu bulmayı ve güncellemeyi kolaylaştırır.
Ayrıntılı Açıklama: Örnek Klasör Yapısı
Örnek bir klasör yapısı şöyle görünebilir:
/src
index.tsx: React bileşen ağacını oluşturan giriş noktası dosyası
/store
store.ts: mağaza kurulumu
rootReducer.ts: root reducer (isteğe bağlı)
App.tsx: root React bileşeni
/common: hooks, genel componentler, yardımcı programlar, vb.
/features: tüm "özellik klasörlerini" içerir
/todos: tek bir özellik klasörü
todosSlice.ts: Redux reducer mantığı ve ilgili eylemler
Todos.tsx: bir React bileşeni
/app, diğer tüm klasörlere bağlı olan uygulama çapında kurulum ve düzeni içerir.
/common gerçekten genel ve yeniden kullanılabilir yardımcı programları ve bileşenleri içerir.
/features, belirli bir özellikle ilgili tüm işlevleri içeren klasörlere sahiptir. Bu örnekte, todosSlice.ts, RTK'nın createSlice() işlevine bir çağrı içeren ve dilim azaltıcıyı ve eylem oluşturucuları dışa aktaran "ducks" tarzı bir dosyadır.
Put as Much Logic as Possible in Reducers
Mümkün olan her yerde, action’ı dispatch eden ve hazırlayan kodda (bir click handler gibi) yeni bir state hesaplama mantığının çoğunu uygun reducer’a koymaya çalışın. Bu, gerçek uygulama mantığının daha fazlasının kolayca test edilebilir olmasını sağlamaya yardımcı olur, time-travel hata ayıklamasının daha etkili kullanılmasını sağlar ve mutasyonlara ve hatalara yol açabilecek yaygın hatalardan kaçınmaya yardımcı olur.
Yeni state’in bir kısmının veya tamamının önce hesaplanması gerektiği (unique id oluşturmak gibi), ancak bunun minimumda tutulması gereken geçerli durumlar vardır.
Detaylı açıklama
Redux çekirdeği aslında yeni bir durum değerinin reducerda mi yoksa action oluşturma mantığında mı hesaplandığıyla ilgilenmez. Örneğin, bir yapılacaklar uygulaması için, bir "toggle todo" eyleminin mantığı, bir yapılacaklar dizisinin değişmez bir şekilde güncellenmesini gerektirir. Eylemin yalnızca yapılacaklar idsini içermesi ve reducer’daki yeni diziyi hesaplaması gerekir.
// Click handler: const onTodoClicked = (id) => { dispatch({type: "todos/toggleTodo", payload: {id}}) } // Reducer: case "todos/toggleTodo": { return state.map(todo => { if(todo.id !== action.payload.id) return todo; return {...todo, completed: !todo.completed }; }) }
Ayrıca önce yeni diziyi hesaplamak ve tüm yeni diziyi eyleme geçirmek için:
// Click handler: const onTodoClicked = id => { const newTodos = todos.map(todo => { if (todo.id !== id) return todo return { ...todo, completed: !todo.completed } }) dispatch({ type: 'todos/toggleTodo', payload: { todos: newTodos } }) } // Reducer: case "todos/toggleTodo": return action.payload.todos;
Bununla birlikte, mantığın reducerda yapılması birkaç nedenden dolayı tercih edilir:
- Redüktörleri test etmek her zaman kolaydır, çünkü bunlar saf fonksiyonlardır - sadece const result = reducer(testState, action) çağırırsınız ve sonucun beklediğiniz gibi olduğunu iddia edersiniz. Yani, bir reducer’a ne kadar çok mantık koyarsanız, o kadar çok mantığa sahip olursunuz ve bu kolayca test edilebilir.
- Redux state güncellemeleri her zaman immutable güncellemelerin kurallarına uymalıdır. Çoğu Redux kullanıcısı, bir reducer içindeki kurallara uymaları gerektiğinin farkındadır, ancak yeni durum reducerın dışında hesaplanıyorsa, bunu yapmanız gerektiği de açık değildir. Bu, yanlışlıkla mutasyonlar veya hatta Redux storedan bir değer okuyup onu bir eylemin içine geri gönderme gibi hatalara kolayca yol açabilir. Tüm durum hesaplamalarını bir reducer’da yapmak bu hataları önler.
- Redux Toolkit veya Immer kullanıyorsanız, reducer’da immutabe güncelleme mantığı yazmak çok daha kolaydır ve Immer durumu dondurur ve yanlışlıkla oluşabilecek mutasyonları yakalar.
- Time-travel hata ayıklaması, gönderilen bir action "undo", ardından farklı bir şey yapmanıza veya eylemi "redo" izin vererek çalışır. Ek olarak, reducerlar çalışırken yeniden yüklenmesi normalde yeni reducer’ın mevcut actionlarla yeniden çalıştırılmasını içerir. Doğru bir action varsa ancak bir buggy reducerınız varsa, hatayı düzeltmek için reducer’ı düzenleyebilir, çalışırken yeniden yükleyebilir ve hemen doğru state’e gelmelisiniz. action’ın kendisi yanlışsa, o action’ın gönderilmesine yol açan adımları yeniden çalıştırmanız gerekir. Bu nedenle, reducer’da daha fazla mantık varsa hata ayıklamak daha kolaydır.
- Son olarak, mantığı reducer’lara koymak, uygulama kodunun rastgele diğer bölümlerine dağıtmak yerine güncelleme mantığını nerede arayacağınızı bildiğiniz anlamına gelir.
Reducers Should Own the State Shape
Redux root state’i, tek root reducer işlevine aittir ve bu fonksiyon tarafından hesaplanır. Sürdürülebilirlik için, bu reducer’in key/value "slice"ları ile bölünmesi amaçlanmıştır ve her "slice reducer" bir başlangıç state’i sağlamaktan ve state’in o slice’ına yönelik güncellemeleri hesaplamaktan sorumludur.
Ek olarak, slice reducer, hesaplanan state’in bir parçası olarak başka hangi değerlerin döndürüleceği üzerinde denetim yapmalıdır. Return action.payload veya return {...state, ...action.payload} gibi "blind spreadler/return" kullanımını en aza indirin, çünkü bunlar içeriği doğru bir şekilde biçimlendirmek için action’ı gönderen koda ve reducer’a bağlıdır. o state’in neye benzediğinin mülkiyetinden etkili bir şekilde vazgeçer. action içeriği doğru değilse, bu hatalara yol açabilir.
Not: Her bir alan için ayrı bir action type’ını yazmanın zaman alıcı ve çok az fayda sağlayacağı bir formdaki verileri düzenleme gibi senaryolar için "spread return” reducer makul bir seçim olabilir.
Detailed Explanation
Şuna benzeyen bir "current user" reducer’ı hayal edin:
const initialState = { firstName: null, lastName: null, age: null, }; export default usersReducer = (state = initialState, action) { switch(action.type) { case "users/userLoggedIn": { return action.payload; } default: return state; } }const initialState = { firstName: null, lastName: null, age: null, }; export default usersReducer = (state = initialState, action) { switch(action.type) { case "users/userLoggedIn": { return action.payload; } default: return state; } }
Bu örnekte, reducer, action.payload'ın doğru biçimlendirilmiş bir nesne olacağını tamamen varsayar.
Ancak, kodun bir kısmının eylemin içinde bir "user" nesnesi yerine bir "yapılacak" nesnesi göndereceğini hayal edin:
dispatch({ type: 'users/userLoggedIn', payload: { id: 42, text: 'Buy milk' } })
Reducer, yapılacakları körü körüne döndürür ve şimdi uygulamanın geri kalanı, user’ı store’dan okumaya çalıştığında muhtemelen bozulur.
Reducer’ın action.payload'ın gerçekten doğru alanlara sahip olduğundan emin olmak için bazı doğrulama kontrolleri varsa veya doğru alanları ada göre okumaya çalışırsa, bu en azından kısmen düzeltilebilir. Yine de bu daha fazla kod ekler, bu yüzden güvenlik için daha fazla kod takası meselesidir.
Statik typing’in kullanılması, bu tür bir kodu daha güvenli ve biraz daha kabul edilebilir hale getirir. Reducer, action’ın bir PayloadAction<User> olduğunu biliyorsa, action.payload'u döndürmek güvenli olmalıdır.
Name State Slices Based On the Stored Data
Reducerlar state şekline sahip olmalıdır’da belirtildiği gibi, reducer mantığını bölmek için standart yaklaşım, durumun "slice"lara ayrılmasına dayanır. Buna uygun olarak, CombineReducers, bu dilim reducerlarını daha büyük bir reducer fonksiyonuna birleştirmek için standart fonksiyondur.
CombineReducers'a iletilen nesnedeki key adları, sonuçtaki state nesnesindeki key adlarını tanımlayacaktır. Bu keylere içerde tutulan verilerden sonra isim vermeyi unutmayınız ve anahtar isimlerinde "reducer" kelimesini kullanmaktan kaçınınız. Nesneniz {usersReducer: {}, postsReducer: {}} yerine {users: {}, posts: {}} gibi görünmelidir.
Detailed Explanation
ES6 object literal shorthand’i, bir nesnede aynı anda bir key adı ve bir değer tanımlamayı kolaylaştırır:
const data = 42 const obj = { data } // same as: {data: data}
CombineReducers, reducer fonksiyonlarla dolu bir nesneyi kabul eder ve bunu aynı key adlarına sahip state nesneleri oluşturmak için kullanır. Bu, function nesnesindeki key adlarının durum nesnesindeki key adlarını tanımladığı anlamına gelir.
Bu, bir reducer’ın değişken adındaki "reducer" kullanılarak içe aktarıldığı ve daha sonra object literal shorthand’ini kullanarak CombineReducers'a iletildiği yaygın bir hatayla sonuçlanır:
import usersReducer from 'features/users/usersSlice' const rootReducer = combineReducers({ usersReducer })
Bu durumda, nesnenin object literal shorthand kullanımı {usersReducer: usersReducer} gibi bir nesne yarattı. Yani, "reducer" artık state’in keyi adındadır. Bu gereksiz ve işe yaramaz.
Bunun yerine, yalnızca içindeki verilerle ilgili anahtar adları tanımlayın. Açıklık için açık key: value sözdizimi kullanmanızı öneririz:
import usersReducer from 'features/users/usersSlice' import postsReducer from 'features/posts/postsSlice' const rootReducer = combineReducers({ users: usersReducer, posts: postsReducer })
Biraz daha fazla kod yazarsınız, ancak en anlaşılır kod ve durum tanımıyla sonuçlanır.