Skip to content

Przemysław Konieczniak - Dev Notes

Mediator Pattern

OOP, Design Patterns, JavaScript, TypeScript, React

Introduction

Mediator is a behavioral design pattern, which defines how a set of objects interact.

This pattern allows keeping loose relations between objects by managing the communication between them in one object instead of keeping explicit relations.

The benefit of this pattern is reusable, maintainable and testable code. This pattern reflects the SOLID rule.

The pattern assumptions:

  • Define the mediator object which encapsulates the interaction between a set of objects.
  • The objects delegates their interaction to the mediator object instead of interacting with each other directly.

Mediator is one of the most popular pattern using in the modern web application architecture. The components tree itself implements a mediator pattern.

To present this design pattern, let's consider the following use case.

Use Case

We would like to add a feature to the React App that allows to upload photos and preview them.

It doesn't sound good to keep a logic being responsible for uploading the photos and previewing them in one place. In this case, our component would have too much responsibility. Additionally, our component wouldn't be reusable and would be hard to test.

At this moment we know that the logic of this component should be separated. We have to also think about how to handle communication between these two components.

If we put the Preview component inside the Upload component it will cause both components coupled closely.

To solve this problem we can use the Mediator pattern. We provide one more component which will be the broker between these two components and it will be responsible for handling communication between them.

Components Structure:

1└── Upload.tsx
2 ├── UploadInput.tsx
3 └── UploadPreview.tsx

Example

You can find the full example here

Upload.tsx
1import React, { FC, useState } from 'react'
2import { UploadInput } from './UploadInput'
3import { UploadPreview } from './UploadPreview'
4
5interface UploadState {
6 errors: string[],
7 files: string[],
8 isLoading: boolean
9}
10
11export const Upload: FC = () => {
12 const [uploadState, setUploadState] = useState<UploadState>({
13 errors: [],
14 files: [],
15 isLoading: false
16 })
17
18 const onUploadStart = () => {
19 setUploadState({ ...uploadState, isLoading: true })
20 }
21
22 const onUploadDone = (errors: string[], files: string[]) => {
23 setUploadState({
24 errors,
25 files,
26 isLoading: false
27 })
28 }
29
30 return (
31 <React.Fragment>
32 <UploadInput
33 onUploadStart={onUploadStart}
34 onUploadDone={onUploadDone}
35 />
36 <UploadPreview
37 isLoading={uploadState.isLoading}
38 files={uploadState.files}
39 errors={uploadState.errors}
40 />
41 </React.Fragment>
42 )
43}
UploadInput.tsx
1import React, { FC, ChangeEvent } from 'react'
2import { readFile } from '../../utils'
3
4export const UploadInput: FC<any> = ({ onUploadStart, onUploadDone }) => {
5 const onChange = async (event: ChangeEvent<HTMLInputElement>) => {
6 if (!event.target.files?.length) return
7 await readFile(
8 event.target.files,
9 onUploadStart,
10 onUploadDone
11 )
12 }
13
14 return (
15 <div className="custom-file">
16 <label htmlFor="file-upload" className="custom-file-label">Select files</label>
17 <input
18 id="file-upload"
19 className="custom-file-input"
20 name="file-upload"
21 type="file"
22 accept="image/*"
23 multiple
24 onChange={onChange}
25 >
26 </input>
27 </div>
28 )
29}
UploadPreview.tsx
1import React, { FC } from 'react'
2
3interface UploadPreviewProps {
4 files: string[],
5 errors: string[],
6 isLoading: boolean
7}
8
9export const UploadPreview: FC<UploadPreviewProps> = ({ isLoading, files, errors }) => {
10 if (isLoading) {
11 return (
12 <div className="spinner-border mt-5" role="status">
13 <span className="sr-only">Loading...</span>
14 </div>
15 )
16 }
17
18 return (
19 <div className="container mt-3">
20 <div className="row">
21 <div className="col-12 p-0">
22 <p>Preview</p>
23 </div>
24 {files.map((file, index) => {
25 return <img className="img-fluid rounded" src={file} key={index} alt="..." />
26 })}
27 {errors.map((error, index) => {
28 return (
29 <div className="alert alert-primary" role="alert" key={index}>
30 {error}
31 </div>
32 )
33 })}
34 </div>
35 </div>
36 )
37}
© 2020 by Przemysław Konieczniak - Dev Notes. All rights reserved.