NgRx 사용 사례, 파트 III: 의사결정
Hansjörg Keller가 Unsplash에 게시한 원본 표지 사진.
이전 기사에서는 NgRx를 사용한 액세스 제한 및 처리 목록을 다루었습니다. 오늘 우리는 주로 NgRx 효과(그러나 약간의 리듀서 및 선택기도 포함)를 활용하는 Angular 앱 전반에 걸친 의사 결정의 보다 일반적인 문제를 다룰 것입니다. 이 기사에서는 다음 주제를 다룰 것입니다:
- NgRx 오류 처리
- 리디렉션 처리
- 데이터 로딩 처리
- 사용자 상호작용
시작해 보세요!
NgRx를 사용한 오류 처리
오류 처리는 모두가 싫어하는 일이지만(종종 잊어버리는 경우도 있음) 모든 사람에게 정말 꼭 필요한 일이기도 합니다. NgRx 앱을 사용하면 적절한 주의를 기울여 이 문제를 해결하지 않으면 오류 처리의 복잡성이 실제로 증가합니다. 일반적으로 NgRx의 오류는 대부분의 시나리오에서 HTTP 요청으로 인해 발생하는 영향으로 인해 발생합니다.
일반적으로 오류를 처리하는 방법에는 로컬 처리 또는 전역 처리라는 두 가지 접근 방식이 있습니다. 로컬에서 처리한다는 것은 앱의 특정 부분에서 발생한 매우 구체적인 오류를 실제로 해결한다는 의미입니다. 예를 들어, 사용자가 로그인에 실패한 경우 "문제가 발생했습니다"와 같은 일반적인 오류 메시지 대신 "잘못된 사용자 이름 또는 비밀번호"와 같은 매우 구체적인 오류 메시지를 표시할 수 있습니다.
반면에 전역적으로 처리한다는 것은 모든 오류를 하나의 "파이프라인"으로 묶고 이전에 언급한 매우 일반적인 오류 메시지를 사용한다는 의미입니다. 물론 어떤 아이디어가 어떤 시나리오에 더 적합한지에 대한 논의를 시작할 수도 있지만, 가혹한 현실은 거의 모든 앱에 두 가지 세계가 모두 필요하다는 것입니다. 우리는 어떤 오류가 발생하면 오류 메시지를 표시하기를 원합니다("무언가 잘못되었습니다"는 조용한 실패보다 낫습니다). 🎜>몇 가지 오류가 발생했습니다. 두 가지 시나리오를 모두 살펴보겠습니다.
NgRx를 사용한 일반 오류 처리NgRx에서는 일어나는 모든 일이 행동에 의해 촉발됩니다. 오류 발생과 관련된 활동(HTTP 호출, WebSocket 등)의 경우 일반적으로 단일 활동에 대해 여러 작업을 생성합니다.
export const DataActions = createActionGroup({ source: 'Data', events: { 'Load Data': emptyProps(), 'Load Data Success': props<{ data: Data }>(), 'Load Data Error': props<{ error: string }>(), }, });
그러면 오류를 나타내고 일반 오류 메시지를 표시하는 모든 작업을 구독할 수 있습니다. 이는 NgRx 앱에서 매우 일반적인 패턴이며 일반적으로 "전역 오류 처리"로 이해됩니다. 우리의 경우 모든 작업을 구독하고 페이로드에 오류 속성이 있는 작업을 필터링하여 이를 수행할 수 있습니다.
export const handleErrors$ = createEffect(() => { const actions$ = inject(Actions); const notificationsService = inject(NotificationsService); return actions$.pipe( filter((action) => !!action.payload.error), tap((action) => { notificationsService.add({ severity: 'error', summary: 'Error', detail, }); }), }, { functional: true, dispatch: false });
export function errorProps(error: string) { return function() { return({error}); }; }
export const DataActions = createActionGroup({ source: 'Data', events: { 'Load Data': emptyProps(), 'Load Data Success': props<{ data: Data }>(), 'Load Data Error': errorProps('Failed to load data'), }, });
특정 오류 처리
우리의 경우에는 일부 특정 사례에 대해 일반 오류 처리기가 작동하는 방식을 사용자 정의할 수 있기를 원할 수 있습니다. 우리는 할 수 있기를 원합니다
- 일반 오류 메시지를 표시할지 여부를 효과에 알리기 위해
- 일부 사전 정의된 데이터가 포함된 오류 페이지로 리디렉션
- 페이지 내부에 오류 알림 표시
export function errorProps(error: string, showNotififcation = true) { return function() { return({error, showNotification}); }; }
export const DataActions = createActionGroup({ source: 'Data', events: { 'Load Data': emptyProps(), 'Load Data Success': props<{ data: Data }>(), 'Load Data Error': errorProps('Failed to load data', false), }, });
export const handleErrors$ = createEffect(() => { const actions$ = inject(Actions); const notificationsService = inject(NotificationsService); return actions$.pipe( filter((action) => !!action.payload.error), tap((action) => { if (action.payload.showNotification) { notificationsService.add({ severity: 'error', summary: 'Error', detail, }); } }), ); }, { functional: true, dispatch: false });
export function errorProps(error: string, showNotification = true, redirectTo?: string) { return function() { return({error, showNotification, redirectTo}); }; }
export const DataActions = createActionGroup({ source: 'Data', events: { 'Load Data': emptyProps(), 'Load Data Success': props<{ data: Data }>(), 'Load Data Error': errorProps('Failed to load data', false, '/error'), }, });
Next, let's finalize our effect by adding a redirection to the error page if the redirectTo property is present in the action payload:
export const handleErrors$ = createEffect(() => { const actions$ = inject(Actions); const notificationsService = inject(NotificationsService); const router = inject(Router); return actions$.pipe( filter((action) => !!action.payload.error), tap((action) => { if (action.payload.showNotification) { notificationsService.add({ severity: 'error', summary: 'Error', detail, }); } if (action.payload.redirectTo) { router.navigateByUrl(action.payload.redirectTo); } }), ); }, { functional: true, dispatch: false });
And that is it to this. Of course, if we need something really custom for a particular error action, we can just write a completely separate effect to handle that. Sometimes, if we want to also do something in the UI in relation to an error, we can also add the error message (and any other data) to the store and use them via a selector anywhere.
Next, let us discuss loading data into our component, and several approaches to it.
Handling loading data
Before we proceed, we should first understand that the approaches listed in this section are not better or worse than one another. Instead, they are approaches for different situations, depending on what we want for our UX. Let's examine them step by step.
Selecting data in the component to use
The most straightforward way we can get some data (presumably from an API) is by just selecting it in the component. With the latest APIs, we can select a signal of our data and use it directly in the template. Here is a very simple example:
@Component({ selector: 'app-my', template: ` <div> <h1>Data</h1> <p>{{ data() }}</p> </div> `, }) export class MyComponent { data = this.store.selectSignal(dataFeature.selectData); }
Of course, in real life, we often need to deal with scenarios like loading, errors, and so on. In this case, our state might look like this:
export interface State { data: Data | null; loading: boolean; error: string | null; }
If we are using the createFeature function to register our state, we can make use of the selectDataState function that the feature automatically creates for us. This will return the entire state with loading, error, and data properties. We can then use this in our component:
@Component({ selector: 'app-my', template: ` <div> @if (vm().loading) { <p>Loading...</p> } @if (vm().error) { <p>Error: {{ vm().error }}</p> } @else { <h1>Data</h1> <p>{{ vm().data }}</p> } </div> `, }) export class MyComponent { vm = this.store.selectSignal(dataFeature.selectDataState); }
This is very useful in most scenarios. However, sometimes we might not want to display the entire page if this important piece of data is not loaded. In Angular, this is commonly achieved with the use of routing resolvers, functions that return some Observables that routing waits for to emit before displaying a particular page. This is easy with the use of the HttpClient service, however, this becomes a bit complicated with NgRx (because we only make HTTP calls inside effects), resulting in lots of developers skipping resolvers entirely. However, there is an easy way to achieve this functionality. Let's build a simple resolver that utiliuzes the Store and the Actions Observable to know when the data is actually loaded:
export const dataResolver: ResolveFn<Data[]> = () => { const store = inject(Store); const actions$ = inject(Actions); store.dispatch(DataActions.loadData()); return store.select(dataFeature.selectData).pipe( skipUntil(actions.pipe(ofType(DataActions.loadDataSuccess))), ); }
Here, we first dispatch the action that actually initiates the HTTP call, then we just return the selected data from the store as an Observable, but with a catch - we tell it to wait until the action signaling that the data has been loaded is dispatched. Given that effects are guaranteed to run after reducers, this will ensure that the data is actually put into the store before the resolver returns it. We can then just pick this data up in our component:
@Component({ selector: 'app-my', template: ` <div> <h1>Data</h1> <p>{{ vm.data() }}</p> </div> `, }) export class MyComponent { private readonly route = inject(ActivatedRoute); readonly vm = toSignal(this.route.data, { initialValue: null, }) as Signal<{data: Data}>; }
We used ActivatedRoute instead of the Store because we already returned this data in the resolver. This makes our components even leaner - we don't even have to inject the Store and in unit testing, it can be often easier to mock the ActivatedRoute than the Store.
Finally, let's take a look at advanced decision-making with NgRx actions and effects, and see how this can help work with complex cases in large applications.
User interactions
NgRx is very useful when writing declarative code, as it allows us to just select the relevant state, and use it in our templates. However, sometimes, especially when dealing with third-party libraries, we need to perform some "imperative actions" which is tangentially related to our store. Consider this code which uses the Angular Material MatDialog service to open a confirmation dialog:
export class MyComponent { private readonly dialog = inject(MatDialog); private readonly store = inject(Store); openConfirmationDialog() { const dialogRef = this.dialog.open(ConfirmationDialogComponent, { data: { title: 'Confirmation', message: 'Are you sure you want to do this?', }, }); dialogRef.componentInstance.confirm.subscribe(() => { this.store.dispatch(DataActions.deleteData()); }); dialogRef.componentInstance.cancel.subscribe(() => { dialogRef.close(); }); } }
As we can see, there is a lot of imperative code in just this one method, there are two subscriptions, and they aren't even particularly simple (we just omitted unsubscription logic). Also, we must consider that in a normal application, we might have a dozen different places where the same confirmation dialog is used, with the only difference being the action that is performed when the user confirms/rejects.
Let's now approach this with an NgRx mindset, and try to create an action that can handle such a scenario, with callbacks as payloads.
export function confirmAction(callbacks: {confirm: () => void, reject: () => void}) { return function() { return({type: 'Open Confirmation Dialog', callbacks}); }; }
Now, we can create an action that will later tell the effect to redirect to an error page:
export const DataActions = createActionGroup({ source: 'Data', events: { 'Delete Data': confirmAction({ confirm: () => { return({action: 'Delete Data Confirmed'}); }, reject: () => { return({action: 'Delete Data Rejected'}); }, }), }, });
Now, we can create an effect that will handle all such actions:
export const handleConfirmationDialog$ = createEffect(() => { const actions$ = inject(Actions); const dialog = inject(MatDialog); return actions$.pipe( ofType(DataActions.openConfirmationDialog), tap((action) => { const dialogRef = dialog.open(ConfirmationDialogComponent, { data: { title: 'Confirmation', message: 'Are you sure you want to do this?', }, }); dialogRef.componentInstance.confirm.subscribe(() => { action.payload.callbacks.confirm(); }); dialogRef.componentInstance.cancel.subscribe(() => { action.payload.callbacks.reject(); }); }), ); }, { functional: true, dispatch: false });
Finally, we can really simplify our component:
export class MyComponent { private readonly store = inject(Store); openConfirmationDialog() { this.store.dispatch(DataActions.openConfirmationDialog({ confirm: () => { this.store.dispatch(DataActions.deleteData()); }, reject: () => { // Do nothing }, })); } }
And that is it. However... we might face a problem if we try to make our application as fine as possible. In NgRx, it is a good practice to keep everything serializable, which is a fancy way of saying "easily convertible to JSON". In the app configuration, it is possible to set specific options to help safeguard us from, for example, putting functions in the store. This is done with two options, strictStoreSerializability and strictActionSerializability.
export const config: ApplicationConfig = { providers: [ provideStore({}, { runtimeChecks: { strictActionSerializability: true, strictStoreSerializability: true, }, }), };
This goes a long mile to help keep our applications maintainable and prevent hard-to-debug issues.
[!NOTE] You can read more about runtime checks in the NgRx docs.
However, if we make actions strictly serializable, our confirmAction action will not work with the callbacks we passed! So, what can we do about it? Well, the easiest way is to give it other actions for confirm/reject options to handle by the effect. Because the nested actions will also be required to be serializable, this will help us bring everything back to a workable state, an approach that I personally call "higher-order actions".
export function confirmAction(confirmAction: string, rejectAction: string, callbackActions: { confirm: ActionCreator<any, any>, reject: ActionCreator<any, any> }) { return function() { return({type: 'Open Confirmation Dialog', callbackActions}); }; }
Next, we need to do a major update to our effect:
export const handleConfirmationDialog$ = createEffect(() => { const actions$ = inject(Actions); const dialog = inject(MatDialog); return actions$.pipe( ofType(DataActions.openConfirmationDialog), map(({callbackActions}) => { const dialogRef = dialog.open(ConfirmationDialogComponent, { data: { title: 'Confirmation', message: 'Are you sure you want to do this?', }, }); return merge([ dialogRef.componentInstance.confirm.pipe( map(() => callbackActions.confirm()), ), dialogRef.componentInstance.cancel.pipe( tap(() => dialogRef.close()), map(() => callbackActions.reject()), ), ]) }), ); }, { functional: true, dispatch: false });
Let's deconstruct what goes on here.
- The ConfirmationDialogComponent exposes the confirm and cancel observables.
- We open a MatDialog with the ConfirmationDialogComponent and every time an action created by the confirmAction is dispatched, we subscribe to the confirm observable and dispatch the action that was passed to the confirmAction function.
- We also subscribe to the cancel observable and dispatch the action that was passed to the confirmAction function.
- We return a merge of the two observables so that when either of them emits, the whole effect will emit.
With this implementation, it is easy to perform complex decision-making just using several actions in a component:
export class MyComponent { private readonly store = inject(Store); openConfirmationDialog() { this.store.dispatch(DataActions.openConfirmationDialog({ confirm: DataActions.deleteData, reject: DataActions.cancelDeleteData, })); } }
And that is it. With higher-order actions, we can easily delegate decision-making further to other effects and reducers as necessary, making the component code as declarative as possible.
Conclusion
In this article, we covered a few approaches to complex logic in Angular applications utilizing NgRx. NgRx is a huge tapestry of opportunities, and often we can easily transform ugly code into something understandable and maintainable. Such approaches are underappreciated, and with this piece, I try to bring them forth to help developers improve their state management solutions.
위 내용은 NgRx 사용 사례, 파트 III: 의사결정의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

Python은 부드러운 학습 곡선과 간결한 구문으로 초보자에게 더 적합합니다. JavaScript는 가파른 학습 곡선과 유연한 구문으로 프론트 엔드 개발에 적합합니다. 1. Python Syntax는 직관적이며 데이터 과학 및 백엔드 개발에 적합합니다. 2. JavaScript는 유연하며 프론트 엔드 및 서버 측 프로그래밍에서 널리 사용됩니다.

C/C에서 JavaScript로 전환하려면 동적 타이핑, 쓰레기 수집 및 비동기 프로그래밍으로 적응해야합니다. 1) C/C는 수동 메모리 관리가 필요한 정적으로 입력 한 언어이며 JavaScript는 동적으로 입력하고 쓰레기 수집이 자동으로 처리됩니다. 2) C/C를 기계 코드로 컴파일 해야하는 반면 JavaScript는 해석 된 언어입니다. 3) JavaScript는 폐쇄, 프로토 타입 체인 및 약속과 같은 개념을 소개하여 유연성과 비동기 프로그래밍 기능을 향상시킵니다.

웹 개발에서 JavaScript의 주요 용도에는 클라이언트 상호 작용, 양식 검증 및 비동기 통신이 포함됩니다. 1) DOM 운영을 통한 동적 컨텐츠 업데이트 및 사용자 상호 작용; 2) 사용자가 사용자 경험을 향상시키기 위해 데이터를 제출하기 전에 클라이언트 확인이 수행됩니다. 3) 서버와의 진실한 통신은 Ajax 기술을 통해 달성됩니다.

실제 세계에서 JavaScript의 응용 프로그램에는 프론트 엔드 및 백엔드 개발이 포함됩니다. 1) DOM 운영 및 이벤트 처리와 관련된 TODO 목록 응용 프로그램을 구축하여 프론트 엔드 애플리케이션을 표시합니다. 2) Node.js를 통해 RESTFULAPI를 구축하고 Express를 통해 백엔드 응용 프로그램을 시연하십시오.

보다 효율적인 코드를 작성하고 성능 병목 현상 및 최적화 전략을 이해하는 데 도움이되기 때문에 JavaScript 엔진이 내부적으로 작동하는 방식을 이해하는 것은 개발자에게 중요합니다. 1) 엔진의 워크 플로에는 구문 분석, 컴파일 및 실행; 2) 실행 프로세스 중에 엔진은 인라인 캐시 및 숨겨진 클래스와 같은 동적 최적화를 수행합니다. 3) 모범 사례에는 글로벌 변수를 피하고 루프 최적화, Const 및 Lets 사용 및 과도한 폐쇄 사용을 피하는 것이 포함됩니다.

Python과 JavaScript는 커뮤니티, 라이브러리 및 리소스 측면에서 고유 한 장점과 단점이 있습니다. 1) Python 커뮤니티는 친절하고 초보자에게 적합하지만 프론트 엔드 개발 리소스는 JavaScript만큼 풍부하지 않습니다. 2) Python은 데이터 과학 및 기계 학습 라이브러리에서 강력하며 JavaScript는 프론트 엔드 개발 라이브러리 및 프레임 워크에서 더 좋습니다. 3) 둘 다 풍부한 학습 리소스를 가지고 있지만 Python은 공식 문서로 시작하는 데 적합하지만 JavaScript는 MDNWebDocs에서 더 좋습니다. 선택은 프로젝트 요구와 개인적인 이익을 기반으로해야합니다.

개발 환경에서 Python과 JavaScript의 선택이 모두 중요합니다. 1) Python의 개발 환경에는 Pycharm, Jupyternotebook 및 Anaconda가 포함되어 있으며 데이터 과학 및 빠른 프로토 타이핑에 적합합니다. 2) JavaScript의 개발 환경에는 Node.js, VScode 및 Webpack이 포함되어 있으며 프론트 엔드 및 백엔드 개발에 적합합니다. 프로젝트 요구에 따라 올바른 도구를 선택하면 개발 효율성과 프로젝트 성공률이 향상 될 수 있습니다.

C와 C는 주로 통역사와 JIT 컴파일러를 구현하는 데 사용되는 JavaScript 엔진에서 중요한 역할을합니다. 1) C는 JavaScript 소스 코드를 구문 분석하고 추상 구문 트리를 생성하는 데 사용됩니다. 2) C는 바이트 코드 생성 및 실행을 담당합니다. 3) C는 JIT 컴파일러를 구현하고 런타임에 핫스팟 코드를 최적화하고 컴파일하며 JavaScript의 실행 효율을 크게 향상시킵니다.
