DEV Community

Cover image for Umbraco 11 + .NET 7 + React + Vite + Hooks Part 3
Mohammed Zaki
Mohammed Zaki

Posted on

Umbraco 11 + .NET 7 + React + Vite + Hooks Part 3

in this article we will do the following

  • update homepage content in umbraco and add new property
  • create a service and interface to return data from umbraco and add it to DI
  • create a controller to use that service
  • create custom hook to fetch data from umbraco API

Updating our current homepage

  • navigate to /umbraco and login to dashboard
  • click on settings/document types > homepage
  • click on add property > enter name as image, then click on select editor and select image cropper

Image description

  • click on save to save the changes
  • then click on settings (check image below) and select models builder to update homepage model so we can use it our service

Image description

  • then click on Content, update the content and click Save and publish

Image description

current folder structure without ClientApp,

├├─> MyCustomUmbracoProject
 │ ├─> .vscode
 │  │ ├─> launch.json
 │  │ ├─> tasks.json
 │ ├─> appsettings.json
 │ ├─> ClientApp //to be covered in bit
 │ ├─> Controllers
 │  │ ├─> HomeController.cs
 │  │ ├─> ContentController.cs
 │ ├─> Interfaces
 │  ├─> IContentService.cs
 │ ├─> Models
 │  │ ├─> GenericResult.cs
 │  │ ├─> HomepageDTO.cs
 │ ├─> Services
 │  ├─> ContentService.cs
 │ ├─> umbraco
 │  ├─> models
 │    │ ├─> Homepage.generated.cs
Enter fullscreen mode Exit fullscreen mode

add Interfaces, Services Folder and create the following

  • under Models add 2 classes HomepageDTO, GenericResult

namespace MyCustomUmbracoProject.Models
{
    public class GenericResult<T>
    {
        public T Data { get; set; }
        public bool Success { get; set; }
        public string Message { get; set; } = null;
        public string Error { get; set; } = null;
        public IEnumerable<string> ErrorMessages { get; set; } = Enumerable.Empty<string>();
    }

}
Enter fullscreen mode Exit fullscreen mode
namespace MyCustomUmbracoProject.Models
{
    public class HomepageDTO
    {
        public string? Title { get; set; }
        public string? ImageUrl { get; set; }
    }
}

Enter fullscreen mode Exit fullscreen mode
  • create a interface named IContentService
using MyCustomUmbracoProject.Models;

namespace MyCustomUmbracoProject.Interfaces
{
    public interface IContentService
    {
        GenericResult<HomepageDTO> GetHomeContent();

    }
}
Enter fullscreen mode Exit fullscreen mode
  • create a Service named ContentService

using MyCustomUmbracoProject.Interfaces;
using MyCustomUmbracoProject.Models;
using Umbraco.Cms.Web.Common;
using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;

namespace MyCustomUmbracoProject.Services
{
    public class ContentService : IContentService
    {
        private UmbracoHelper _umbracoHelper;
        private readonly ILogger<ContentService> _logger;

        public ContentService(ILogger<ContentService> logger, UmbracoHelper umbracoHelper)
        {
            _logger = logger;
            _umbracoHelper = umbracoHelper;

        }

        public GenericResult<HomepageDTO> GetHomeContent()
        {
            GenericResult<HomepageDTO> result = new GenericResult<HomepageDTO>();
            try
            {
                var model = this._umbracoHelper?.ContentAtRoot()?.DescendantsOrSelf<ContentModels.Homepage>().FirstOrDefault() ?? null;
                result.Data = new HomepageDTO()
                {
                    Title = model?.Title ?? "",
                    ImageUrl = model?.Image?.Src ?? ""
                };
                result.Success = true;
                result.Message = "Content Fetched Successfully";
            }
            catch (System.Exception ex)
            {
                result.Success = false;
                result.Message = "Something went wrong";

                result.Error = ex.Message;
                this._logger.LogError(ex, "error while getting data for HomeContent");
            }
            return result;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • inject the service and interface in DI in Startup.cs
 public void ConfigureServices(IServiceCollection services)
        {
           //Rest of code
            services.AddScoped<IContentService, ContentService>();
        }
Enter fullscreen mode Exit fullscreen mode
  • create controller named Content Controller and inject content service into constructor
using Microsoft.AspNetCore.Mvc;
using MyCustomUmbracoProject.Models;
using MyCustomUmbracoProject.Interfaces;

namespace MyCustomUmbracoProject
{
    [Route("api/[controller]")]
    public class ContentController : ControllerBase
    {
        private IContentService _contentService;

        public ContentController(IContentService contentService)
        {
            _contentService = contentService;
        }

        [HttpGet]
        [Route("home-content")]
        public ActionResult<HomepageDTO> FetchHomeContent()
        {
            var result = this._contentService.GetHomeContent();

            return Ok(result);
        }
    }

}

Enter fullscreen mode Exit fullscreen mode

now for the ClientApp

├─> MyCustomUmbracoProject
 │ ├─> ClientApp
 │  │ ├─> src
 │  │  │ ├─> components
 │  │  │  ├─> Spinner.tsx
 │  │  │ ├─> hooks
 │  │  │  ├─> use-fetch.ts
 │  │  │ ├─> models
 │  │  │  │─> generic-result.ts
 │  │  │  ├─> home-page.ts

Enter fullscreen mode Exit fullscreen mode

create use-fetch.ts hook into hooks folder

import { useCallback, useEffect, useReducer, useRef } from 'react'

interface State<T> {
    response?: T
    error?: Error
    loading: boolean;
    runQuery: (params?: Record<string, any>) => void;
}
interface FetchOptions<T> {
    url?: string;
    options?: RequestInit;
    runOnFirstRender?: boolean;
}

type Cache<T> = { [url: string]: T }

// discriminated union type
type Action<T> =
    | { type: 'loading' }
    | { type: 'fetched'; payload: T }
    | { type: 'error'; payload: Error }

const useFetch = <T = unknown>({ url, options, runOnFirstRender = true }: FetchOptions<T>): State<T> => {
    const cache = useRef<Cache<T>>({})

    // Used to prevent state update if the component is unmounted
    const cancelRequest = useRef<boolean>(false)

    const initialState: State<T> = {
        error: undefined,
        response: undefined,
        loading: false,
        runQuery: () => (params?: Record<string, any> | undefined): void => {

        }
    }

    // Keep state logic separated
    const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
        switch (action.type) {
            case 'loading':
                return { ...initialState, loading: true }
            case 'fetched':
                return { ...initialState, response: action.payload, loading: false }
            case 'error':
                return { ...initialState, error: action.payload, loading: false }
            default:
                return state
        }
    }

    const [state, dispatch] = useReducer(fetchReducer, initialState)
    const runQuery = useCallback((params?: Record<string, any>) => {
        if (url) {
            fetchData(url, params)
        }

    }, [url])
    const fetchData = async (url: string, params?: Record<string, any>) => {
        dispatch({ type: 'loading' })

        // If a cache exists for this url, return it
        if (cache.current[url]) {
            dispatch({ type: 'fetched', payload: cache.current[url] })
            return
        }

        try {
            const response = await fetch(url, options)
            if (!response.ok) {
                throw new Error(response.statusText ? response.statusText : response.status.toString())
            }

            const data = (await response.json()) as T
            cache.current[url] = data
            if (cancelRequest.current) return

            dispatch({ type: 'fetched', payload: data })
        } catch (error) {
            if (cancelRequest.current) return

            dispatch({ type: 'error', payload: error as Error })
        }
    }


    useEffect(() => {
        // Do nothing if the url is not given
        if (!url) return

        cancelRequest.current = false


        if (runOnFirstRender) fetchData(url, options)
        // Use the cleanup function for avoiding a possibly...
        // ...state update after the component was unmounted
        return () => {
            cancelRequest.current = true
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [url, runOnFirstRender])

    return { response: state.response, loading: state.loading, error: state.error, runQuery: runQuery }
}

export default useFetch

Enter fullscreen mode Exit fullscreen mode

-update App.tsx

import { Route, HashRouter as Router, Routes } from "react-router-dom";
import "./App.css";
import reactLogo from "./assets/react.svg";
import viteLogo from "./assets/vite.png";
import { About } from "./pages/About";
import { ContactUs } from "./pages/Contact";
import { Home } from "./pages/Home";
import { NotFound } from "./pages/NotFound";

export const App = () => {
  return (
    <>
      <nav>
        <h2 className="title">Vite + React + Umbraco</h2>
        <ul>
          <li>
            <a href="/">Home </a>
          </li>
          <li>
            <a href="#/about">About</a>
          </li>
          <li>
            <a href="#/contact">Contact</a>
          </li>
        </ul>

      </nav>
      <div className="App">
        <div>
          <a href="https://vitejs.dev" target="_blank">
            <img src={viteLogo} className="logo" alt="Vite logo" />
          </a>
          <a href="https://reactjs.org" target="_blank">
            <img src={reactLogo} className="logo react" alt="React logo" />
          </a>
          <a href="https://reactjs.org" target="_blank">
            <img
              src="https://user-images.githubusercontent.com/6791648/60256231-6e710c00-98d1-11e9-8120-06eecbdb858e.png"
              className="umbraco logo"
              alt="umbraco logo"
            />
          </a>
        </div>
        <Router>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/contact" element={<ContactUs />} />
            <Route path="*" element={<NotFound />} />
          </Routes>
        </Router>
        <div className="card"></div>
      </div>
    </>
  );
};

Enter fullscreen mode Exit fullscreen mode
  • update app.css
* {
  padding: 0;
  margin: 0;
}

#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
  filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: no-preference) {
  a:nth-of-type(2) .logo {
    animation: logo-spin infinite 20s linear;
  }
}

.error {
  color: red;
}

/* nav start */
nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background: #cfd8dc;
}

nav ul {
  display: flex;
  list-style: none;
}

nav li {
  padding-left: 1rem;
}

nav a {
  text-decoration: none;
  color: #0d47a1;
}

/* 
  Extra small devices (phones, 600px and down) 
*/
@media only screen and (max-width: 600px) {
  nav {
    flex-direction: column;
  }
  nav ul {
    flex-direction: column;
    padding-top: 0.5rem;
  }
  nav li {
    padding: 0.5rem 0;
  }
}

.title {
  color: #242424;
}

/* To center the spinner*/
.pos-center {
  position: fixed;
  top: calc(50% - 40px);
  left: calc(50% - 40px);
}

.loader {
  border: 10px solid #f3f3f3;
  border-top: 10px solid #3498db;
  border-radius: 50%;
  width: 80px;
  height: 80px;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.center {
  display: flex;
  justify-content: center;
  align-items: center;
}

Enter fullscreen mode Exit fullscreen mode

-update Home.tsx

import useFetch from "../hooks/use-fetch";
import { HomePage } from "../models/home-page";
import { GenericResult } from "../models/generic-result";
import Spinner from "../components/Spinner";

export const Home = () => {
  const {
    response: { data, success, error: errorMsg } = {
      data: undefined,
      success: false,
      error: undefined,
    },
    error,
    loading,
  } = useFetch<GenericResult<HomePage>>({ url: "api/content/home-content" });



  return (
    <>
      <Spinner loading={loading} />
      {error && <p className="error">error ...{error?.message}</p>}
      {!loading && !success && <p className="error">error ...{errorMsg}</p>}
      {success && data && (
        <div>
          title : {data.title}
          <div>
            {data.imageUrl && (
              <img
                title={data.title}
                src={`${data.imageUrl}`}
                width="auto"
                height="200px"
              />
            )}
          </div>
        </div>
      )}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Final Output

Image description

let me know if you're interested in content like,

if would like to continue this series also let me know..

Socials:
LinkedIn: https://www.linkedin.com/in/mohammedzaky
GitHub: https://github.com/mozaky

Top comments (3)

Collapse
 
j0p profile image
Jop ⚡

An excellent resource for developers looking to explore the integration of these technologies! This article strikes a balance between being informative and concise, making it accessible to developers at various skill levels. Thank you for sharing! 🙌

Collapse
 
mozaky profile image
Mohammed Zaki

thank you 🙏for your feedback

Collapse
 
gmanrodt profile image
Greg Manrodt

Thank you for this article. I assume that you would replace your content service with the Content Delivery API that Umbraco ships OOTB? This is a great guide for getting started!