DEV Community

Danish
Danish

Posted on

Ultimate Guide To Write Reusable Components In React

Peace be upon you :) السلام عليكم

At Glance:

  1. The Why & Whats ?
  2. The Solutions - When and where to use or not to use.
  3. Wrap up - Revision

The Why & Whats

Why should we worry about reusability in the first place ? Isn't it a thing that only UI Library authors should do ?

Let me answer it the other way around, you should not worry about reusability only if you are developing something on which no more features are to be added.

Hmmm, but we need to add features on pretty much every project right ? Yes then we should start thinking about reusability as early as possible.

What should we think about

Remember these basics

  • Component = UI + Functionality
  • Two ways to control component's UI & Functionality, Props & States.

Props are controlled by whoever is using the component and states are controlled by component itself, So the more the component depends on props for its ui and functionality the more general it is and hence Reusable. Tada!

Now what we have to think is, How to make a component more dependent on props? Right.

Do we want to make it dependent on compile-time or run-time i.e statically or dynamically?

The Solutions

The questions that are asked above are the scenario you, me and every developer comes across, our solutions, The [X] Pattern, answers for us these questions, lets dive into the patterns then

The Container-View Pattern

The all time famous pattern, coined by dan abramov, whenever we need to achieve reusability, the first try is this pattern.

Container: is a component which handles the functionality, all the side effects are written here i.e Controls the behaviour.

View: is a component which handles the ui rendering based on props. We should always try to make view generic.

UI Code does not changes on any change in business logic.

Here is the basic structure


const Container = () => {

      // all the side effects 
      // states
      return (<View {...states}/>);

}

const View = (props) => {
      return ( UI that depends on props );
}

Enter fullscreen mode Exit fullscreen mode

Example: See followers list on XYZ social media


// so we make a view component, try to make it generic one

const RenderList = ({data}) => {

      <ul>
      {data.map((item) => (
      <li key={item.id}>
          {item.name}
      </li>
      ))}
  </ul>

}

const RenderFollowers = () => {

 // const [followers, setFollowers] = useState([])

 //get followers logic using useEffect or ReactQuery

 return (
     <RenderList data={followers}/>
 );
}

Enter fullscreen mode Exit fullscreen mode

Here we have made our list component reusable, it will remain same if in future we need to add some functionality like search features it will only increase code in the Container component.

Pros

  • Easy to write and understand
  • Makes the UI reusable
  • Separates the UI and Logic concerns beautifully

Cons

  • Can not reuse functionality

The Higher Order Component Pattern

The higher order or enhancer pattern, used to share functionality.

Higher Order Component is a component which receives a component, enhance it and returns enhanced component.

Structure


const withHOC = (Component) => (props) => {

 // The reusable functionality comes here

 return <Component {...props}/>

}

const EnhancedComponent = withHOC(ComponentThatNeedsEnhancing)

Enter fullscreen mode Exit fullscreen mode

So continuing our followers example, what if we decided to add something like if the followers list is empty then show this, if it's loading show loader, if there is some error show error i.e general validations.

we will add our logic in RenderFollowers() component right ? now we decided to make some other lists, that are in need of these general validations too ... hmm ? HOC to rescue


const withGeneralValidations = (Component) => (props) => {

      {props.isLoading && <LoadingComp />}
      {props.error && <ErrorComp {...props}/>}
      {!props.data.length && <EmptyListComp />}


      return (

         <Component {...props} />

       );
}

//we can wrap our RenderList Component in //withGeneralValidations(), and then render enhancedRenderList //in RenderFollowers Component.

const EnhancedRenderList = withGeneralValidations(RenderList);

// Change RenderList to EnhancedRenderList inside of RenderFollowers Component.

}
Enter fullscreen mode Exit fullscreen mode

Here we have written reusable functionality, which we can use with other lists too.

HOC Pattern is heavily used in Redux and also Middleware in backend are a type of HOC usage.

Pros

  • HOC's are highly composable, that means we can add more functionality by using compose.

  • HOC's have accessibility to its children props, that can be a huge plus in some scenarios.

Cons

  • The functionality is applied in a static way i.e at compile time, so we can not do some dynamic stuff.

  • Props Collision

The Component With Render Callbacks/Render Props

Now there is a shared piece of state/information that needs to be used dynamically.

So What do you do ? You Surrender the render :)

Render Props: means the component to be rendered are passed via props, and are called Parent will only call props.render() inside its return.

Structure:


const Parent = (props) => {

  //Shared States

  return props.children(sharedStates);


}

//we can also pass a prop render then call props.render(sharedStates)

//Usage

<Parent>

  {(sharedStates) => {

    //Work with sharedStates
    //here the user of Parent Component have freedom to render any ui

    return ComponentsWhichNeedsSharedStates;

  }}

</Parent>

Enter fullscreen mode Exit fullscreen mode

Example: assuming the same case as HOC


const RenderFollowersList = (props) => {

      //get followers and set the states here
      const {isLoading, error, data} = getFollowers();

      return (

        {isLoading && <LoadingComp />}
        {error && <ErrorComp {...props}/>}
        {!data.length && <EmptyListComp />}

        props.children(data)

       );
}

//it is doing the same as hoc right ? its power comes at use time

<RenderFollowersList>
 {(data) => {

   return(
     <>
       <RenderList data={data} />
     </>
   );

 }}

</RenderFollowersList>

//Now we want also to show count of followers and also perform some search on the followers

//we just add these inside our return 

  ...
  return(
     <> 
       <SearchBar data={data}/>
       <Count count={data.length} />
       <RenderList data={data} />

     </>
   );
  ...

//Simple right we can add our features on the go.

Enter fullscreen mode Exit fullscreen mode

Pros

  • Gives freedom to user of code

Cons

  • Adds extra layer of complexity

The Compound Component

Compound Components: are components which are separate in their logic and ui but uses same state or Multiple components that work together to achieve single task.

  • To share the state between components, ContextApi is used

Structure:


   //CompoundComp.js

   const SharedContext = createContext()

   export default ParentComp = (props) => {

     const [sharedState, setSharedState] = useState(false)

     return (
       <SharedContext.Provider value={{ sharedState, setSharedState }}>
         {props.children}
       </SharedContext.Provider>
     );


   }

//Now we create as many pieces as we like

const ComponentThatNeedsContext = () => {
  const { sharedState, setSharedState } = useContext(SharedContext);

  return (
    <div onClick={() => changeSharedState()}>
      {//something that uses sharedstate}
    </div>
  );
}

ParentComp.ComponentThatNeedsContext = ComponentThatNeedsContext;


//otherFile.js

// To use our compound component

import Parent from 'CompoundComp.js';

...
<Parent>
   <ComponentThatNeedsContext />
<Parent>
...


Enter fullscreen mode Exit fullscreen mode

Example:

there can be many use cases like the nav hamburger or accordions, UI libraries make use of this heavily, but i will change our list component


//so we can make Followers data as context 

const FollowersContext = useContext()

const RenderFollowers = (props) => {

 const [followers, setFollowers] = useState([]);

 //Getting and setting the followers

 return(

  <FollowersContext value={{followers, setFollowers}}>
     {props.children}
  </FollowersContext>

 );

const RenderFollowersList = () => {
 const { followers } = useContext(FollowersContext);

 return <RenderList data = {followers}/>
}

RenderFollowers.RenderFollowersList = RenderFollowersList

const RenderSearch = () => {
 const { followers, setFollowers } = useContext(FollowersContext);

 const filterFollowers = (e) => {
   const query = e.target.value;
   //filter logic
   setFollowers(filteredFollowers)
 }

 return <Search data = {followers} onChange={filterFollowers}/>
}

RenderFollowers.RenderSearch = RenderSearch;


const RenderFollowersCount = () => {
 const { followers} = useContext(FollowersContext);

 return ({`Followers: followers.count`})
}}

RenderFollowers.RenderFollowersCount = RenderFollowersCount;


//Now we can make it a whole component

const Followers = () => {

  <RenderFollowers>
     <RenderFollowers.RenderSearch />

     <RenderFollowers.RenderFollowersList />

     <RenderFollowers.RenderFollowersCount />

  </RenderFollowers>

}

Enter fullscreen mode Exit fullscreen mode

//Sorry for the naming :P

Pros

  • Avoids Props Drilling
  • Complexity is reduced as compare to other patterns

Cons

  • Only direct children can have access to shared state.

Wrap up

Wrapping it all up, there are no perfect solutions so pick and weigh all your pros and cons and then apply it. You will only see affects after some time in a project.

  • Container/View is very good and easy, we should try to apply it everywhere except when the needs arise.

  • HOC are very good at sharing functionalities, are highly composable, that means where ever you need to enhance functionalities you should apply. Whenever there is a need in sharing the behaviour, use HOC until the need arise.

  • Render Props pattern is heavily focused on giving as much as possible freedom to the user of the code. So if you don't know how a functionality is to be used you should 'surrender the render' .

  • Compound Components are great at sharing the functionality with reduced complexity.

Note: There may be issues with the examples in other ways, I tried to make it as real as possible, if you find any mistakes or any good practices, I will be more than happy to do post scripts.

All of the above, is what I have learned from others so I feel heavily in dept, and also to get better insight see there work too

Credits

Top comments (0)