Building your first React app, part 2

Continued from Part 1

This is Part 2 of the Building your first React app tutorial.

  • In Part 1, you have a created a simple React app that retrieves and displays articles from a Kentico Cloud Sample Project.
  • In Part 2, you will modify the app to render embedded media, such as videos, and to resolve links between content items.

Table of contents

    Adding videos to articles

    Kentico Cloud gives you a great amount of flexibility for embedding pieces of content inside Rich text elements, such as videos, tweets or code examples.

    For example, here is how inserting videos works in a nutshell:

    • You have a Hosted video content type defining what information about the video your application needs.
    • When writing an article, an editor inserts a Hosted video component or item into a Rich text element.
    • You application resolves the component or item to the appropriate HTML and renders it.

    1. Hosted video content type

    The Sample Project already contains the Hosted video content type.

    Hosted video content type.

    Learn more about adding content types.

    2. Hosted video component in an article

    For example, the Which brewing fits you? article already contains a component based on the Hosted video content type.

    Hosted video component inserted into an article.

    Learn more about adding components. A component is basically a single-use content item. Components and items in Rich text are resolved the same way, their differences only affect how they are displayed in Kentico Cloud.

    3. Define a resolver inside your React app

    Use a content item resolver to define how to render items and components of a specific type in Rich text elements.

    • A content resolver accepts a Content item object as its argument and returns the desired HTML output as a string.
    • It's good practice to define your resolvers in a separate file and import them to your components.

    In the {~src~} folder, create a new file and name it {~itemResolver.js~}:

    • JavaScript
    export const resolveItemInRichText = (item) => { if (item.system.type === "hosted_video") { let video = item; if (video.video_host.value.find(item => item.codename === "vimeo")) { return `<iframe class="hosted-video__wrapper" src="https://player.vimeo.com/video/${ video.video_id.value }?title=0&byline=0&portrait=0" width="100%" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen > </iframe>`; } else if ( video.video_host.value.find(item => item.codename === "youtube") ) { return `<iframe class="hosted-video__wrapper" width="100%" height="360" src="https://www.youtube.com/embed/${ video.video_id.value }" frameborder="0" allowfullscreen > </iframe>`; } } return undefined; };
    export const resolveItemInRichText = (item) => { if (item.system.type === "hosted_video") { let video = item; if (video.video_host.value.find(item => item.codename === "vimeo")) { return `<iframe class="hosted-video__wrapper" src="https://player.vimeo.com/video/${ video.video_id.value }?title=0&byline=0&portrait=0" width="100%" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen > </iframe>`; } else if ( video.video_host.value.find(item => item.codename === "youtube") ) { return `<iframe class="hosted-video__wrapper" width="100%" height="360" src="https://www.youtube.com/embed/${ video.video_id.value }" frameborder="0" allowfullscreen > </iframe>`; } } return undefined; };

    In a real app, you would define a resolver for each content type your editors could use when inserting components or content items into Rich text elements.

    You can add a sprinkle of CSS to the {~index.css~} file to give your videos consistent width:

    • CSS
    body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; } .hosted-video__wrapper, .article_body img { display: block; margin: 0 auto; width: 100%; max-width: 560px !important; }
    body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; } .hosted-video__wrapper, .article_body img { display: block; margin: 0 auto; width: 100%; max-width: 560px !important; }

    4. Register the resolver

    In the {~src~} folder, modify the {~ArticleView.js~} component:

    • Import your content item resolver
    • Add a {~.queryConfig~} method to your API query to register your content resolver.
    • JavaScript
    ... import { resolveItemInRichText } from "./itemResolver"; ... fetchArticle(slug) { client.items() .equalsFilter("elements.url_pattern", slug) .depthParameter(1) .queryConfig({ richTextResolver: resolveItemInRichText }) .getObservable() .subscribe((response) => { console.log(response); this.setState({ loaded: true, article: response.items[0] }); }); }
    ... import { resolveItemInRichText } from "./itemResolver"; ... fetchArticle(slug) { client.items() .equalsFilter("elements.url_pattern", slug) .depthParameter(1) .queryConfig({ richTextResolver: resolveItemInRichText }) .getObservable() .subscribe((response) => { console.log(response); this.setState({ loaded: true, article: response.items[0] }); }); }

    Here, you have added the resolver to a single API query, but you can also define resolvers globally.

    Your app can now display videos inserted into articles.

    Resolved and rendered Hosted video component.

    Linking between content items

    Since content from Kentico Cloud can be delivered to any platform, not just the Web, links to content items inside Rich text elements are by default rendered as {~<a>~} tags:

    • HTML
    <a data-item-id="23f71096-fa89-4f59-a3f9-970e970944ec" href="">delicious cup lying on your table</a>
    <a data-item-id="23f71096-fa89-4f59-a3f9-970e970944ec" href="">delicious cup lying on your table</a>

    Use a link resolver to define how to render links to content items of a specific content type.

    • A link resolver accepts a Rich text link object as its argument and returns the desired relative URL as a string.
    • It's good practice to define your resolvers in a separate file and import them to your components.

    In the {~src~} folder, create a {~linkResolver.js~} file:

    • JavaScript
    export const resolveContentLink = (link) => { if (link.type === "article") { return `/post/${link.urlSlug}`; } return undefined; };
    export const resolveContentLink = (link) => { if (link.type === "article") { return `/post/${link.urlSlug}`; } return undefined; };

    We are keeping it short here on purpose. In a real app, you would define a link resolver for each content type your editors could use when linking to content items in Rich text elements.

    Now you need to register the resolver in the {~ArticleView.js~} component.

    • JavaScript
    //... import { resolveContentLink } from "./linkResolver"; //... fetchArticle(slug) { client.items() .equalsFilter("elements.url_pattern", slug) .depthParameter(1) .queryConfig({ linkResolver: resolveContentLink, }) .getObservable() .subscribe((response) => { console.log(response); this.setState({ loaded: true, article: response.items[0] }); }); }
    //... import { resolveContentLink } from "./linkResolver"; //... fetchArticle(slug) { client.items() .equalsFilter("elements.url_pattern", slug) .depthParameter(1) .queryConfig({ linkResolver: resolveContentLink, }) .getObservable() .subscribe((response) => { console.log(response); this.setState({ loaded: true, article: response.items[0] }); }); }

    You have added the resolver to a single API query, but you can also define resolvers globally.

    Clicking on links to other content items of {~Article~} content type now takes you to the linked article. But the app reloads completely every time you click a link.

    Since you can't use {~<Link>~} components in your resolvers, you need to write a handler method that will navigate to the linked article using {~react-router-dom~} programmatically.

    1. Add a click event handler

    Add an {~onClick~} handler to the {~div~} rendering the HTML of the Rich text element. You need to pass the {~event~} object and the {~bodyCopy~} Rich text element of the article containing metadata about all links to content items.

    • JavaScript
    // ... return ( <div> <Link to="/">Home</Link> <h1>{title}</h1> <div className="article_body" dangerouslySetInnerHTML={{ __html: bodyCopy.getHtml() }} onClick={event => this.handleClick(event, bodyCopy)} /> </div> ); // ...
    // ... return ( <div> <Link to="/">Home</Link> <h1>{title}</h1> <div className="article_body" dangerouslySetInnerHTML={{ __html: bodyCopy.getHtml() }} onClick={event => this.handleClick(event, bodyCopy)} /> </div> ); // ...

    2. Resolve and navigate to the new route

    Add a {~handleClick~} method to the {~ArticleView~} class.

    • JavaScript
    handleClick(event, richTextElement) { if (event.target.tagName === "A" && event.target.hasAttribute("data-item-id")) { event.preventDefault(); const id = event.target.getAttribute("data-item-id"); const link = richTextElement.links.find(link => link.itemId === id); const newPath = resolveContentLink(link); if (newPath) { this.props.history.push(newPath); } } }
    handleClick(event, richTextElement) { if (event.target.tagName === "A" && event.target.hasAttribute("data-item-id")) { event.preventDefault(); const id = event.target.getAttribute("data-item-id"); const link = richTextElement.links.find(link => link.itemId === id); const newPath = resolveContentLink(link); if (newPath) { this.props.history.push(newPath); } } }
    • First, check if the user clicked on a link to a content item, other links should behave normally. Prevent the browser from handling the event with {~event.preventDefault();~}.
    • Instead, handle the event yourself by constructing the new path with your imported {~resolveContentLink~} method.
    • Then, push the new path into the {~history~} object to navigate to the new URL. This passes new props to the {~ViewArticle~} component.

    A note on routing

    There is more that one way to navigate to a new route programmatically and this article does a good job explaining the pros and cons of a different approach.

    It also explains how to use {~withRouter~} if your component is not rendered by {~Router~} and can't access {~history~} through its props.

    3. React to the new route and re-render

    Finally, to fetch the new article and re-render the component after receiving new props, use the {~componentDidUpdate~} lifecycle method in the {~ArticleView~} class.

    • JavaScript
    componentDidUpdate(oldprops) { let oldSlug = oldprops.match.params.slug; let newSlug = this.props.match.params.slug; if (oldSlug !== newSlug) { this.fetchArticle(newSlug); } }
    componentDidUpdate(oldprops) { let oldSlug = oldprops.match.params.slug; let newSlug = this.props.match.params.slug; if (oldSlug !== newSlug) { this.fetchArticle(newSlug); } }

    Conclusion

    Congratulations! Your app can retrieve and display content from Kentico Cloud, change routes, resolve links between content items and display embedded videos.

    You can view the final source code on Github.

    For learning purposes, we have simplified some aspects of the application in this tutorial. For example, we recommend defining strongly-typed models for your project's content types.

    What's next?