Refactoring React: Dealing with Monster Render Methods

In the last year and a half at Revelry, we’ve used React as our view framework for about a dozen projects. We were React early adopters and we’ve worked on some large React codebases, so we’ve gained some insight into the issues that can creep into growing React projects. Today I want to show you some React code that has gotten out of hand and how you can clean it up.

Refactoring React

Let’s say you are building a social network with React. You have a “card” component which can display different kinds of media. You started with plain text posts, but later you added images and then videos. You might end up with something like this:

class Card extends React.Component {
	// ...

	render() {
		if(this.props.type == 'text') {
			return <div class="Card">
				<div>
					<AuthorPhoto />
					{this.props.card.author.name}
				</div>
				{this.props.card.body}
			</div>
		} else if(this.props.type == 'image') {
			return <div class="Card Card--image">
				<div>
					<AuthorPhoto />
					{this.props.card.author.name}
				</div>
				<img src={this.props.card.src} />
			</div>
		} else if(this.props.type == 'video') {
			return <div class="Card Card--video">
				<div>
					<AuthorPhoto />
					{this.props.card.author.name}
				</div>
				<video>
				 <source src={this.props.card.src} />
				</video>
			</div>
		}
	}
}

This is obviously bad, but it is easy to accidentally create this kind of code when you move fast and add feature after feature without pausing to refactor.

Notice that most of the markup between the three cases is the same. The first thing we can do is move some of the JSX outside of the conditionals:

class Card extends React.Component {
	// ...

	render() {
		let content
		let cardClass = `Card Card--${this.props.type}`

		if(this.props.type == 'text') {
			content = this.props.card.body
		} else if(this.props.type == 'image') {
			content = <img src={this.props.card.src} />
		} else if(this.props.type == 'video') {
			content = <video>
				<source src={this.props.card.src} />
			</video>
		}

		return <div class={this.props.cardClass}>
				<div>
					<AuthorPhoto />
					{this.props.card.author.name}
				</div>
				{content}
			</div>
	}
}

It is a little DRYer, but it still reads like a procedure rather than the nice declarative template that we want from JSX. You can make the render method easier to read by shifting some of the complexity to separate methods:

class Card extends React.Component {
	// ...

	get cardClass() {
		return `Card Card--${this.props.type}`
	}

	get cardContent() {
		if(this.props.type == 'text') {
			return this.props.card.body
		} else if(this.props.type == 'image') {
			return <img src={this.props.card.src} />
		} else if(this.props.type == 'video') {
			return <video>
				<source src={this.props.card.src} />
			</video>
		}
	}

	render() {
		return <div class={this.cardClass}>
				<div>
					<AuthorPhoto />
					{this.props.card.author.name}
				</div>
				{this.cardContent}
			</div>
	}
}

This is a little better. At least the render method is simple and reads like a template.

However, there’s a more fundamental problem: we have one component doing three jobs. We also aren’t following the React philosophy of making small, reusable components and then composing them to build our interface. Let’s try to split this up in a better way:

class TextCard extends React.Component {
	render() {
		return <CardAuthorFrame>
			{this.props.card.text}
		</CardAuthorFrame>
	}
}

class ImageCard extends React.Component {
	render() {
		return <CardAuthorFrame>
			<img src={this.props.card.src} />
		</CardAuthorFrame>
	}
}

class VideoCard extends React.Component {
	render() {
		return <CardAuthorFrame>
			<video>
				<source src={this.props.card.src} />
			</video>
		</CardAuthorFrame>
	}
}

class CardAuthorFrame extends React.Component {
	render() {
		return <div>
			<div>
				<AuthorPhoto />
				{this.props.card.author.name}
			</div>
			{this.props.children}
		</div>
	}
}

class Card extends React.Component {
	get cardComponent() {
		switch(this.props.card.type) {
			case 'text':
				return TextCard
			case 'image':
				return ImageCard
			case 'video':
				return VideoCard
		}
	}

	render() {
		return React.createElement(this.cardComponent, this.props)
	}
}

Even though this is more lines of code, it is easier to follow because we’ve divided it up into smaller named chunks that have a single responsibility. We’d actually split each component into its own file to make things clearer and easier to search.

I hope this example helps you to tackle your monster render methods. If you have other React problems you’d like me to tackle in future posts, leave a comment or send me a question on ask.fm. Good luck with refactoring react and updating your code.

Love making great software? Let’s talk!

Apply to work with us!

Revelry provides Executive Advising Services, Custom Software Development, and Design Sprints.
Keep in touch by subscribing to CODING CREATIVITY.

More Posts by Robert Prehn: