Redux Toolkit ve RTK Query ile React Uygulamaları Oluşturma
Yayınlanan: 2022-03-11Redux'u React Query gibi özelliklerle kullanmak istediniz mi? Artık Redux Toolkit'i ve en son eklentisi olan RTK Query'yi kullanarak yapabilirsiniz.
RTK Query, gelişmiş bir veri alma ve istemci tarafı önbelleğe alma aracıdır. İşlevselliği React Query'ye benzer, ancak doğrudan Redux ile entegre olma avantajına sahiptir. API etkileşimi için geliştiriciler, Redux ile çalışırken genellikle Thunk gibi zaman uyumsuz ara yazılım modüllerini kullanır. Böyle bir yaklaşım esnekliği sınırlar; bu nedenle React geliştiricilerinin artık Redux ekibinden günümüzün istemci/sunucu iletişiminin tüm gelişmiş ihtiyaçlarını karşılayan resmi bir alternatifi var.
Bu makale, RTK Sorgusunun gerçek dünya senaryolarında nasıl kullanılabileceğini gösterir ve her adım, eklenen işlevleri vurgulamak için bir kesinleştirme farkı için bir bağlantı içerir. Sonunda kod tabanının tamamına bir bağlantı görünür.
Kazan plakası ve Konfigürasyon
Proje Başlatma Farkı
Öncelikle bir proje oluşturmamız gerekiyor. Bu, TypeScript ve Redux ile kullanım için Create React App (CRA) şablonu kullanılarak yapılır:
npx create-react-app . --template redux-typescriptYol boyunca ihtiyaç duyacağımız birkaç bağımlılığı var, en dikkat çekici olanlar:
- Redux Araç Takımı ve RTK Sorgusu
- Malzeme Kullanıcı Arayüzü
- Lodaş
- formik
- React Router
Ayrıca web paketi için özel yapılandırma sağlama yeteneğini de içerir. Normalde, siz çıkarmadığınız sürece CRA bu tür yetenekleri desteklemez.
başlatma
Çıkarmaktan çok daha güvenli bir yol, özellikle bu değişiklikler küçükse, yapılandırmayı değiştirebilecek bir şey kullanmaktır. Bu ortak özellik, özel bir babel yapılandırması sunmak için bu işlevselliği gerçekleştirmek için tepki-app-rewired ve custom-cra kullanır:
const plugins = [ [ 'babel-plugin-import', { 'libraryName': '@material-ui/core', 'libraryDirectory': 'esm', 'camel2DashComponentName': false }, 'core' ], [ 'babel-plugin-import', { 'libraryName': '@material-ui/icons', 'libraryDirectory': 'esm', 'camel2DashComponentName': false }, 'icons' ], [ 'babel-plugin-import', { "libraryName": "lodash", "libraryDirectory": "", "camel2DashComponentName": false, // default: true } ] ]; module.exports = { plugins };Bu, içe aktarmaya izin vererek geliştirici deneyimini daha iyi hale getirir. Örneğin:
import { omit } from 'lodash'; import { Box } from '@material-ui/core';Bu tür içe aktarmalar genellikle paket boyutunun artmasına neden olur, ancak yapılandırdığımız yeniden yazma işleviyle bunlar şu şekilde çalışır:
import omit from 'lodash/omit'; import Box from '@material-ui/core/Box';Yapılandırma
Redux Kurulum Farkı
Tüm uygulama Redux'u temel aldığından, başlatmadan sonra mağaza yapılandırmasını ayarlamamız gerekecek:
import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { Reducer } from 'redux'; import { FLUSH, PAUSE, PERSIST, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'; import { RESET_STATE_ACTION_TYPE } from './actions/resetState'; import { unauthenticatedMiddleware } from './middleware/unauthenticatedMiddleware'; const reducers = {}; const combinedReducer = combineReducers<typeof reducers>(reducers); export const rootReducer: Reducer<RootState> = ( state, action ) => { if (action.type === RESET_STATE_ACTION_TYPE) { state = {} as RootState; } return combinedReducer(state, action); }; export const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] } }).concat([ unauthenticatedMiddleware ]), }); export const persistor = persistStore(store); export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType<typeof combinedReducer>; export const useTypedDispatch = () => useDispatch<AppDispatch>(); export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;Standart mağaza yapılandırmasının yanı sıra, hem uygulamaların kendileri hem de test için gerçek dünyadaki uygulamalarda kullanışlı olan bir genel sıfırlama durumu eylemi için yapılandırma ekleyeceğiz:
import { createAction } from '@reduxjs/toolkit'; export const RESET_STATE_ACTION_TYPE = 'resetState'; export const resetStateAction = createAction( RESET_STATE_ACTION_TYPE, () => { return { payload: null }; } );Ardından, mağazayı temizleyerek 401 yanıtı işlemek için özel ara katman yazılımı ekleyeceğiz:
import { isRejectedWithValue, Middleware } from '@reduxjs/toolkit'; import { resetStateAction } from '../actions/resetState'; export const unauthenticatedMiddleware: Middleware = ({ dispatch }) => (next) => (action) => { if (isRejectedWithValue(action) && action.payload.status === 401) { dispatch(resetStateAction()); } return next(action); };Çok uzak çok iyi. Kazan plakasını oluşturduk ve Redux'u yapılandırdık. Şimdi biraz işlevsellik ekleyelim.
kimlik doğrulama
Erişim Simgesi Farkını Alma
Kimlik doğrulama, basitlik için üç adıma ayrılmıştır:
- Erişim belirtecini almak için API tanımları ekleme
- GitHub web kimlik doğrulama akışını işlemek için bileşenler ekleme
- Kullanıcıya tüm uygulamayı sağlamak için yardımcı program bileşenleri sağlayarak kimlik doğrulamayı sonlandırma
Bu adımda, erişim belirtecini alma özelliğini ekliyoruz.
RTK Sorgu ideolojisi, tüm API tanımlarının tek bir yerde görünmesini gerektirir; bu, birkaç uç noktaya sahip kurumsal düzeyde uygulamalarla uğraşırken kullanışlıdır. Kurumsal bir uygulamada, her şey tek bir yerde olduğunda, istemci önbelleğe almanın yanı sıra entegre API'yi düşünmek çok daha kolaydır.
RTK Sorgusu, OpenAPI standartlarını veya GraphQL'yi kullanarak API tanımlarını otomatik olarak oluşturmak için araçlar içerir. Bu araçlar henüz emekleme aşamasındadır, ancak aktif olarak geliştirilmektedir. Ek olarak, bu kitaplık, sürdürülebilirliği iyileştirme yeteneği nedeniyle kurumsal uygulamalar için giderek daha fazla tercih edilen TypeScript ile mükemmel geliştirici deneyimi sağlamak üzere tasarlanmıştır.
Bizim durumumuzda tanımlar API klasörü altında yer alacaktır. Şimdilik sadece bunu talep ettik:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { AuthResponse } from './types'; export const AUTH_API_REDUCER_KEY = 'authApi'; export const authApi = createApi({ reducerPath: AUTH_API_REDUCER_KEY, baseQuery: fetchBaseQuery({ baseUrl: 'https://tp-auth.herokuapp.com', }), endpoints: (builder) => ({ getAccessToken: builder.query<AuthResponse, string>({ query: (code) => { return ({ url: 'github/access_token', method: 'POST', body: { code } }); }, }), }), });GitHub kimlik doğrulaması, GitHub API gereksinimleri nedeniyle Heroku'da ayrı olarak barındırılan açık kaynaklı bir kimlik doğrulama sunucusu aracılığıyla sağlanır.
Kimlik Doğrulama Sunucusu
Bu örnek proje için gerekli olmasa da, kimlik doğrulama sunucusunun kendi kopyasını barındırmak isteyen okuyucuların şunları yapması gerekir:
- Kendi istemci kimliğini ve sırrını oluşturmak için GitHub'da bir OAuth uygulaması oluşturun.
-
GITHUB_CLIENT_IDveGITHUB_SECRETortam değişkenleri aracılığıyla kimlik doğrulama sunucusuna GitHub ayrıntılarını sağlayın. - Yukarıdaki API tanımlarında kimlik doğrulama bitiş noktası
baseUrldeğerini değiştirin. - React tarafında, sonraki kod örneğinde
client_idparametresini değiştirin.
Sonraki adım, bu API'yi kullanan bileşenleri eklemektir. GitHub web uygulaması akışının gereksinimleri nedeniyle, GitHub'a yönlendirmeden sorumlu bir oturum açma bileşenine ihtiyacımız olacak:
import { Box, Container, Grid, Link, Typography } from '@material-ui/core'; import GitHubIcon from '@material-ui/icons/GitHub'; import React from 'react'; const Login = () => { return ( <Container maxWidth={false}> <Box height="100vh" textAlign="center" clone> <Grid container spacing={3} justify="center" alignItems="center"> <Grid item xs="auto"> <Typography variant="h5" component="h1" gutterBottom> Log in via Github </Typography> <Link href={`https://github.com/login/oauth/authorize?client_id=b1bd2dfb1d172d1f1589`} color="textPrimary" data-test aria-label="Login Link" > <GitHubIcon fontSize="large"/> </Link> </Grid> </Grid> </Box> </Container> ); }; export default Login; GitHub uygulamamıza geri yönlendirdiğinde, kodu işlemek ve buna dayalı olarak access_token almak için bir rotaya ihtiyacımız olacak:
import React, { useEffect } from 'react'; import { Redirect } from 'react-router'; import { StringParam, useQueryParam } from 'use-query-params'; import { authApi } from '../../../../api/auth/api'; import FullscreenProgress from '../../../../shared/components/FullscreenProgress/FullscreenProgress'; import { useTypedDispatch } from '../../../../shared/redux/store'; import { authSlice } from '../../slice'; const OAuth = () => { const dispatch = useTypedDispatch(); const [code] = useQueryParam('code', StringParam); const accessTokenQueryResult = authApi.endpoints.getAccessToken.useQuery( code!, { skip: !code } ); const { data } = accessTokenQueryResult; const accessToken = data?.access_token; useEffect(() => { if (!accessToken) return; dispatch(authSlice.actions.updateAccessToken(accessToken)); }, [dispatch, accessToken]); Daha önce React Query kullandıysanız, API ile etkileşim mekanizması RTK Query için benzerdir. Bu, ek özellikler uygularken gözlemleyeceğimiz Redux entegrasyonu sayesinde bazı düzgün özellikler sağlar. access_token için yine de, bir eylem göndererek onu mağazaya manuel olarak kaydetmemiz gerekiyor:
dispatch(authSlice.actions.updateAccessToken(accessToken));Bunu, sayfa yeniden yüklemeleri arasında belirteci kalıcı kılmak için yapıyoruz. Hem kalıcılık hem de eylemi gönderme yeteneği için, kimlik doğrulama özelliğimiz için bir mağaza yapılandırması tanımlamamız gerekiyor.
Kurallara göre, Redux Toolkit bunları dilimler olarak ifade eder:
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import { AuthState } from './types'; const initialState: AuthState = {}; export const authSlice = createSlice({ name: 'authSlice', initialState, reducers: { updateAccessToken(state, action: PayloadAction<string | undefined>) { state.accessToken = action.payload; }, }, }); export const authReducer = persistReducer({ key: 'rtk:auth', storage, whitelist: ['accessToken'] }, authSlice.reducer);Önceki kodun çalışması için bir gereksinim daha vardır. Her API, mağaza yapılandırması için bir azaltıcı olarak sağlanmalıdır ve her API, eklemeniz gereken kendi ara yazılımıyla birlikte gelir:
import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { Reducer } from 'redux'; import { FLUSH, PAUSE, PERSIST, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'; import { AUTH_API_REDUCER_KEY, authApi } from '../../api/auth/api'; import { authReducer, authSlice } from '../../features/auth/slice'; import { RESET_STATE_ACTION_TYPE } from './actions/resetState'; import { unauthenticatedMiddleware } from './middleware/unauthenticatedMiddleware'; const reducers = { [authSlice.name]: authReducer, [AUTH_API_REDUCER_KEY]: authApi.reducer, }; const combinedReducer = combineReducers<typeof reducers>(reducers); export const rootReducer: Reducer<RootState> = ( state, action ) => { if (action.type === RESET_STATE_ACTION_TYPE) { state = {} as RootState; } return combinedReducer(state, action); }; export const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] } }).concat([ unauthenticatedMiddleware, authApi.middleware ]), }); export const persistor = persistStore(store); export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType<typeof combinedReducer>; export const useTypedDispatch = () => useDispatch<AppDispatch>(); export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector; Bu kadar! Artık uygulamamız access_token ve bunun üzerine daha fazla kimlik doğrulama özelliği eklemeye hazırız.
Kimlik Doğrulamayı Tamamlama
Kimlik Doğrulama Farkını Tamamlama
Kimlik doğrulama için bir sonraki özellik listesi şunları içerir:
- Kullanıcıyı GitHub API'sinden alma ve uygulamanın geri kalanı için sağlama yeteneği.
- Yalnızca kimliği doğrulandığında veya misafir olarak gezinirken erişilebilen rotalara sahip yardımcı program.
Kullanıcıyı geri alma yeteneği eklemek için bazı API kalıplarına ihtiyacımız olacak. Kimlik doğrulama API'sinden farklı olarak, GitHub API'sinin Redux mağazamızdan erişim belirtecini alma ve her isteğe bir Yetkilendirme başlığı olarak uygulama yeteneğine ihtiyacı olacaktır.
Özel bir temel sorgu oluşturularak elde edilen RTK Sorgusunda:
import { RequestOptions } from '@octokit/types/dist-types/RequestOptions'; import { BaseQueryFn } from '@reduxjs/toolkit/query/react'; import axios, { AxiosError } from 'axios'; import { omit } from 'lodash'; import { RootState } from '../../shared/redux/store'; import { wrapResponseWithLink } from './utils'; const githubAxiosInstance = axios.create({ baseURL: 'https://api.github.com', headers: { accept: `application/vnd.github.v3+json` } }); const axiosBaseQuery = (): BaseQueryFn<RequestOptions> => async ( requestOpts, { getState } ) => { try { const token = (getState() as RootState).authSlice.accessToken; const result = await githubAxiosInstance({ ...requestOpts, headers: { ...(omit(requestOpts.headers, ['user-agent'])), Authorization: `Bearer ${token}` } }); return { data: wrapResponseWithLink(result.data, result.headers.link) }; } catch (axiosError) { const err = axiosError as AxiosError; return { error: { status: err.response?.status, data: err.response?.data } }; } }; export const githubBaseQuery = axiosBaseQuery();Burada axios kullanıyorum, ancak diğer istemciler de kullanılabilir.
Sonraki adım, GitHub'dan kullanıcı bilgilerini almak için bir API tanımlamaktır:
import { endpoint } from '@octokit/endpoint'; import { createApi } from '@reduxjs/toolkit/query/react'; import { githubBaseQuery } from '../index'; import { ResponseWithLink } from '../types'; import { User } from './types'; export const USER_API_REDUCER_KEY = 'userApi'; export const userApi = createApi({ reducerPath: USER_API_REDUCER_KEY, baseQuery: githubBaseQuery, endpoints: (builder) => ({ getUser: builder.query<ResponseWithLink<User>, null>({ query: () => { return endpoint('GET /user'); }, }), }), }); Burada özel temel sorgumuzu kullanıyoruz, yani userApi kapsamındaki her isteğin bir Yetkilendirme başlığı içereceği anlamına geliyor. API'nin kullanılabilir olması için ana mağaza yapılandırmasını değiştirelim:
import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { Reducer } from 'redux'; import { FLUSH, PAUSE, PERSIST, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'; import { AUTH_API_REDUCER_KEY, authApi } from '../../api/auth/api'; import { USER_API_REDUCER_KEY, userApi } from '../../api/github/user/api'; import { authReducer, authSlice } from '../../features/auth/slice'; import { RESET_STATE_ACTION_TYPE } from './actions/resetState'; import { unauthenticatedMiddleware } from './middleware/unauthenticatedMiddleware'; const reducers = { [authSlice.name]: authReducer, [AUTH_API_REDUCER_KEY]: authApi.reducer, [USER_API_REDUCER_KEY]: userApi.reducer, }; const combinedReducer = combineReducers<typeof reducers>(reducers); export const rootReducer: Reducer<RootState> = ( state, action ) => { if (action.type === RESET_STATE_ACTION_TYPE) { state = {} as RootState; } return combinedReducer(state, action); }; export const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] } }).concat([ unauthenticatedMiddleware, authApi.middleware, userApi.middleware ]), }); export const persistor = persistStore(store); export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType<typeof combinedReducer>; export const useTypedDispatch = () => useDispatch<AppDispatch>(); export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;Ardından, uygulamamız oluşturulmadan önce bu API'yi çağırmamız gerekiyor. Basit olması için bunu, Angular rotalar için çözümleme işlevinin nasıl çalıştığına benzer bir şekilde yapalım, böylece kullanıcı bilgilerini alana kadar hiçbir şey işlenmez.
Kullanıcının yokluğu, önceden bir kullanıcı arayüzü sağlayarak daha ayrıntılı bir şekilde ele alınabilir, böylece kullanıcı daha hızlı bir şekilde anlamlı bir işleme sahip olur. Bu, daha fazla düşünce ve çalışma gerektirir ve kesinlikle üretime hazır bir uygulamada ele alınmalıdır.
Bunu yapmak için bir ara katman bileşeni tanımlamamız gerekiyor:
import React, { FC } from 'react'; import { userApi } from '../../../../api/github/user/api'; import FullscreenProgress from '../../../../shared/components/FullscreenProgress/FullscreenProgress'; import { RootState, useTypedSelector } from '../../../../shared/redux/store'; import { useAuthUser } from '../../hooks/useAuthUser'; const UserMiddleware: FC = ({ children }) => { const accessToken = useTypedSelector( (state: RootState) => state.authSlice.accessToken ); const user = useAuthUser(); userApi.endpoints.getUser.useQuery(null, { skip: !accessToken }); if (!user && accessToken) { return ( <FullscreenProgress/> ); } return children as React.ReactElement; }; export default UserMiddleware;Bunun yaptığı şey basittir. Kullanıcı bilgilerini almak için GitHub API ile etkileşime girer ve yanıt hazır olana kadar çocukları oluşturmaz. Şimdi, uygulama işlevselliğini bu bileşenle sararsak, kullanıcı bilgilerinin başka herhangi bir şey oluşturulmadan önce çözüleceğini biliyoruz:
import { CssBaseline } from '@material-ui/core'; import React from 'react'; import { Provider } from 'react-redux'; import { BrowserRouter as Router, Route, } from 'react-router-dom'; import { PersistGate } from 'redux-persist/integration/react'; import { QueryParamProvider } from 'use-query-params'; import Auth from './features/auth/Auth'; import UserMiddleware from './features/auth/components/UserMiddleware/UserMiddleware'; import './index.css'; import FullscreenProgress from './shared/components/FullscreenProgress/FullscreenProgress'; import { persistor, store } from './shared/redux/store'; const App = () => { return ( <Provider store={store}> <PersistGate loading={<FullscreenProgress/>} persistor={persistor}> <Router> <QueryParamProvider ReactRouterRoute={Route}> <CssBaseline/> <UserMiddleware> <Auth/> </UserMiddleware> </QueryParamProvider> </Router> </PersistGate> </Provider> ); }; export default App; Gelelim en şık kısma. access_token ile yaptığımız gibi kullanıcı bilgilerini mağazada manuel olarak kaydetmemiş olsak da, artık kullanıcı bilgilerini uygulamanın herhangi bir yerinden alma olanağına sahibiz.

Nasıl? Bunun için basit bir özel React Hook oluşturarak:
import { userApi } from '../../../api/github/user/api'; import { User } from '../../../api/github/user/types'; export const useAuthUser = (): User | undefined => { const state = userApi.endpoints.getUser.useQueryState(null); return state.data?.response; }; RTK Sorgusu, her uç nokta için useQueryState seçeneğini sağlar ve bu, bize o uç nokta için geçerli durumu alma yeteneği verir.
Bu neden bu kadar önemli ve faydalı? Çünkü kodu yönetmek için çok fazla ek yük yazmamız gerekmiyor. Bonus olarak, kutudan çıktığı haliyle Redux'ta API/istemci verileri arasında bir ayrım elde ediyoruz.
RTK Sorgusu kullanmak, güçlükleri ortadan kaldırır. RTK Query, veri getirmeyi durum yönetimiyle birleştirerek, React Query kullansak bile orada olacak olan boşluğu ortadan kaldırır. (React Query ile, alınan verilere UI katmanındaki alakasız bileşenler tarafından erişilmelidir.)
Son bir adım olarak, bir rotanın oluşturulup oluşturulmayacağını belirlemek için bu kancayı kullanan standart bir özel rota bileşeni tanımlarız:
import React, { FC } from 'react'; import { Redirect, Route, RouteProps } from 'react-router'; import { useAuthUser } from '../../hooks/useAuthUser'; export type AuthenticatedRouteProps = { onlyPublic?: boolean; } & RouteProps; const AuthenticatedRoute: FC<AuthenticatedRouteProps> = ({ children, onlyPublic = false, ...routeProps }) => { const user = useAuthUser(); return ( <Route {...routeProps} render={({ location }) => { if (onlyPublic) { return !user ? ( children ) : ( <Redirect to={{ pathname: '/', state: { from: location } }} /> ); } return user ? ( children ) : ( <Redirect to={{ pathname: '/login', state: { from: location } }} /> ); }} /> ); }; export default AuthenticatedRoute;Kimlik Doğrulama Testleri Farklı
React uygulamaları için testler yazmak söz konusu olduğunda, RTK Query'ye özgü hiçbir şey yoktur. Şahsen ben Kent C. Dodds'un test etme yaklaşımından ve kullanıcı deneyimi ve kullanıcı etkileşimine odaklanan bir test stilinden yanayım. RTK Sorgusu kullanılırken pek bir şey değişmez.
Bununla birlikte, RTK Query ile yazılmış bir uygulamanın mükemmel bir şekilde test edilebilir olduğunu göstermek için her adım kendi testlerini içerecektir.
Not: Örnek, neyin test edileceğine, neyin alay edileceğine ve ne kadar kod yeniden kullanılabilirliğinin tanıtılacağına ilişkin olarak bu testlerin nasıl yazılması gerektiği konusundaki fikrimi göstermektedir.
RTK Sorgu Depoları
RTK Sorgusunu sergilemek için, belirli senaryolarda nasıl performans gösterdiğini ve nasıl kullanılabileceğini görmek için uygulamaya bazı ek özellikler tanıtacağız.
Depolar Farkı ve Testler Farkı
Yapacağımız ilk şey, depolar için bir özellik tanıtmak. Bu özellik, GitHub'da deneyimleyebileceğiniz Depolar sekmesinin işlevselliğini taklit etmeye çalışacaktır. Profilinizi ziyaret edecek ve depoları arama ve belirli kriterlere göre sıralama yeteneğine sahip olacaktır. Bu adımda tanıtılan birçok dosya değişikliği vardır. İlgilendiğiniz kısımları incelemenizi tavsiye ederim.
Önce depoların işlevselliğini kapsayacak şekilde gerekli API tanımlarını ekleyelim:
import { endpoint } from '@octokit/endpoint'; import { createApi } from '@reduxjs/toolkit/query/react'; import { githubBaseQuery } from '../index'; import { ResponseWithLink } from '../types'; import { RepositorySearchArgs, RepositorySearchData } from './types'; export const REPOSITORY_API_REDUCER_KEY = 'repositoryApi'; export const repositoryApi = createApi({ reducerPath: REPOSITORY_API_REDUCER_KEY, baseQuery: githubBaseQuery, endpoints: (builder) => ({ searchRepositories: builder.query< ResponseWithLink<RepositorySearchData>, RepositorySearchArgs >( { query: (args) => { return endpoint('GET /search/repositories', args); }, }), }), refetchOnMountOrArgChange: 60 });Bu hazır olduğunda, Arama/Izgara/Sayfalandırmadan oluşan bir Depo özelliğini tanıtalım:
import { Grid } from '@material-ui/core'; import React from 'react'; import PageContainer from '../../../../../../shared/components/PageContainer/PageContainer'; import PageHeader from '../../../../../../shared/components/PageHeader/PageHeader'; import RepositoryGrid from './components/RepositoryGrid/RepositoryGrid'; import RepositoryPagination from './components/RepositoryPagination/RepositoryPagination'; import RepositorySearch from './components/RepositorySearch/RepositorySearch'; import RepositorySearchFormContext from './components/RepositorySearch/RepositorySearchFormContext'; const Repositories = () => { return ( <RepositorySearchFormContext> <PageContainer> <PageHeader title="Repositories"/> <Grid container spacing={3}> <Grid item xs={12}> <RepositorySearch/> </Grid> <Grid item xs={12}> <RepositoryGrid/> </Grid> <Grid item xs={12}> <RepositoryPagination/> </Grid> </Grid> </PageContainer> </RepositorySearchFormContext> ); }; export default Repositories;Depolar API'si ile etkileşim, şimdiye kadar karşılaştığımızdan daha karmaşıktır, bu yüzden bize şunları yapabilmemizi sağlayacak özel kancalar tanımlayalım:
- API çağrıları için bağımsız değişkenler alın.
- Durumda saklanan geçerli API sonucunu alın.
- API uç noktalarını çağırarak verileri alın.
import { debounce } from 'lodash'; import { useCallback, useEffect, useMemo } from 'react'; import urltemplate from 'url-template'; import { repositoryApi } from '../../../../../../../api/github/repository/api'; import { RepositorySearchArgs } from '../../../../../../../api/github/repository/types'; import { useTypedDispatch } from '../../../../../../../shared/redux/store'; import { useAuthUser } from '../../../../../../auth/hooks/useAuthUser'; import { useRepositorySearchFormContext } from './useRepositorySearchFormContext'; const searchQs = urltemplate.parse('user:{user} {name} {visibility}'); export const useSearchRepositoriesArgs = (): RepositorySearchArgs => { const user = useAuthUser()!; const { values } = useRepositorySearchFormContext(); return useMemo<RepositorySearchArgs>(() => { return { q: decodeURIComponent( searchQs.expand({ user: user.login, name: values.name && `${values.name} in:name`, visibility: ['is:public', 'is:private'][values.type] ?? '', }) ).trim(), sort: values.sort, per_page: values.per_page, page: values.page, }; }, [values, user.login]); }; export const useSearchRepositoriesState = () => { const searchArgs = useSearchRepositoriesArgs(); return repositoryApi.endpoints.searchRepositories.useQueryState(searchArgs); }; export const useSearchRepositories = () => { const dispatch = useTypedDispatch(); const searchArgs = useSearchRepositoriesArgs(); const repositorySearchFn = useCallback((args: typeof searchArgs) => { dispatch(repositoryApi.endpoints.searchRepositories.initiate(args)); }, [dispatch]); const debouncedRepositorySearchFn = useMemo( () => debounce((args: typeof searchArgs) => { repositorySearchFn(args); }, 100), [repositorySearchFn] ); useEffect(() => { repositorySearchFn(searchArgs); // Non debounced invocation should be called only on initial render // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { debouncedRepositorySearchFn(searchArgs); }, [searchArgs, debouncedRepositorySearchFn]); return useSearchRepositoriesState(); };Bir soyutlama katmanı olarak bu düzeyde bir ayırmaya sahip olmak, bu durumda hem okunabilirlik açısından hem de RTK Sorgu gereksinimleri nedeniyle önemlidir.
useQueryState kullanarak kullanıcı verilerini alan bir kancayı tanıttığımızda fark etmiş olabileceğiniz gibi, gerçek API çağrısı için sağladığımız argümanların aynısını sağlamamız gerekiyordu.
import { userApi } from '../../../api/github/user/api'; import { User } from '../../../api/github/user/types'; export const useAuthUser = (): User | undefined => { const state = userApi.endpoints.getUser.useQueryState(null); return state.data?.response; }; Bir argüman olarak sağladığımız bu boş, useQuery veya useQueryState olarak adlandırsak da oradadır. Bu gereklidir, çünkü RTK Sorgusu bir bilgiyi ilk etapta bu bilgiyi almak için kullanılan argümanlarla tanımlayıp önbelleğe alır.
Bu, herhangi bir zamanda gerçek API çağrısından ayrı olarak bir API çağrısı için gerekli argümanları alabilmemiz gerektiği anlamına gelir. Bu şekilde, gerektiğinde API verilerinin önbelleğe alınmış durumunu almak için kullanabiliriz.
API tanımımızdaki bu kod parçasında dikkat etmeniz gereken bir şey daha var:
refetchOnMountOrArgChange: 60Niye ya? Çünkü RTK Query gibi kitaplıkları kullanırken önemli noktalardan biri istemci önbelleği ve önbellek geçersiz kılma işlemlerini yapmaktır. Bu hayati önem taşır ve aynı zamanda içinde bulunduğunuz geliştirme aşamasına bağlı olarak sağlanması zor olabilecek önemli miktarda çaba gerektirir.
Bu konuda RTK Sorgusunu çok esnek buldum. Bu yapılandırma özelliğini kullanmak bize şunları sağlar:
- İlk adım olarak önbellek sorunlarından kaçınarak RTK Sorgusuna geçiş yapmak istediğinizde kullanışlı olan önbelleğe almayı tamamen devre dışı bırakın.
- Bazı bilgilerin X kadar süreyle önbelleğe alınabileceğini bildiğinizde kullanabileceğiniz basit bir geçersiz kılma mekanizması olan zamana dayalı önbelleğe almayı tanıtın.
taahhütler
Farkı Kabul Eder ve Farkı Test Eder
Bu adım, her bir havuz için taahhütleri görüntüleme, bu taahhütleri sayfalandırma ve şubeye göre filtreleme yeteneği ekleyerek havuz sayfasına daha fazla işlevsellik ekler. Ayrıca GitHub sayfasında alacağınız işlevselliği taklit etmeye çalışır.
Depoların uygulanması sırasında oluşturduğumuz stili izleyerek, dallar ve taahhütler almak için iki uç nokta daha ve bu uç noktalar için özel kancalar ekledik:
github/repository/api.ts
import { endpoint } from '@octokit/endpoint'; import { createApi } from '@reduxjs/toolkit/query/react'; import { githubBaseQuery } from '../index'; import { ResponseWithLink } from '../types'; import { RepositoryBranchesArgs, RepositoryBranchesData, RepositoryCommitsArgs, RepositoryCommitsData, RepositorySearchArgs, RepositorySearchData } from './types'; export const REPOSITORY_API_REDUCER_KEY = 'repositoryApi'; export const repositoryApi = createApi({ reducerPath: REPOSITORY_API_REDUCER_KEY, baseQuery: githubBaseQuery, endpoints: (builder) => ({ searchRepositories: builder.query< ResponseWithLink<RepositorySearchData>, RepositorySearchArgs >( { query: (args) => { return endpoint('GET /search/repositories', args); }, }), getRepositoryBranches: builder.query< ResponseWithLink<RepositoryBranchesData>, RepositoryBranchesArgs >( { query(args) { return endpoint('GET /repos/{owner}/{repo}/branches', args); } }), getRepositoryCommits: builder.query< ResponseWithLink<RepositoryCommitsData>, RepositoryCommitsArgs >( { query(args) { return endpoint('GET /repos/{owner}/{repo}/commits', args); }, }), }), refetchOnMountOrArgChange: 60 }); useGetRepositoryBranches.ts
import { useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { repositoryApi } from '../../../../../../../api/github/repository/api'; import { RepositoryBranchesArgs } from '../../../../../../../api/github/repository/types'; import { useTypedDispatch } from '../../../../../../../shared/redux/store'; import { useAuthUser } from '../../../../../../auth/hooks/useAuthUser'; import { CommitsRouteParams } from '../types'; export const useGetRepositoryBranchesArgs = (): RepositoryBranchesArgs => { const user = useAuthUser()!; const { repositoryName } = useParams<CommitsRouteParams>(); return useMemo<RepositoryBranchesArgs>(() => { return { owner: user.login, repo: repositoryName, }; }, [repositoryName, user.login]); }; export const useGetRepositoryBranchesState = () => { const queryArgs = useGetRepositoryBranchesArgs(); return repositoryApi.endpoints.getRepositoryBranches.useQueryState(queryArgs); }; export const useGetRepositoryBranches = () => { const dispatch = useTypedDispatch(); const queryArgs = useGetRepositoryBranchesArgs(); useEffect(() => { dispatch(repositoryApi.endpoints.getRepositoryBranches.initiate(queryArgs)); }, [dispatch, queryArgs]); return useGetRepositoryBranchesState(); }; useGetRepositoryCommits.ts
import isSameDay from 'date-fns/isSameDay'; import { useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { repositoryApi } from '../../../../../../../api/github/repository/api'; import { RepositoryCommitsArgs } from '../../../../../../../api/github/repository/types'; import { useTypedDispatch } from '../../../../../../../shared/redux/store'; import { useAuthUser } from '../../../../../../auth/hooks/useAuthUser'; import { AggregatedCommitsData, CommitsRouteParams } from '../types'; import { useCommitsSearchFormContext } from './useCommitsSearchFormContext'; export const useGetRepositoryCommitsArgs = (): RepositoryCommitsArgs => { const user = useAuthUser()!; const { repositoryName } = useParams<CommitsRouteParams>(); const { values } = useCommitsSearchFormContext(); return useMemo<RepositoryCommitsArgs>(() => { return { owner: user.login, repo: repositoryName, sha: values.branch, page: values.page, per_page: 15 }; }, [repositoryName, user.login, values]); }; export const useGetRepositoryCommitsState = () => { const queryArgs = useGetRepositoryCommitsArgs(); return repositoryApi.endpoints.getRepositoryCommits.useQueryState(queryArgs); }; export const useGetRepositoryCommits = () => { const dispatch = useTypedDispatch(); const queryArgs = useGetRepositoryCommitsArgs(); useEffect(() => { if (!queryArgs.sha) return; dispatch(repositoryApi.endpoints.getRepositoryCommits.initiate(queryArgs)); }, [dispatch, queryArgs]); return useGetRepositoryCommitsState(); }; export const useAggregatedRepositoryCommitsData = (): AggregatedCommitsData => { const { data: repositoryCommits } = useGetRepositoryCommitsState(); return useMemo(() => { if (!repositoryCommits) return []; return repositoryCommits.response.reduce((aggregated, commit) => { const existingCommitsGroup = aggregated.find(a => isSameDay( new Date(a.date), new Date(commit.commit.author!.date!) )); if (existingCommitsGroup) { existingCommitsGroup.commits.push(commit); } else { aggregated.push({ date: commit.commit.author!.date!, commits: [commit] }); } return aggregated; }, [] as AggregatedCommitsData); }, [repositoryCommits]); };Bunu yaptıktan sonra, birisi depo adının üzerine gelir gelmez taahhüt verilerini önceden getirerek UX'i iyileştirebiliriz:
import { Badge, Box, Chip, Divider, Grid, Link, Typography } from '@material-ui/core'; import StarOutlineIcon from '@material-ui/icons/StarOutline'; import formatDistance from 'date-fns/formatDistance'; import React, { FC } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { repositoryApi } from '../../../../../../../../api/github/repository/api'; import { Repository } from '../../../../../../../../api/github/repository/types'; import { useGetRepositoryBranchesArgs } from '../../../Commits/hooks/useGetRepositoryBranches'; import { useGetRepositoryCommitsArgs } from '../../../Commits/hooks/useGetRepositoryCommits'; const RepositoryGridItem: FC<{ repo: Repository }> = ({ repo }) => { const getRepositoryCommitsArgs = useGetRepositoryCommitsArgs(); const prefetchGetRepositoryCommits = repositoryApi.usePrefetch( 'getRepositoryCommits'); const getRepositoryBranchesArgs = useGetRepositoryBranchesArgs(); const prefetchGetRepositoryBranches = repositoryApi.usePrefetch( 'getRepositoryBranches'); return ( <Grid container spacing={1}> <Grid item xs={12}> <Typography variant="subtitle1" gutterBottom aria-label="repository-name"> <Link aria-label="commit-link" component={RouterLink} to={`/repositories/${repo.name}`} onMouseEnter={() => { prefetchGetRepositoryBranches({ ...getRepositoryBranchesArgs, repo: repo.name, }); prefetchGetRepositoryCommits({ ...getRepositoryCommitsArgs, sha: repo.default_branch, repo: repo.name, page: 1 }); }} > {repo.name} </Link> <Box marginLeft={1} clone> <Chip label={repo.private ? 'Private' : 'Public'} size="small"/> </Box> </Typography> <Typography component="p" variant="subtitle2" gutterBottom color="textSecondary"> {repo.description} </Typography> </Grid> <Grid item xs={12}> <Grid container alignItems="center" spacing={2}> <Box clone flex="0 0 auto" display="flex" alignItems="center" marginRight={2}> <Grid item> <Box clone marginRight={1} marginLeft={0.5}> <Badge color="primary" variant="dot"/> </Box> <Typography variant="body2" color="textSecondary"> {repo.language} </Typography> </Grid> </Box> <Box clone flex="0 0 auto" display="flex" alignItems="center" marginRight={2}> <Grid item> <Box clone marginRight={0.5}> <StarOutlineIcon fontSize="small"/> </Box> <Typography variant="body2" color="textSecondary"> {repo.stargazers_count} </Typography> </Grid> </Box> <Grid item> <Typography variant="body2" color="textSecondary"> Updated {formatDistance(new Date(repo.pushed_at), new Date())} ago </Typography> </Grid> </Grid> </Grid> <Grid item xs={12}> <Divider/> </Grid> </Grid> ); }; export default RepositoryGridItem;While the hover may seem artificial, this heavily impacts UX in real-world applications, and it is always handy to have such functionality available in the toolset of the library we use for API interaction.
The Pros and Cons of RTK Query
Final Source Code
We have seen how to use RTK Query in our apps, how to test those apps, and how to handle different concerns like state retrieval, cache invalidation, and prefetching.
There are a number of high-level benefits showcased throughout this article:
- Data fetching is built on top of Redux, leveraging its state management system.
- API definitions and cache invalidation strategies are located in one place.
- TypeScript improves the development experience and maintainability.
There are some downsides worth noting as well:
- The library is still in active development and, as a result, APIs may change.
- Information scarcity: Besides the documentation, which may be out of date, there isn't much information around.
We covered a lot in this practical walkthrough using the GitHub API, but there is much more to RTK Query, such as:
- Hata yönetimi
- mutasyonlar
- Polling
- Optimistic Updates
If you're intrigued by RTK Query's benefits, I encourage you to dig into those concepts further. Feel free to use this example as a basis to build on.
