Null Object Pattern with React

Damian Galarza
Catch&Release Technology
3 min readFeb 25, 2019

In object oriented programming, the Null Object pattern is an object that can be used to represent null behavior. This typically helps make your code more readable and easier to maintain. Let’s take a look at how we might practice this in React.

Rendering null

In our application, a user can upload different types of documents. When viewing a list of those documents, they can select an action to perform. See the screenshot below.

Documents table with multiple types of documents.

Each of the document types has a different set of actions that can be performed. In order to encapsulate this complexity, we extracted a separate component to act as an adapter. Handling the check against the type of document we’re operating on and what should be rendered for it.

const DocumentActions = ({ document }) => {
if (document.documentType === ‘external_document’) {
return <ExternalDocumentActions document={document} />;
} else if (document.documentType === ‘uploaded_document’) {
return <UploadedDocumentActions document={document} />;
}
};

This allows us to have a pretty straightforward test.

import { shallow } from ‘jest’;describe(‘<DocumentActions />’, () => {
context(‘actions for an external document’, () => {
it(‘renders the ExternalDocumentActions component’, () => {
const document = { documentType: ‘external_document’ };
const documentActions = shallow(
<DocumentActions document={document} />
);
expect(documentActions.find(ExternalDocumentActions))
.toExist();
});
});
context(‘actions for uploaded document’, () => {
it(‘renders the UploadedDocumentActions component’, () => {
const document = { documentType: ‘uploaded_document’ };
const documentActions = shallow(
<DocumentActions document={document} />
);
expect(documentActions.find(UploadedDocumentActions))
.toExist();
});
});
});

With only two document types, we can already see some duplication. That is, the props being passed down into the document actions for a given type. Each actions component requires access to the document itself. Another problem we have is that we can’t easily test what happens in the event that an unsupported document type is passed in.

Let’s start by cleaning up some of the duplication.

const DocumentActions = ({ document }) => {
let Component;
if (document.documentType === 'external_document') {
Component = ExternalDocumentActions;
} else if (document.documentType === 'uploaded_document') {
Component = UploadedDocumentActions
}
Component ? <Component document={document} /> : null;
};

This cleans up the duplication and we could easily stop here. However, we’ve now introduced another branch in our code and as a result, more complexity. Now we have to make sure that we check if `Component` is actually something that can be rendered by React or if we need to do something else instead.

Let’s introduce a `Null` component to see how we might clean this up further.

const Null = () => null;

Our Null component implementation is quite basic. All it is is a function that returns null. This satisfies the JSX expected interface to be a function, which can take args for props and returns what to render if anything.

Now our code can become:

const DocumentActions = ({ document }) => {
let Component;
switch (document.documentType) {
case 'external_document':
Component = ExternalDocumentActions;
break;
case 'uploaded_document':
Component = UploadedDocumentActions;
default:
Component = Null;
}
return <Component document={document} />;
};

Now, our DocumentActions component has a single clear interface and responsibility; and we can even test the default state.

import { shallow } from 'jest';describe('<DocumentActions />', () => {
context('actions for an external document', () => {
it('renders the ExternalDocumentActions component', () => {
const document = { documentType: 'external_document' };
const documentActions = shallow(
<DocumentActions document={document} />
);
expect(documentActions.find(ExternalDocumentActions))
.toExist();
});
});
context('actions for uploaded document', () => {
it('renders the UploadedDocumentActions component', () => {
const document = { documentType: 'uploaded_document' };
const documentActions = shallow(
<DocumentActions document={document} />
);
expect(documentActions.find(UploadedDocumentActions))
.toExist();
});
});
context('default', () => {
it('renders the Null component', () => {
const document = { documentType: 'unsupported_document' };
const documentActions = shallow(
<DocumentActions document={document} />;
);
expect(documentActions.find(Null)).toExist();
});
});
});

--

--

Damian Galarza
Catch&Release Technology

Developer @catch-release-engineering. Formerly @thoughtbot