Building reusable components with React and Redux

Jan 10, 2017 Anton Domratchev

Building reusable components with React and Redux.

Recently, I've been spending a majority of my time in React and I began to really enjoy front-end JavaScript development. I avoided JavaScript from the beginning of my career as a programmer, and I think it was mostly because I didn't have a good way of keeping and maintaining a state in components. I've been writing a React Redux single page application for the past several months and have learned a great deal about making front end applications. Most of all, I learned to build reusable components in React. These components can be used throughout the application and even packaged to be used in other projects because of their scoped action prop. While on the project, I made a component which could dispatch an action to a number different reducers. The component would receive a scope prop from its highest order parent, that prop would contain the component's scoped action type .

What makes React a great platform for building applications is its powerful component composition model. The React team at Facebook encourages the use of component composition for building container components. An example of a container component would be a Tweet on a page which is composed of TweetHeader , TweetLikeButton , and TweetBody among other similar components. Some components can have children components as well, MentionedUsernames and Hashtags would compose the TweetBody component. Such component structure allows for high reusability of each child component throughout the application. A generic button component can be specialized to work in any context with use of props. Props are simply properties that are passed from the parent component to the child component. Props are also useful when working Redux for state management. By providing the component with a scoped action type prop we can use the same component action with different reducers.

Let's look at an example of the component structure in this situation.

|app |--containers |----HomeFeedPage |----UserFeedPage |----TweetPage |--components |----Tweet |------TweetHeader |------TweetLikeButton

From the containers directory we can see that a Tweet component may be used in three different contexts of HomeFeedPage , UserFeedPage , and TweetPage . A Tweet component contains a TweetLikeButton component which would dispatch an action to transform some data in the state.

First, we will need to create a scope for the shared action types

export const HOME_FEED = { INCREMENT_LIKE_COUNT : "HOME_FEED_INCREMENT_LIKE_COUNT" , } export const USER_FEED = { INCREMENT_LIKE_COUNT : "USER_FEED_INCREMENT_LIKE_COUNT" , } export const TWEET_PAGE = { INCREMENT_LIKE_COUNT : "TWEET_PAGE_INCREMENT_LIKE_COUNT" , }

As you may have already noticed, the scope is just a simple object which namespaces the action type. We can continue to use this scope to designate which action belongs where.

Let's start looking at the components going from the highest order parent down to the child component with our scoped action. HomeFeedPage container might look like so:

/** imported dependencies and components */ import { HOME_FEED } = 'actions/actionTypes' export default class HomeFeedPage extends React . Component { componentWillMount () { /** Do some async action */ } render () { const { tweets } = this . props return ( < div > tweets . map ( tweet => < Tweet {... tweet } scope = { HOME_FEED } /> ) || nul l < /div > ) } }

Notice the scope is being injected into props at this compoent. In this component we map over the tweet data and return a component with props.

Let's take a look at what Tweet would look like:

/** imported dependencies and components */ export default const Tweet = ( props ) => { return ( < div > < TweetHeader tweetHeader = { props . tweetHeader } / > < TweetLikeButton tweetId = { props . tweetId } scope = { props . scope } / > < /div > ) }

In this component we pass the scope prop down to the connected component. The parent component should have PropTypes validation to require the scope prop. We dont need to validate the structure at this point, we just want to make sure we have that prop present, since it is being used by child compoents.

Next, let's take a look at the button code:

/** imported dependencies and components */ class TweetLikeButton extends React . Component { handleLikeClick = ( e ) => { const { dispatch } = this . props dispatch ( handleTweet ( this . props . tweetId , this . props . scope . INCREMENT_LIKE_COUNT // This action should be validated in the props )) } render () { return < button onClick = { handleLikeClick } value = "Like this Tweet" /> } } export default connect ()( TweetLikeButton ) /** Validate props for presence of the action type */

By providing the scope prop to TweetLikeButton we can now dispatch actions to the reducer provided in the scope. At this point we will need to validate that the passed scope prop contains the action that our component is expecting.

Now, let's look at the handleTweet action:

function incrementLikeCount ( tweetId , actionType ) { return { type : actionType , payload : tweetId } } export function handleTweet ( tweetId , actionType ) { return dispatch => { return asyncCallToApi ( tweetId ) . then ( json => dispatch ( incrementLikeCount ( tweetId , actionType ))) } }

Our handleTweet action takes an id and an actionType arguments. The action type is scoped so now it will be picked up by the corresponding reducer. I am omitting the reducer code as it is irrelevant for this example.

The above implementation is simplified for brevity but a more robust solution would be to implement a switch case on the action type in the action creator function. By white listing the actions we would expect to get from the component, it would allow us to add a default NOOP action in case the component gave the action creator something unexpected.