DEV Community

loading...
Cover image for A powerful and proven way to include views on list rendering

A powerful and proven way to include views on list rendering

Coding Bugs
Updated on ・5 min read

React is a powerful javascript library that allows us to implement incredibly easy code. Hooks, use of JSX, easy components creation, and more features make developers create rich web experiences in few minutes. When complexity knocks on the door, we should combine great technical designs along with the features provided by React and give smart solutions to our problems.

This article shows how lists rendering can evolve from the simplest solution to solve easy requirements to a solution that follows the open/close principle and uses the visitor pattern for complex requirements.

Consider this article to be programming language agnostic although I use React for the examples provided.

The standard way

The standard way of rendering any list of items in React is very simple and efficient. The following example has been taken from the React official documentation. You may notice you can render any list of items just in 3 lines of code or 8 lines for readability.

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li>{number}</li>
);

ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

This is the main way of rendering list of items and you should follow it in case of having very simple lists without any logic or simple logic involved in them.

What about if we have several kind of visualizations

But, what happens when you have to modify the way your items have to be shown depending on an external variable?

Your logic can be adapted and create the proper components to render data in one way or another. For example, if we have a requirement to render the previous numbers in a table instead of a list we must change our code. Besides this requirement, we also have another one that lets the user set the way she wants to view the items.

The following code is an improvement of the previous one setting the proper components to reach the requirements:

const numbers = [1, 2, 3, 4, 5];

// View components
function ListView({ items }) {
  return <ul>
    {items && items.map(i => <li key={i}>{i}</li>)}
  </ul>;
}

function TableView({ items }) {
  return <table>
    <tbody>
    {items && items.map(i => <tr key={i}><td>{i}</td></tr>)}
    </tbody>
  </table>;
}

// View selector
function ViewSelector({ options, onSelect }) {
  return <div>
    {options && options.map(o => 
      <div key={o}><a href="#" onClick={() => onSelect(o)}>{o}</a></div>)
    }
  </div>;
}

// Application component
function App() {
  const options = ['list', 'table'];
  const [view, setView] = React.useState(options[0]);

  const onSelectHandler = (option) => {
    setView(option);
  };

  return <div>
    <ViewSelector options={options} onSelect={onSelectHandler} />
    {view === 'list' && <ListView items={numbers} />}
    {view === 'table' && <TableView items={numbers} />}
  </div>;
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

This code works fine and, visually, is really simple and easy to read. As a new developer in the team, you will be able to understand the previous code fastly and identify the responsibilities of each component. Hence, you will be able to evolve the code or to solve any issue that could appear in it.

As an example of evolution, a new requirement could be added to see the numbers inline, and would be easy to create a new View component and add it to the options to be selected. The new code could be something like the following:

const numbers = [1, 2, 3, 4, 5];

// Notice the new view component
function InlineView({ items }) {
  return items && items.map(i => <span>{i}</span>);
}

function ListView({ items }) {
  return <ul>
    {items && items.map(i => <li key={i}>{i}</li>)}
  </ul>;
}

function TableView({ items }) {
  return <table>
    <tbody>
    {items && items.map(i => <tr key={i}><td>{i}</td></tr>)}
    </tbody>
  </table>;
}

function ViewSelector({ options, onSelect }) {
  return <div>
    {options && options.map(o => 
      <div key={o}><a href="#" onClick={() => onSelect(o)}>{o}</a></div>)
    }
  </div>;
}

function App() {
  // Notice the new option
  const options = ['list', 'table', 'inline'];
  const [view, setView] = React.useState(options[0]);

  const onSelectHandler = (option) => {
    setView(option);
  };

  // Notice how the new component has been added depending on `view` value
  return <div>
    <ViewSelector options={options} onSelect={onSelectHandler} />
    {view === 'list' && <ListView items={numbers} />}
    {view === 'table' && <TableView items={numbers} />}
    {view === 'inline' && <InlineView items={numbers} />}
  </div>;
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

We are breaking the open/close principle

Imagine now that requirements are focused on providing more functionality to how items are shown in the app. In addition to this, if we want to apply more quality to our code and get greener lights on code review processes, we have to understand that the previous code is breaking the open/close principle.

The open/close principle says: "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"

Our App component has to be modified every time a new view is created or an existing one is replaced. Test -unit, integration, or any other kind- has to be modified as well in case we have to code them. All these factors add more uncertainty on how our code will work and this is something we need to avoid.

How visitor pattern can be helpful

Our objective here is to close our App component functionality to avoid any modification in the code. To reach this, we need to apply some changes that we will see in the paragraphs below.

Firstly, we need to create a new service with all the view types available and the View components related to each of these options.

function ViewersService() {

  // service variable
  const views = {};

  return {
    // provide a copy of the views variable
    get() {
      return Object.assign({}, views);
    },

    // associate a view component to a type   
    register(type, viewComponent) {
      if(undefined === views[type]) {
        views[type] = [];
      }

      views[type].push(viewComponent);
    }
  };
}

// service instantiation
const viewers = new ViewersService();

// views registration
viewers.register('list', ListView);
viewers.register('table', TableView);
viewers.register('inline', InlineView);
Enter fullscreen mode Exit fullscreen mode

Secondly, we have to provide this instance to our App component through parameters. And, then, we will use it for getting the available options and to render the proper view component depending on the user selection.

In the following code, we are using the option selected as the validator to determine if we need to visit the view component. We assume that this value is the one to check for.

// Notice viewers parameter
function App({ viewers }) {

  // Notice here that we get the views registrations from the instance
  const views = viewers.get();

  // Notice how options are obtained from the views keys
  const options = Object.keys(views);
  const [viewOption, setViewOption] = React.useState(options[0]);

  const onSelectHandler = (option) => {
    setViewOption(option);
  };

  // _views[viewOption]_ is the formula that determines the components to be visited  
  const viewsToVisit = views[viewOption];

  // Notice how we go through all views registered for the option selected and render altogether.
  const allViews = viewsToVisit.map(View => <View items={numbers} />);

  return <div>
    <ViewSelector options={options} onSelect={onSelectHandler} />
    {allViews}
  </div>;
}
Enter fullscreen mode Exit fullscreen mode

At first sight, this code could be a bit challenging for a newbie because of the components and objects involved. I know this example is something relatively small but consider this solution to a wider and larger application.

In the case of new requirements, a developer has to create the new View component and register it in the service. As an example, if we must only render the first item, the following code should be added:

function FirstItemView({ items }) {
  return items && <span>{items[0]}</span>;
}

// this line to be added in the proper place
viewers.register('first', FirstItemView);
Enter fullscreen mode Exit fullscreen mode

Wrapping up

This article tries to show a way to improve our code and its maintainability and readability by applying the visitor pattern widely used.

I think this is something challenging in the very first moment but will be useful when the increase in complexity and, therefore, the lines of code happens.

What do you think about the exercise made in this article?
Hope this can be useful to you or just have fun reading it.

Discussion (0)